From e26d87f4fa18999c6bcfbcf32cfa85adab11acdd Mon Sep 17 00:00:00 2001 From: rtkay123 Date: Sat, 26 Jul 2025 19:24:38 +0200 Subject: feat(auth): create user call --- crates/auth/src/server/csrf_token_validation.rs | 40 ---- crates/auth/src/server/grpc.rs | 2 - crates/auth/src/server/grpc/auth.rs | 50 ----- crates/auth/src/server/grpc/interceptor.rs | 17 -- crates/auth/src/server/routes.rs | 62 ------ crates/auth/src/server/routes/authorised.rs | 232 --------------------- crates/auth/src/server/routes/discord.rs | 10 - .../auth/src/server/routes/discord/discord_auth.rs | 58 ------ 8 files changed, 471 deletions(-) delete mode 100644 crates/auth/src/server/csrf_token_validation.rs delete mode 100644 crates/auth/src/server/grpc.rs delete mode 100644 crates/auth/src/server/grpc/auth.rs delete mode 100644 crates/auth/src/server/grpc/interceptor.rs delete mode 100644 crates/auth/src/server/routes.rs delete mode 100644 crates/auth/src/server/routes/authorised.rs delete mode 100644 crates/auth/src/server/routes/discord.rs delete mode 100644 crates/auth/src/server/routes/discord/discord_auth.rs (limited to 'crates/auth/src/server') diff --git a/crates/auth/src/server/csrf_token_validation.rs b/crates/auth/src/server/csrf_token_validation.rs deleted file mode 100644 index 94424c8..0000000 --- a/crates/auth/src/server/csrf_token_validation.rs +++ /dev/null @@ -1,40 +0,0 @@ -use anyhow::{Context, anyhow}; -use oauth2::CsrfToken; -use tower_sessions::{CachingSessionStore, SessionStore, session::Id}; -use tower_sessions_moka_store::MokaStore; -use tower_sessions_sqlx_store::PostgresStore; - -use crate::{ - error::AppError, - server::{CSRF_TOKEN, routes::authorised::AuthRequest}, -}; - -pub async fn csrf_token_validation_workflow( - auth_request: &AuthRequest, - store: &CachingSessionStore, - oauth_session_id: Id, -) -> Result<(), AppError> { - let oauth_session = store.load(&oauth_session_id).await.unwrap().unwrap(); - - // Extract the CSRF token from the session - let csrf_token_serialized = oauth_session - .data - .get(CSRF_TOKEN) - .context("failed to get value from session")?; - let csrf_token = serde_json::from_value::(csrf_token_serialized.clone()) - .context("CSRF token not found in session")? - .to_owned(); - - // Cleanup the CSRF token session - store - .delete(&oauth_session_id) - .await - .context("Failed to destroy old session")?; - - // Validate CSRF token is the same as the one in the auth request - if *csrf_token.secret() != auth_request.state { - return Err(anyhow!("CSRF token mismatch").into()); - } - - Ok(()) -} diff --git a/crates/auth/src/server/grpc.rs b/crates/auth/src/server/grpc.rs deleted file mode 100644 index 0fd775b..0000000 --- a/crates/auth/src/server/grpc.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod auth; -pub mod interceptor; diff --git a/crates/auth/src/server/grpc/auth.rs b/crates/auth/src/server/grpc/auth.rs deleted file mode 100644 index fb00291..0000000 --- a/crates/auth/src/server/grpc/auth.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::str::FromStr; - -use jsonwebtoken::DecodingKey; -use sellershut_core::auth::{ValidationRequest, ValidationResponse, auth_server::Auth}; -use tonic::{Request, Response, Status, async_trait}; -use tower_sessions::{SessionStore, session::Id}; -use tracing::warn; - -use crate::{auth::Claims, state::AppHandle}; - -#[async_trait] -impl Auth for AppHandle { - async fn validate_auth_token( - &self, - request: Request, - ) -> Result, Status> { - let token = request.into_inner().token; - - let token = jsonwebtoken::decode::( - &token, - &DecodingKey::from_secret(self.local_config.oauth.jwt_encoding_key.as_bytes()), - &jsonwebtoken::Validation::default(), - ); - - match token { - Ok(value) => { - let session_id = value.claims.sid; - let store = &self.session_store; - match Id::from_str(&session_id) { - Ok(ref id) => { - if let Ok(Some(_)) = store.load(id).await { - return Ok(Response::new(ValidationResponse { valid: true })); - } else { - return Ok(Response::new(Default::default())); - } - } - Err(e) => { - warn!("{e}"); - - return Ok(Response::new(Default::default())); - } - } - } - Err(e) => { - warn!("{e}"); - Ok(Response::new(ValidationResponse::default())) - } - } - } -} diff --git a/crates/auth/src/server/grpc/interceptor.rs b/crates/auth/src/server/grpc/interceptor.rs deleted file mode 100644 index 155a306..0000000 --- a/crates/auth/src/server/grpc/interceptor.rs +++ /dev/null @@ -1,17 +0,0 @@ -use tonic::{ - Status, - service::{Interceptor, interceptor::InterceptedService}, - transport::Channel, -}; -use tracing::Span; - -pub type Intercepted = InterceptedService; - -#[derive(Clone, Copy)] -pub struct MyInterceptor; - -impl Interceptor for MyInterceptor { - fn call(&mut self, request: tonic::Request<()>) -> Result, Status> { - Ok(request) - } -} diff --git a/crates/auth/src/server/routes.rs b/crates/auth/src/server/routes.rs deleted file mode 100644 index 6773962..0000000 --- a/crates/auth/src/server/routes.rs +++ /dev/null @@ -1,62 +0,0 @@ -pub mod authorised; -pub mod discord; - -use std::fmt::Display; - -use axum::response::IntoResponse; -use serde::Deserialize; - -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Provider { - Discord, -} - -impl Display for Provider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Provider::Discord => "discord", - } - ) - } -} - -pub async fn health_check() -> impl IntoResponse { - let name = env!("CARGO_PKG_NAME"); - let ver = env!("CARGO_PKG_VERSION"); - - format!("{name} v{ver} is live") -} - -#[cfg(test)] -mod tests { - use axum::{ - body::Body, - http::{Request, StatusCode}, - }; - use sqlx::PgPool; - use stack_up::Services; - use tower::ServiceExt; - - use crate::{ - server::{self, test_config}, - state::AppState, - }; - - #[sqlx::test] - async fn health_check(pool: PgPool) { - let services = Services { postgres: pool }; - let (state, _) = AppState::create(services, &test_config()).await.unwrap(); - let app = server::router(state); - - let response = app - .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - } -} diff --git a/crates/auth/src/server/routes/authorised.rs b/crates/auth/src/server/routes/authorised.rs deleted file mode 100644 index 32dd929..0000000 --- a/crates/auth/src/server/routes/authorised.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::{str::FromStr, time::Duration}; - -use anyhow::Context; -use axum::{ - extract::{Query, State}, - http::HeaderMap, - response::{IntoResponse, Redirect}, -}; -use axum_extra::{TypedHeader, headers}; -use oauth2::{AuthorizationCode, TokenResponse}; -use reqwest::{StatusCode, header::SET_COOKIE}; -use sellershut_core::profile::CreateUserRequest; -use serde::{Deserialize, Serialize}; -use sqlx::types::uuid; -use time::OffsetDateTime; -use tower_sessions::{ - SessionStore, - session::{Id, Record}, -}; -use uuid::Uuid; - -use crate::{ - auth::Claims, - error::AppError, - server::{ - OAUTH_CSRF_COOKIE, csrf_token_validation::csrf_token_validation_workflow, routes::Provider, - }, - state::AppHandle, -}; - -#[derive(Debug, Deserialize)] -pub struct AuthRequest { - provider: Provider, - code: String, - pub state: String, -} - -#[derive(Debug, Deserialize, Serialize)] -struct User { - id: String, - avatar: Option, - username: String, - discriminator: String, - verified: bool, - email: String, -} - -#[derive(Debug, Deserialize, Serialize)] -struct DbUser { - id: Uuid, - email: String, - created_at: OffsetDateTime, - updated_at: OffsetDateTime, -} - -/// The cookie to store the session id for user information. -const SESSION_COOKIE: &str = "info"; -const SESSION_DATA_KEY: &str = "data"; - -pub async fn login_authorised( - Query(query): Query, - State(state): State, - TypedHeader(cookies): TypedHeader, -) -> Result { - let provider = query.provider.to_string(); - let oauth_session_id = Id::from_str( - cookies - .get(OAUTH_CSRF_COOKIE) - .context("missing session cookie")?, - )?; - csrf_token_validation_workflow(&query, &state.session_store, oauth_session_id).await?; - - let client = state.http_client.clone(); - let store = state.session_store.clone(); - - // Get an auth token - let token = state - .discord_client - .exchange_code(AuthorizationCode::new(query.code.clone())) - .request_async(&client) - .await - .context("failed in sending request request to authorisation server")?; - - let user_data = client - // https://discord.com/developers/docs/resources/user#get-current-user - .get("https://discordapp.com/api/users/@me") - .bearer_auth(token.access_token().secret()) - .send() - .await - .context("failed in sending request to target Url")? - .json::() - .await - .context("failed to deserialise response as JSON")?; - - dbg!(&user_data); - - let user_data: User = serde_json::from_value(user_data)?; - - if !user_data.verified { - return Ok((StatusCode::UNAUTHORIZED, "email is not verified").into_response()); - } - - // Create a new session filled with user data - let session_id = Id(i128::from_le_bytes(uuid::Uuid::new_v4().to_bytes_le())); - - let mut transaction = state.services.postgres.begin().await?; - - let user = sqlx::query_as!( - DbUser, - " - select - p.* - from - auth_user p - inner join - oauth_account a - on - p.id=a.user_id - where a.provider_id = $1 and a.provider_user_id = $2 - ", - provider, - user_data.id - ) - .fetch_optional(&mut *transaction) - .await?; - - let user = if let Some(user) = user { - user - } else { - let uuid = uuid::Uuid::now_v7(); - let user = sqlx::query_as!( - DbUser, - "insert into auth_user (id, email) values ($1, $2) - on conflict (email) do update - set email = excluded.email - returning *; - ", - uuid, - user_data.email, - ) - .fetch_one(&mut *transaction) - .await?; - - sqlx::query_as!( - DbUser, - "with upsert as ( - insert into oauth_account (provider_id, provider_user_id, user_id) values ($1, $2, $3) - on conflict (provider_id, provider_user_id) do update - set provider_id = excluded.provider_id -- no-op - returning user_id - ) - select u.* - from upsert - join auth_user u on u.id = upsert.user_id; - ", - provider, - user_data.id, - user.id - ) - .fetch_one(&mut *transaction) - .await? - }; - - let exp = OffsetDateTime::now_utc() + Duration::from_secs(15 * 60); - - let claims = Claims { - sub: user.id, - exp: exp.unix_timestamp(), - iss: "sellershut".to_owned(), - sid: session_id.to_string(), - aud: "sellershut".to_owned(), - iat: OffsetDateTime::now_utc().unix_timestamp(), - }; - - let token = jsonwebtoken::encode( - &jsonwebtoken::Header::default(), - &claims, - &jsonwebtoken::EncodingKey::from_secret( - state.local_config.oauth.jwt_encoding_key.as_bytes(), - ), - )?; - - let user_request = CreateUserRequest{ - email: user_data.email.to_owned(), - avatar: user_data.avatar.as_ref().map(|value| { - format!( - "https://cdn.discordapp.com/avatars/{}/{value}", - user_data.id - ) - }) - }; - - - store - .create(&mut Record { - id: session_id, - data: [( - SESSION_DATA_KEY.to_string(), - serde_json::to_value(user_data).unwrap(), - )] - .into(), - expiry_date: time::OffsetDateTime::now_utc() - + Duration::from_secs(state.local_config.oauth.session_lifespan), - }) - .await - .context("failed in inserting serialised value into session")?; - - sqlx::query!( - "insert into token (user_id, token, session_id) values ($1, $2, $3)", - user.id, - token, - session_id.to_string() - ) - .execute(&mut *transaction) - .await?; - - let cookie = format!("{SESSION_COOKIE}={session_id}; SameSite=Lax; HttpOnly; Secure; Path=/"); - - let mut profile_client = state.profile_client.clone(); - let resp = profile_client.create_user(user_request).await?.into_inner(); - let user_id = resp.temp_id; - - let mut headers = HeaderMap::new(); - headers.insert( - SET_COOKIE, - cookie.parse().context("failed to parse cookie")?, - ); - - transaction.commit().await?; - - Ok((headers, Redirect::to(&format!("/?user={user_id}&token={token}"))).into_response()) -} diff --git a/crates/auth/src/server/routes/discord.rs b/crates/auth/src/server/routes/discord.rs deleted file mode 100644 index e1a834f..0000000 --- a/crates/auth/src/server/routes/discord.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod discord_auth; -use axum::{Router, routing::get}; - -use crate::state::AppHandle; - -pub fn discord_router(state: AppHandle) -> Router { - Router::new() - .route("/auth/discord", get(discord_auth::discord_auth)) - .with_state(state) -} diff --git a/crates/auth/src/server/routes/discord/discord_auth.rs b/crates/auth/src/server/routes/discord/discord_auth.rs deleted file mode 100644 index a45de86..0000000 --- a/crates/auth/src/server/routes/discord/discord_auth.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::time::Duration; - -use anyhow::Context; -use axum::{ - extract::State, - http::HeaderMap, - response::{IntoResponse, Redirect}, -}; -use oauth2::{CsrfToken, Scope}; -use reqwest::header::SET_COOKIE; -use sqlx::types::uuid; -use tower_sessions::{ - SessionStore, - session::{Id, Record}, -}; - -use crate::{ - error::AppError, - server::{CSRF_TOKEN, OAUTH_CSRF_COOKIE}, - state::AppHandle, -}; - -pub async fn discord_auth(State(state): State) -> Result { - let (auth_url, csrf_token) = state - .discord_client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new("identify".to_string())) - .url(); - - // Store the token in the session and retrieve the session cookie. - let session_id = Id(i128::from_le_bytes(uuid::Uuid::new_v4().to_bytes_le())); - let store = state.session_store.clone(); - - store - .create(&mut Record { - id: session_id, - data: [( - CSRF_TOKEN.to_string(), - serde_json::to_value(csrf_token).unwrap(), - )] - .into(), - expiry_date: time::OffsetDateTime::now_utc() - + Duration::from_secs(state.local_config.oauth.session_lifespan), - }) - .await - .context("failed in inserting CSRF token into session")?; - - // Attach the session cookie to the response header - let cookie = - format!("{OAUTH_CSRF_COOKIE}={session_id}; SameSite=Lax; HttpOnly; Secure; Path=/"); - let mut headers = HeaderMap::new(); - headers.insert( - SET_COOKIE, - cookie.parse().context("failed to parse cookie")?, - ); - - Ok((headers, Redirect::to(auth_url.as_ref()))) -} -- cgit v1.2.3