use axum::{extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode}, response::{Html, IntoResponse}, Extension, Form}; use chrono::{SecondsFormat, Utc}; use fully_pub::fully_pub; use log::{debug, error, info}; use minijinja::context; use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; use kernel::models::{authorization::Authorization, config::AppAuthorizeFlow}; use utils::get_random_alphanumerical; use crate::{ renderer::TemplateRenderer, services::oauth2::{parse_scope, verify_redirect_uri}, token_claims::UserTokenClaims, AppState }; #[derive(Debug, Serialize, Deserialize)] #[fully_pub] /// query params described in [RFC6749 section 4.1.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1) struct AuthorizationParams { response_type: String, client_id: String, scope: String, redirect_uri: String, /// An opaque value used by the client to maintain state between the request and callback state: String, nonce: Option } fn redirect_to_client( authorization_code: &str, authorization_params: &AuthorizationParams ) -> impl IntoResponse { let target_url = format!("{}?code={}&state={}", authorization_params.redirect_uri, authorization_code, authorization_params.state ); debug!("Redirecting to {}", target_url); let mut headers = HeaderMap::new(); headers.insert("Location", HeaderValue::from_str(&target_url).unwrap()); ( StatusCode::FOUND, headers, Html("Redirecting to client…") ).into_response() } /// The authorization endpoint is used to interact with the resource owner and obtain an authorization grant. /// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1 pub async fn authorize_form( State(app_state): State, Extension(token_claims): Extension, Extension(renderer): Extension, query_params: Query ) -> impl IntoResponse { let Query(authorization_params) = query_params; dbg!(&authorization_params); // 1. Verify the app details let app = match app_state.config.applications .iter() .find(|a| a.client_id == authorization_params.client_id) { Some(app) => app, None => { return ( StatusCode::BAD_REQUEST, Html("Invalid client_id query params, app not found.") ).into_response(); } }; // 1.1. Verify that the app redirect_uri is authorized if !verify_redirect_uri(app, &authorization_params.redirect_uri) { return ( StatusCode::BAD_REQUEST, Html("Unauthorized redirect_uri.") ).into_response(); } // 1.3. Parse and validate redirect_uri. // Note: for now, we only support HTTP(s) redirect URLs. let parsed_redirect_uri: Url = match Url::parse(&authorization_params.redirect_uri) { Ok(url) => url, Err(_) => { return ( StatusCode::BAD_REQUEST, Html("Invalid redirect URL. Could not parse as HTTP(S) URL.") ).into_response(); } }; // 1.4. Parse and validate scopes let scopes = match parse_scope(&authorization_params.scope) { Ok(v) => v, Err(_err) => { return ( StatusCode::BAD_REQUEST, Html("Invalid scopes. Scopes must be space-delimited and snake_case.") ).into_response(); } }; // 2. Check if the app is already authorized let authorizations_res = sqlx::query_as::<_, Authorization>( "SELECT * FROM authorizations WHERE user_id = $1 AND client_id = $2 AND scopes = $3" ) .bind(&token_claims.sub) .bind(&authorization_params.client_id) .bind(sqlx::types::Json(&scopes)) .fetch_one(&app_state.db.0) .await; match authorizations_res { Ok(existing_authorization) => { info!("Reusing existing authorization: {}", &existing_authorization.id); // Create new auth code let authorization_code = get_random_alphanumerical(32); // Update last used timestamp for this authorization let _result = sqlx::query("UPDATE authorizations SET code = $2, nonce = $3, last_used_at = $4 WHERE id = $1") .bind(existing_authorization.id) .bind(authorization_code.clone()) .bind(authorization_params.nonce.clone()) .bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)) .execute(&app_state.db.0) .await.unwrap(); // Authorization already given, just redirect to the app return redirect_to_client( &authorization_code, &authorization_params ).into_response() }, Err(sqlx::Error::RowNotFound) => { debug!("Authorization not found."); }, Err(err) => { error!("Cannot get existing authorization. {}", err); return ( StatusCode::INTERNAL_SERVER_ERROR, Html("Internal server error: Failed to verify conditions.") ).into_response(); } } // 3. Check for implicit/explicit flow if app.authorize_flow == AppAuthorizeFlow::Implicit { debug!("Performing Implicit authorization flow."); // Authorization already given, just redirect to the app return perform_authorize( State(app_state), Extension(token_claims), Form(authorization_params) ).await.into_response() } // 4. Show form that POST to authorize debug!("Performing explicit authorization flow."); renderer .render( "pages/authorize", context!( app => app, authorization_params => authorization_params, redirect_uri_host => parsed_redirect_uri.host_str() ) ) .into_response() } pub async fn perform_authorize( State(app_state): State, Extension(token_claims): Extension, Form(authorize_form): Form ) -> impl IntoResponse { dbg!(&authorize_form); // 1. Get the app details let app = match app_state.config.applications .iter() .find(|a| a.client_id == authorize_form.client_id) { Some(app) => app, None => { return ( StatusCode::BAD_REQUEST, Html("Invalid client_id, app not found.") ).into_response(); } }; // 1.2. Parse and validate scope to use in DB let scopes = match parse_scope(&authorize_form.scope) { Ok(v) => v, Err(_err) => { return ( StatusCode::BAD_REQUEST, Html("Invalid scopes.") ).into_response(); } }; // 2. Create an authorization code let authorization_code = get_random_alphanumerical(32); let authorization = Authorization { id: Uuid::new_v4().to_string(), user_id: token_claims.sub, client_id: app.client_id.clone(), scopes: sqlx::types::Json(scopes), code: authorization_code.clone(), nonce: authorize_form.nonce.clone(), last_used_at: Some(Utc::now()), created_at: Utc::now(), }; // 3. Save authorization in DB with state let res = sqlx::query(" INSERT INTO authorizations (id, user_id, client_id, scopes, code, nonce, last_used_at, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ") .bind(authorization.id.clone()) .bind(authorization.user_id) .bind(authorization.client_id) .bind(authorization.scopes) .bind(authorization.code) .bind(authorization.nonce) .bind(authorization.last_used_at.map(|x| x.to_rfc3339_opts(SecondsFormat::Millis, true))) .bind(authorization.created_at.to_rfc3339_opts(SecondsFormat::Millis, true)) .execute(&app_state.db.0) .await; if let Err(err) = res { error!("Failed to save authorization in DB. {}", err); return (StatusCode::INTERNAL_SERVER_ERROR, Html("Internal server error: Failed to process authorization form.")).into_response(); } info!("Created authorization {}", &authorization.id); // 4. Redirect to the app with the authorization code and state redirect_to_client(&authorization_code, &authorize_form).into_response() }