use anyhow::{Context, Result}; use async_session::{Session, SessionStore as _}; use axum::{ extract::{FromRequestParts, Query, State}, http::request::Parts, response::{IntoResponse, Redirect, Response}, routing::get, RequestPartsExt, Router, }; use axum_extra::{ extract::cookie::{Cookie, CookieJar, SameSite}, headers, TypedHeader, }; use diesel::prelude::*; use oauth2::{ basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, TokenResponse, TokenUrl, }; use serde::{Deserialize, Serialize}; use tracing::{debug, trace_span}; use crate::{app_error::AppError, app_state::AppState, schema, settings::Settings}; pub fn new_oauth_client(settings: &Settings) -> Result { Ok(BasicClient::new( ClientId::new(settings.auth.client_id.clone()), Some(ClientSecret::new(settings.auth.client_secret.clone())), AuthUrl::new(settings.auth.auth_url.clone()) .context("failed to create new authorization server URL")?, Some( TokenUrl::new(settings.auth.token_url.clone()) .context("failed to create new token endpoint URL")?, ), ) .set_redirect_uri( RedirectUrl::new(settings.auth.redirect_url.clone()) .context("failed to create new redirection URL")?, )) } pub fn new_router() -> Router { Router::new() .route("/login", get(propel_auth)) .route("/callback", get(login_authorized)) .route("/logout", get(logout)) } pub async fn propel_auth(State(state): State) -> impl IntoResponse { let csrf_token = CsrfToken::new_random(); let (auth_url, _csrf_token) = state .oauth_client .authorize_url(|| csrf_token) // .add_scopes(vec![Scope::new("openid".to_string())]) .url(); // FIXME: check CSRF token Redirect::to(auth_url.as_ref()) } pub async fn logout( State(state): State, TypedHeader(cookies): TypedHeader, ) -> Result { let cookie = cookies .get(state.settings.auth.cookie_name.as_str()) .context("couldn't get session cookie")?; let session_id = Session::id_from_cookie_value(cookie)?; state .db_pool .get() .await? .interact(move |conn| { diesel::delete(schema::browser_sessions::table) .filter(schema::browser_sessions::id.eq(session_id)) .execute(conn) }) .await .unwrap()?; // FIXME: call logout endpoint of OIDC provider Ok(Redirect::to(&state.settings.base_path)) } #[derive(Debug, Deserialize)] pub struct AuthRequestQuery { code: String, state: String, // CSRF token } pub const AUTH_INFO_SESSION_KEY: &'static str = "user"; #[derive(Debug, Deserialize, Serialize)] pub struct AuthInfo { pub sub: String, pub email: String, } async fn get_user_info( settings: &Settings, access_token: &oauth2::AccessToken, ) -> Result { let client = reqwest::Client::new(); Ok(client .get(settings.auth.userinfo_url.as_str()) .bearer_auth(access_token.secret()) .send() .await? .json() .await?) } pub async fn login_authorized( Query(query): Query, State(state): State, jar: CookieJar, ) -> Result { let response = state .oauth_client .exchange_code(AuthorizationCode::new(query.code.clone())) .request_async(async_http_client) .await?; let user_info = get_user_info(&state.settings, response.access_token()).await?; let mut session = Session::new(); session.insert(AUTH_INFO_SESSION_KEY, &user_info)?; let cookie_value = state .session_store .store_session(session) .await? .context("cookie value from store_session() is None")?; let jar = jar.add( Cookie::build((state.settings.auth.cookie_name.clone(), cookie_value)) .same_site(SameSite::Lax) .http_only(true) .path("/"), ); Ok((jar, Redirect::to(&format!("{}/", state.settings.base_path)))) } pub struct AuthRedirect { base_path: String, } impl AuthRedirect { pub fn new(base_path: &str) -> Self { Self { base_path: base_path.to_string(), } } } impl IntoResponse for AuthRedirect { fn into_response(self) -> Response { Redirect::to(&format!("{}/auth/login", self.base_path)).into_response() } } impl FromRequestParts for AuthInfo { type Rejection = AuthRedirect; async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result>::Rejection> { let _ = trace_span!("AuthInfo from_request_parts()").enter(); let jar = parts.extract::().await.unwrap(); let session_cookie = jar .get(&state.settings.auth.cookie_name) .ok_or(AuthRedirect::new(&state.settings.base_path))?; debug!("session cookie loaded"); let session = state .session_store .load_session(session_cookie.value().to_string()) .await .unwrap() .ok_or(AuthRedirect::new(&state.settings.base_path))?; debug!("session loaded"); let user = session .get::(AUTH_INFO_SESSION_KEY) .ok_or(AuthRedirect::new(&state.settings.base_path))?; Ok(user) } }