From e3cb6ea694a461d18a868b215235012230227482 Mon Sep 17 00:00:00 2001 From: David Hadley Date: Tue, 4 Aug 2026 15:16:01 +0100 Subject: [PATCH] feat(auth-gateway): add user info endpoint --- backend/auth-core/src/oidc.rs | 51 +++++++++++++++---- backend/auth-gateway/src/auth_session_data.rs | 16 ++++++ backend/auth-gateway/src/callback.rs | 12 +++++ backend/auth-gateway/src/main.rs | 2 + backend/auth-gateway/src/userinfo.rs | 34 +++++++++++++ 5 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 backend/auth-gateway/src/userinfo.rs diff --git a/backend/auth-core/src/oidc.rs b/backend/auth-core/src/oidc.rs index 0b0a27a34..a5762e4ea 100644 --- a/backend/auth-core/src/oidc.rs +++ b/backend/auth-core/src/oidc.rs @@ -1,8 +1,8 @@ use crate::config::CommonConfig; use anyhow::anyhow; use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; -use oauth2::{ClientId, ClientSecret, EndpointMaybeSet, EndpointNotSet, EndpointSet, reqwest}; -use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreTokenResponse}; +use oauth2::{ClientId, ClientSecret, reqwest}; +use openidconnect::core::CoreProviderMetadata; use openidconnect::{IssuerUrl, RefreshToken}; use sea_orm::{Database, DatabaseConnection}; use sodiumoxide::crypto::box_::{PublicKey, SecretKey}; @@ -26,13 +26,42 @@ pub async fn create_db_connection(config: &CommonConfig) -> Result, +} + +impl openidconnect::AdditionalClaims for DiamondAdditionalClaims {} + +pub type DiamondIdTokenFields = openidconnect::IdTokenFields< + DiamondAdditionalClaims, + openidconnect::EmptyExtraTokenFields, + openidconnect::core::CoreGenderClaim, + openidconnect::core::CoreJweContentEncryptionAlgorithm, + openidconnect::core::CoreJwsSigningAlgorithm, +>; + +pub type DiamondTokenResponse = + openidconnect::StandardTokenResponse; + +pub type OidcClient = openidconnect::Client< + DiamondAdditionalClaims, + openidconnect::core::CoreAuthDisplay, + openidconnect::core::CoreGenderClaim, + openidconnect::core::CoreJweContentEncryptionAlgorithm, + openidconnect::core::CoreJsonWebKey, + openidconnect::core::CoreAuthPrompt, + openidconnect::StandardErrorResponse, + DiamondTokenResponse, + openidconnect::core::CoreTokenIntrospectionResponse, + openidconnect::core::CoreRevocableToken, + openidconnect::core::CoreRevocationErrorResponse, + openidconnect::EndpointSet, + openidconnect::EndpointNotSet, + openidconnect::EndpointNotSet, + openidconnect::EndpointNotSet, + openidconnect::EndpointMaybeSet, + openidconnect::EndpointMaybeSet, >; pub async fn create_oidc_client(config: &CommonConfig) -> Result<(OidcClient, reqwest::Client)> { @@ -48,7 +77,7 @@ pub async fn create_oidc_client(config: &CommonConfig) -> Result<(OidcClient, re ) .await?; - let oidc_client = CoreClient::from_provider_metadata( + let oidc_client = OidcClient::from_provider_metadata( provider_metadata, ClientId::new(config.client_id.to_string()), if config.client_secret.is_empty() { @@ -72,7 +101,7 @@ pub async fn exchange_refresh_token( oidc_client: &OidcClient, http_client: &reqwest::Client, refresh_token: &RefreshToken, -) -> Result { +) -> Result { let token_response = oidc_client .exchange_refresh_token(refresh_token)? .request_async(http_client) diff --git a/backend/auth-gateway/src/auth_session_data.rs b/backend/auth-gateway/src/auth_session_data.rs index 0a83e9112..77d64bcf0 100644 --- a/backend/auth-gateway/src/auth_session_data.rs +++ b/backend/auth-gateway/src/auth_session_data.rs @@ -23,17 +23,24 @@ pub struct TokenSessionData { pub access_token: AccessToken, pub access_token_expires_at: DateTime, pub refresh_token: RefreshToken, + pub name: Option, + pub preferred_username: Option, + pub fedid: Option, } impl TokenSessionData { pub const SESSION_KEY: &str = "token_session_data"; + #[allow(clippy::too_many_arguments)] pub fn new( issuer: IssuerUrl, subject: SubjectIdentifier, access_token: AccessToken, access_token_expires_at: DateTime, refresh_token: RefreshToken, + name: Option, + preferred_username: Option, + fedid: Option, ) -> Self { Self { issuer, @@ -41,6 +48,9 @@ impl TokenSessionData { access_token, access_token_expires_at, refresh_token, + name, + preferred_username, + fedid, } } @@ -48,6 +58,9 @@ impl TokenSessionData { token_response: &T, issuer: IssuerUrl, subject: SubjectIdentifier, + name: Option, + preferred_username: Option, + fedid: Option, ) -> Result { let access_token = token_response.access_token().clone(); let refresh_token = token_response @@ -64,6 +77,9 @@ impl TokenSessionData { access_token, access_token_expires_at, refresh_token, + name, + preferred_username, + fedid, )) } diff --git a/backend/auth-gateway/src/callback.rs b/backend/auth-gateway/src/callback.rs index 7980d37b5..49b6746c5 100644 --- a/backend/auth-gateway/src/callback.rs +++ b/backend/auth-gateway/src/callback.rs @@ -76,6 +76,18 @@ pub async fn callback( &token_response, claims.issuer().clone(), claims.subject().clone(), + claims + .name() + .and_then(|name| name.get(None)) + .map(|n| n.to_string()), + claims + .preferred_username() + .map(|username| username.to_string()), + claims + .additional_claims() + .fedid + .as_ref() + .map(|fedid| fedid.to_string()), )?; write_token_to_database(&state.database_connection, &token_data, &state.public_key).await?; session diff --git a/backend/auth-gateway/src/main.rs b/backend/auth-gateway/src/main.rs index a9c9eccdf..ca0228473 100644 --- a/backend/auth-gateway/src/main.rs +++ b/backend/auth-gateway/src/main.rs @@ -3,6 +3,7 @@ mod callback; mod config; mod login; mod state; +mod userinfo; use auth_core::middleware::inject_token::inject_token_with; use clap::Parser; @@ -96,6 +97,7 @@ fn create_router(state: Arc, graph_url: String) -> Router { .route("/auth/login", get(login::login)) .route("/auth/callback", get(callback::callback)) .route("/auth/logout", post(logout)) + .route("/auth/me", get(userinfo::userinfo)) .route("/healthcheck", get(auth_core::healthcheck::healthcheck)) .layer(session_layer) .layer( diff --git a/backend/auth-gateway/src/userinfo.rs b/backend/auth-gateway/src/userinfo.rs new file mode 100644 index 000000000..7f46f3476 --- /dev/null +++ b/backend/auth-gateway/src/userinfo.rs @@ -0,0 +1,34 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Json}, +}; +use serde::Serialize; +use tower_sessions::Session; + +use crate::Result; +use crate::auth_session_data::TokenSessionData; + +#[derive(Debug, Serialize)] +pub struct UserInfo { + pub name: Option, + pub preferred_username: Option, + pub fedid: Option, +} + +pub async fn userinfo(session: Session) -> Result { + let token_session_data: Option = + session.get(TokenSessionData::SESSION_KEY).await?; + + match token_session_data { + Some(token) => Ok(( + StatusCode::OK, + Json(UserInfo { + name: token.name, + preferred_username: token.preferred_username, + fedid: token.fedid, + }), + ) + .into_response()), + None => Ok(StatusCode::UNAUTHORIZED.into_response()), + } +}