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::auth::{RegisterUserRequest, auth_server::Auth, register_user_request::AccountDetails}; use serde::{Deserialize, Serialize}; use sqlx::types::uuid; use time::OffsetDateTime; use tonic::IntoRequest; 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, grpc::auth::DbUser, routes::Provider, }, state::AppHandle, }; #[derive(Debug, Deserialize)] pub struct AuthRequest { provider: Provider, code: String, pub state: String, } #[derive(Debug, Deserialize, Serialize, Clone)] struct User { id: String, avatar: Option, username: String, discriminator: String, verified: bool, email: String, } /// 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")?; 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 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(&state.services.postgres) .await?; let user = if let Some(user) = user { user.id } else { let data = user_data.clone(); let request = RegisterUserRequest { email: data.email, account: Some(AccountDetails { provider_id: provider, provider_user_id: data.id, }) }; let resp = state.register_user(request.into_request()).await.unwrap().into_inner(); Uuid::parse_str(&resp.auth_id).unwrap() }; let exp = OffsetDateTime::now_utc() + Duration::from_secs(15 * 60); let claims = Claims { sub: user, 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(), ), )?; 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, token, session_id.to_string() ) .execute(&state.services.postgres) .await?; let cookie = format!("{SESSION_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(&format!("/?token={token}"))).into_response()) }