use std::sync::Arc;
use axum::{
Router, middleware,
routing::{get, post},
};
use tracing::info;
use super::super::{
AuthMeState, AuthPkceState, Server, auth_callback, auth_me, auth_start, oidc_auth_middleware,
};
use crate::auth::{anon_signup, mfa_challenge, mfa_confirm, mfa_enroll, mfa_unenroll, mfa_verify};
impl Server {
pub(super) fn mount_auth_routes(&self, mut app: Router) -> Router {
if let (Some(store), Some(client)) = (&self.pkce_store, &self.oidc_server_client) {
let auth_state = Arc::new(AuthPkceState {
pkce_store: Arc::clone(store),
oidc_client: Arc::clone(client),
http_client: Arc::new(
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_default(),
),
post_login_redirect_uri: None,
});
let auth_router = Router::new()
.route("/auth/start", get(auth_start))
.route("/auth/callback", get(auth_callback))
.with_state(auth_state);
app = app.merge(auth_router);
info!("PKCE auth routes mounted: GET /auth/start, GET /auth/callback");
}
#[cfg(feature = "auth-saml")]
if let Some(ref saml) = self.saml_state {
app = app.merge(fraiseql_auth::saml::saml_routes(saml.clone()));
info!("SAML routes mounted: GET /auth/saml/login, POST /auth/saml/acs");
}
if let Some(ref social) = self.social_login {
app = app.merge(social_router(Arc::clone(social)));
info!(
providers = ?social.provider_names(),
"Social login routes mounted: GET /auth/v1/{{providers,authorize,callback}}"
);
}
if let Some(ref anon) = self.anon_signup_state {
let anon_router = Router::new()
.route("/auth/v1/signup", post(anon_signup))
.with_state(Arc::clone(anon));
app = app.merge(anon_router);
info!("Anonymous signup route mounted: POST /auth/v1/signup");
}
if let Some(ref otp) = self.otp_state {
let otp_router = Router::new()
.route("/auth/v1/otp", post(crate::auth::otp_send))
.route("/auth/v1/verify", post(crate::auth::otp_verify))
.with_state(Arc::clone(otp));
app = app.merge(otp_router);
info!("Email OTP routes mounted: POST /auth/v1/otp, POST /auth/v1/verify");
}
if let Some(ref pw) = self.local_password_state {
app = app.merge(fraiseql_auth::local_password_routes(Arc::clone(pw)));
info!(
"Local password routes mounted: POST /auth/v1/password/{{signup,login,reset}}, \
POST /auth/v1/password/reset/confirm"
);
}
if let Some(ref verify) = self.email_verification_state {
app = app.merge(fraiseql_auth::email_verification_routes(Arc::clone(verify)));
info!(
"Email verification routes mounted: POST \
/auth/v1/email/verify/{{start,confirm}}"
);
}
if let Some(ref mfa) = self.mfa_state {
let mfa_router = Router::new()
.route("/auth/v1/mfa/enroll", post(mfa_enroll))
.route("/auth/v1/mfa/confirm", post(mfa_confirm))
.route("/auth/v1/mfa/challenge", post(mfa_challenge))
.route("/auth/v1/mfa/verify", post(mfa_verify))
.route("/auth/v1/mfa/unenroll", post(mfa_unenroll))
.with_state(Arc::clone(mfa));
app = app.merge(mfa_router);
info!(
"TOTP MFA routes mounted: POST \
/auth/v1/mfa/{{enroll,confirm,challenge,verify,unenroll}}"
);
}
if let (Some(ref validator), Some(me_cfg)) = (
&self.oidc_validator,
self.config.auth.as_ref().and_then(|a| a.me.as_ref()).filter(|m| m.enabled),
) {
let me_state = Arc::new(AuthMeState {
expose_claims: me_cfg.expose_claims.clone(),
});
let auth_state = self.oidc_auth_state(Arc::clone(validator));
let me_router = Router::new()
.route("/auth/me", get(auth_me))
.route_layer(middleware::from_fn_with_state(auth_state, oidc_auth_middleware))
.with_state(me_state);
app = app.merge(me_router);
info!(
expose_claims = ?me_cfg.expose_claims,
"Session identity route mounted: GET /auth/me"
);
}
if let Some(ref rev_mgr) = self.revocation_manager {
if let Some(ref validator) = self.oidc_validator {
let rev_state = Arc::new(crate::routes::RevocationRouteState {
revocation_manager: Arc::clone(rev_mgr),
});
let auth_state = self.oidc_auth_state(Arc::clone(validator));
let rev_router = Router::new()
.route("/auth/revoke", post(crate::routes::revoke_token))
.route("/auth/revoke-all", post(crate::routes::revoke_all_tokens))
.route_layer(middleware::from_fn_with_state(auth_state, oidc_auth_middleware))
.with_state(rev_state);
app = app.merge(rev_router);
info!(
"Token revocation routes mounted (auth-gated): POST /auth/revoke, POST /auth/revoke-all"
);
} else {
tracing::warn!(
"Token revocation is configured but no OIDC validator is available; \
refusing to mount /auth/revoke and /auth/revoke-all unauthenticated. \
Configure [auth] in fraiseql.toml to enable token revocation."
);
}
}
app
}
}
fn social_router(social: Arc<fraiseql_auth::MultiProviderAuthState>) -> Router {
let callback = if social.get_provider("apple").is_some() {
get(fraiseql_auth::multi_provider::callback)
.post(fraiseql_auth::multi_provider::callback_form_post)
} else {
get(fraiseql_auth::multi_provider::callback)
};
Router::new()
.route("/auth/v1/providers", get(fraiseql_auth::multi_provider::list_providers))
.route("/auth/v1/authorize", get(fraiseql_auth::multi_provider::authorize))
.route("/auth/v1/callback", callback)
.with_state(social)
}
#[cfg(test)]
mod router_construction {
use super::*;
struct NoopSessionStore;
fn double_error() -> fraiseql_auth::AuthError {
fraiseql_auth::AuthError::ConfigError {
message: "construction-only double".to_string(),
}
}
#[async_trait::async_trait]
impl fraiseql_auth::SessionStore for NoopSessionStore {
async fn create_session(
&self,
_user_id: &str,
_expires_at: u64,
) -> fraiseql_auth::Result<fraiseql_auth::TokenPair> {
Err(double_error())
}
async fn get_session(
&self,
_refresh_token_hash: &str,
) -> fraiseql_auth::Result<fraiseql_auth::SessionData> {
Err(double_error())
}
async fn revoke_session(&self, _refresh_token_hash: &str) -> fraiseql_auth::Result<()> {
Ok(())
}
async fn revoke_all_sessions(&self, _user_id: &str) -> fraiseql_auth::Result<()> {
Ok(())
}
}
#[tokio::test]
async fn social_router_constructs() {
let state = fraiseql_auth::MultiProviderAuthState::new(
Arc::new(fraiseql_auth::InMemoryStateStore::new()),
Arc::new(NoopSessionStore),
);
let _router = social_router(Arc::new(state));
}
#[tokio::test]
async fn otp_router_constructs() {
let _router: Router = Router::new()
.route("/auth/v1/otp", post(crate::auth::otp_send))
.route("/auth/v1/verify", post(crate::auth::otp_verify))
.with_state(Arc::new(fraiseql_auth::OtpRouteState {
otp_store: Arc::new(fraiseql_auth::InMemoryOtpStore::new()),
email_delivery: Arc::new(fraiseql_auth::NoopEmailDelivery),
session_store: Arc::new(NoopSessionStore),
account_store: None,
}));
}
#[tokio::test]
async fn mfa_router_constructs() {
let _router: Router = Router::new()
.route("/auth/v1/mfa/enroll", post(mfa_enroll))
.route("/auth/v1/mfa/confirm", post(mfa_confirm))
.route("/auth/v1/mfa/challenge", post(mfa_challenge))
.route("/auth/v1/mfa/verify", post(mfa_verify))
.route("/auth/v1/mfa/unenroll", post(mfa_unenroll))
.with_state(Arc::new(fraiseql_auth::MfaRouteState {
mfa_store: Arc::new(fraiseql_auth::InMemoryMfaStore::new()),
session_store: Arc::new(NoopSessionStore),
issuer: "FraiseQL".to_string(),
}));
}
}