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::header::SET_COOKIE; use serde::{Deserialize, Serialize}; use sqlx::types::uuid; use tower_sessions::{ SessionStore, session::{Id, Record}, }; use crate::{ 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, } /// 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 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: User = 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")?; // Create a new session filled with user data let session_id = Id(i128::from_le_bytes(uuid::Uuid::new_v4().to_bytes_le())); 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")?; // Store session and get corresponding cookie. let cookie = format!("{SESSION_COOKIE}={session_id}; SameSite=Lax; HttpOnly; Secure; Path=/"); // Set cookie let mut headers = HeaderMap::new(); headers.insert( SET_COOKIE, cookie.parse().context("failed to parse cookie")?, ); Ok((headers, Redirect::to("/"))) }