use std::sync::Arc;
use foundation_http::shared::context::ContextBag;
use foundation_http::SimpleIncomingRequest;
use foundation_netio::shared::http::SendSafeBody;
use serde::{Deserialize, Serialize};
use super::super::config::IdpConfig;
use super::super::models::{AuthorizationCode, DeviceCode, RefreshToken};
use super::super::services::{TokenService, TokenServiceError};
use super::super::services::user_service;
use super::super::services::{PowService, PowSolution};
use super::super::services::{WebAuthnService, TosService};
use super::super::services::{
WebAuthnRegisterFinishRequest, WebAuthnAuthFinishRequest,
PasskeyLoginStartRequest, PasskeyLoginFinishRequest,
};
use super::super::storage::{
self, HandlerStorage, StorageOpError, find_client_by_id, find_user_by_email, update_user_lockout,
};
use foundation_db::{KeyValueStore, MemoryStorage, core::storage_provider::QueryStore};
use crate::shared::auth_session::{UpstreamAuthSession, UpstreamAuthSessionStore};
use crate::shared::provider::{ProviderMapping, ProviderType, UpstreamProvider};
use crate::shared::upstream_client::UpstreamOidcClient;
#[derive(Debug)]
pub struct HandlerResponse {
pub status: u16,
pub body: serde_json::Value,
pub headers: Vec<(String, String)>,
}
impl HandlerResponse {
pub fn ok(body: serde_json::Value) -> Self {
Self { status: 200, body, headers: vec![] }
}
pub fn accepted(body: serde_json::Value) -> Self {
Self { status: 202, body, headers: vec![] }
}
pub fn redirect(url: &str) -> Self {
Self {
status: 302,
body: serde_json::json!({}),
headers: vec![("Location".to_string(), url.to_string())],
}
}
}
#[derive(Debug, Serialize)]
pub struct OidcDiscoveryDocument {
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub userinfo_endpoint: String,
pub jwks_uri: String,
pub introspection_endpoint: String,
pub device_authorization_endpoint: String,
pub response_types_supported: Vec<String>,
pub grant_types_supported: Vec<String>,
pub subject_types_supported: Vec<String>,
pub id_token_signing_alg_values_supported: Vec<String>,
pub scopes_supported: Vec<String>,
pub token_endpoint_auth_methods_supported: Vec<String>,
pub code_challenge_methods_supported: Vec<String>,
}
impl OidcDiscoveryDocument {
#[must_use]
pub fn from_config(config: &IdpConfig, prefix: &str) -> Self {
let base = config.issuer_url.trim_end_matches('/');
let p = prefix.trim_end_matches('/');
Self {
issuer: config.issuer_url.clone(),
authorization_endpoint: format!("{base}{p}/authorize"),
token_endpoint: format!("{base}{p}/token"),
userinfo_endpoint: format!("{base}{p}/userinfo"),
jwks_uri: format!("{base}{p}/.well-known/jwks.json"),
introspection_endpoint: format!("{base}{p}/introspect"),
device_authorization_endpoint: format!("{base}{p}/device/authorize"),
response_types_supported: vec!["code".into()],
grant_types_supported: vec![
"authorization_code".into(), "refresh_token".into(),
"client_credentials".into(), "urn:ietf:params:oauth:grant-type:device_code".into(),
],
subject_types_supported: vec!["public".into()],
id_token_signing_alg_values_supported: vec!["EdDSA".into()],
scopes_supported: vec!["openid".into(), "profile".into(), "email".into()],
token_endpoint_auth_methods_supported: vec!["client_secret_post".into(), "none".into()],
code_challenge_methods_supported: vec!["S256".into()],
}
}
}
#[derive(Debug, Serialize)]
pub struct TokenResponse {
pub access_token: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub id_token: String,
pub token_type: String,
pub expires_in: u64,
#[serde(skip_serializing_if = "String::is_empty")]
pub refresh_token: String,
pub scope: String,
}
#[derive(Debug, Serialize)]
pub struct DeviceAuthResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: String,
pub expires_in: u64,
pub interval: u32,
}
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub email: Option<String>,
pub password: Option<String>,
pub client_id: Option<String>,
pub redirect_uri: Option<String>,
pub totp: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct MfaRequest {
pub challenge_id: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
pub email: Option<String>,
pub password: Option<String>,
pub preferred_username: Option<String>,
pub given_name: Option<String>,
pub family_name: Option<String>,
pub user_values: Option<serde_json::Value>,
pub redirect_uri: Option<String>,
pub pow: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct PasswordResetRequest {
pub email: Option<String>,
pub redirect_uri: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct PasswordSetRequest {
pub password: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ChangePasswordRequest {
pub current_password: Option<String>,
pub new_password: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TokenRequest {
pub grant_type: String,
pub code: Option<String>,
pub redirect_uri: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub code_verifier: Option<String>,
pub refresh_token: Option<String>,
pub scope: Option<String>,
pub device_code: Option<String>,
}
#[derive(Debug)]
pub enum IdpError {
Internal(String),
BadRequest(String),
Unauthorized(String),
Forbidden(String),
NotFound(String),
Conflict(String),
Locked(i64),
TooManyRequests(i64),
}
impl core::fmt::Display for IdpError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Internal(s) => write!(f, "Internal error: {s}"),
Self::BadRequest(s) => write!(f, "Bad request: {s}"),
Self::Unauthorized(s) => write!(f, "Unauthorized: {s}"),
Self::Forbidden(s) => write!(f, "Forbidden: {s}"),
Self::NotFound(s) => write!(f, "Not found: {s}"),
Self::Conflict(s) => write!(f, "Conflict: {s}"),
Self::Locked(ts) => write!(f, "Account locked until {ts}"),
Self::TooManyRequests(ts) => write!(f, "Too many requests, retry after {ts}"),
}
}
}
impl std::error::Error for IdpError {}
impl From<StorageOpError> for IdpError {
fn from(e: StorageOpError) -> Self {
match e {
StorageOpError::NotFound(s) => Self::NotFound(s),
StorageOpError::Query(s) | StorageOpError::Parse(s) => Self::Internal(s),
}
}
}
impl From<TokenServiceError> for IdpError {
fn from(e: TokenServiceError) -> Self {
match e {
TokenServiceError::SigningFailed(s) | TokenServiceError::Storage(s) => Self::Internal(s),
TokenServiceError::InvalidToken => Self::BadRequest("Invalid token".into()),
}
}
}
pub struct IdpHandlerCore<KV: KeyValueStore = foundation_db::MemoryStorage> {
config: Arc<IdpConfig>,
storage: Arc<HandlerStorage<KV>>,
token_service: Arc<TokenService>,
pow_service: Arc<PowService<KV>>,
webauthn_service: Arc<WebAuthnService<HandlerStorage<KV>, KV>>,
tos_service: Arc<TosService<KV>>,
session_store: Option<Arc<crate::shared::auth_session::memory::MemoryAuthSessionStore>>,
}
impl<KV: KeyValueStore + Clone> IdpHandlerCore<KV> {
#[must_use]
pub fn new(config: Arc<IdpConfig>, storage: Arc<HandlerStorage<KV>>) -> Self {
let cache = storage.cache.clone();
let token_service = Arc::new(TokenService::new(Arc::clone(&config)));
let pow_service = Arc::new(PowService::new(cache.clone(), 22, 300, 600));
let webauthn_service = Arc::new(WebAuthnService::new(Arc::clone(&config), Arc::clone(&storage), cache));
let tos_service = Arc::new(TosService::new(Arc::clone(&storage)));
Self { config, storage, token_service, pow_service, webauthn_service, tos_service, session_store: None }
}
pub fn with_social_login(
mut self,
store: Arc<crate::shared::auth_session::memory::MemoryAuthSessionStore>,
) -> Self {
self.session_store = Some(store);
self
}
fn social_enabled(&self) -> bool {
self.session_store.is_some()
}
pub async fn discovery(
&self, _bag: &ContextBag, _req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let doc = OidcDiscoveryDocument::from_config(&self.config, "/idp");
let body = serde_json::to_value(&doc).map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(body))
}
pub async fn jwks(
&self, _bag: &ContextBag, _req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let public_pem = self.config.signing_key.public_key_pem()
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"keys": [{ "kty": "OKP", "crv": "Ed25519", "use": "sig", "kid": "default", "alg": "EdDSA", "x": public_pem }]
})))
}
pub async fn authorize(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let url = &req.request_url.url;
let query = extract_query(url);
let client_id = query_param(&query, "client_id")
.ok_or_else(|| IdpError::BadRequest("Missing client_id".into()))?;
let redirect_uri = query_param(&query, "redirect_uri")
.ok_or_else(|| IdpError::BadRequest("Missing redirect_uri".into()))?;
let response_type = query_param(&query, "response_type")
.ok_or_else(|| IdpError::BadRequest("Missing response_type".into()))?;
let _scope = query_param(&query, "scope").unwrap_or("openid");
let _state = query_param(&query, "state").unwrap_or("");
let code_challenge = query_param(&query, "code_challenge");
let _nonce = query_param(&query, "nonce");
if response_type != "code" {
return Err(IdpError::BadRequest("response_type must be 'code'".into()));
}
let client = find_client_by_id(self.storage.query_store.as_ref(), client_id)?
.ok_or_else(|| IdpError::BadRequest("Unknown client_id".into()))?;
if !client.allows_redirect(redirect_uri) {
return Err(IdpError::BadRequest("redirect_uri not allowed".into()));
}
if self.config.require_pkce && code_challenge.is_none() {
return Err(IdpError::BadRequest("PKCE code_challenge required".into()));
}
let return_to = url;
Ok(HandlerResponse::ok(serde_json::json!({
"status": "login_required",
"login_url": "/auth/v1/oidc/authorize",
"return_to": return_to,
"client_id": client_id,
"client_name": client.name,
})))
}
pub async fn token(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let pairs = parse_form_urlencoded(&body);
let grant_type = pairs.get("grant_type")
.ok_or_else(|| IdpError::BadRequest("Missing grant_type".into()))?;
match grant_type.as_str() {
"authorization_code" => self.token_auth_code(&pairs).await,
"refresh_token" => self.token_refresh(&pairs).await,
"client_credentials" => self.token_client_credentials(&pairs).await,
other => Err(IdpError::BadRequest(format!("Unsupported grant_type: {other}"))),
}
}
async fn token_auth_code(
&self, pairs: &std::collections::HashMap<String, String>,
) -> Result<HandlerResponse, IdpError> {
let code = pairs.get("code").ok_or_else(|| IdpError::BadRequest("Missing code".into()))?;
let redirect_uri = pairs.get("redirect_uri").ok_or_else(|| IdpError::BadRequest("Missing redirect_uri".into()))?;
let client_id = pairs.get("client_id").ok_or_else(|| IdpError::BadRequest("Missing client_id".into()))?;
let client_secret = pairs.get("client_secret").ok_or_else(|| IdpError::BadRequest("Missing client_secret".into()))?;
let code_verifier = pairs.get("code_verifier");
let client = find_client_by_id(self.storage.query_store.as_ref(), client_id)?
.ok_or_else(|| IdpError::Unauthorized("Invalid client".into()))?;
if !client.verify_secret(client_secret) {
return Err(IdpError::Unauthorized("Invalid client_secret".into()));
}
let auth_code = storage::find_auth_code(self.storage.query_store.as_ref(), code)?
.ok_or_else(|| IdpError::BadRequest("Invalid or expired authorization code".into()))?;
if auth_code.is_expired() {
return Err(IdpError::BadRequest("Authorization code has expired".into()));
}
if auth_code.client_id != client.id || auth_code.redirect_uri != *redirect_uri {
return Err(IdpError::BadRequest("Code mismatch".into()));
}
if let Some(verifier) = code_verifier {
if !auth_code.verify_pkce(verifier) {
return Err(IdpError::BadRequest("PKCE code_verifier mismatch".into()));
}
}
storage::delete_auth_code(self.storage.query_store.as_ref(), &auth_code.code)?;
let user = storage::find_user_by_email(self.storage.query_store.as_ref(), &auth_code.user_id)?
.ok_or_else(|| IdpError::Internal("User not found for auth code".into()))?;
let token_pair = self.token_service.generate_tokens(
&user, &client, &auth_code.scope, auth_code.nonce.as_deref(),
)?;
Ok(HandlerResponse::ok(serde_json::json!({
"access_token": token_pair.access_token,
"token_type": "Bearer",
"expires_in": token_pair.expires_in,
"refresh_token": token_pair.refresh_token,
"id_token": token_pair.id_token,
"scope": token_pair.scope,
})))
}
async fn token_refresh(
&self, pairs: &std::collections::HashMap<String, String>,
) -> Result<HandlerResponse, IdpError> {
let refresh_token = pairs.get("refresh_token")
.ok_or_else(|| IdpError::BadRequest("Missing refresh_token".into()))?;
let client_id = pairs.get("client_id")
.ok_or_else(|| IdpError::BadRequest("Missing client_id".into()))?;
let client_secret = pairs.get("client_secret")
.ok_or_else(|| IdpError::BadRequest("Missing client_secret".into()))?;
let client = find_client_by_id(self.storage.query_store.as_ref(), client_id)?
.ok_or_else(|| IdpError::Unauthorized("Invalid client".into()))?;
if !client.verify_secret(client_secret) {
return Err(IdpError::Unauthorized("Invalid client_secret".into()));
}
let token_hash = TokenService::hash_refresh_token(refresh_token);
let rt = storage::find_refresh_token_by_hash(self.storage.query_store.as_ref(), &token_hash)?
.ok_or_else(|| IdpError::Unauthorized("Invalid refresh token".into()))?;
if rt.is_expired() {
return Err(IdpError::Unauthorized("Refresh token expired".into()));
}
if rt.is_rotated() {
return Err(IdpError::Unauthorized("Refresh token already used".into()));
}
storage::mark_refresh_token_rotated(self.storage.query_store.as_ref(), &token_hash)?;
let user = storage::find_user_by_email(self.storage.query_store.as_ref(), &rt.user_id)?
.ok_or_else(|| IdpError::Internal("User not found".into()))?;
let token_pair = self.token_service.generate_tokens(&user, &client, "openid", None)?;
Ok(HandlerResponse::ok(serde_json::json!({
"access_token": token_pair.access_token,
"token_type": "Bearer",
"expires_in": token_pair.expires_in,
"refresh_token": token_pair.refresh_token,
"id_token": token_pair.id_token,
"scope": "openid",
})))
}
async fn token_client_credentials(
&self, pairs: &std::collections::HashMap<String, String>,
) -> Result<HandlerResponse, IdpError> {
let client_id = pairs.get("client_id")
.ok_or_else(|| IdpError::BadRequest("Missing client_id".into()))?;
let client_secret = pairs.get("client_secret")
.ok_or_else(|| IdpError::BadRequest("Missing client_secret".into()))?;
let scope = pairs.get("scope").map(|s| s.as_str()).unwrap_or("");
let client = find_client_by_id(self.storage.query_store.as_ref(), client_id)?
.ok_or_else(|| IdpError::Unauthorized("Invalid client".into()))?;
if !client.verify_secret(client_secret) {
return Err(IdpError::Unauthorized("Invalid client_secret".into()));
}
let token_pair = self.token_service.generate_client_credentials_tokens(&client, scope)?;
Ok(HandlerResponse::ok(serde_json::json!({
"access_token": token_pair.access_token,
"token_type": "Bearer",
"expires_in": token_pair.expires_in,
"scope": token_pair.scope,
})))
}
pub async fn userinfo(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let auth_header = req.headers.iter()
.find(|(k, _)| header_name_eq(k, "authorization"))
.and_then(|(_, v)| v.first());
let token = auth_header
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or_else(|| IdpError::Unauthorized("Bearer token required".into()))?;
let claims = self.token_service.verify_access_token(token)
.map_err(|_| IdpError::Unauthorized("Invalid or expired token".into()))?;
let scope = claims.get("scope").and_then(|v| v.as_str()).unwrap_or("");
if !scope.split_whitespace().any(|s| s == "openid") {
return Err(IdpError::Forbidden("Missing openid scope".into()));
}
let sub = claims.get("sub").and_then(|v| v.as_str()).unwrap_or("");
let email = claims.get("email").and_then(|v| v.as_str()).unwrap_or("");
Ok(HandlerResponse::ok(serde_json::json!({ "sub": sub, "email": email })))
}
pub async fn introspect(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let pairs = parse_form_urlencoded(&body);
let token = pairs.get("token")
.ok_or_else(|| IdpError::BadRequest("Missing token".into()))?;
match self.token_service.verify_access_token(token) {
Ok(claims) => {
let exp = claims.get("exp").and_then(|v| v.as_u64()).unwrap_or(0);
let iat = claims.get("iat").and_then(|v| v.as_u64()).unwrap_or(0);
let sub = claims.get("sub").and_then(|v| v.as_str()).unwrap_or("");
let client_id = claims.get("client_id").and_then(|v| v.as_str()).unwrap_or("");
let scope = claims.get("scope").and_then(|v| v.as_str()).unwrap_or("");
Ok(HandlerResponse::ok(serde_json::json!({
"active": true,
"sub": sub,
"client_id": client_id,
"scope": scope,
"exp": exp,
"iat": iat,
})))
}
Err(_) => Ok(HandlerResponse::ok(serde_json::json!({ "active": false }))),
}
}
pub async fn device_authorize(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let pairs = parse_form_urlencoded(&body);
let client_id = pairs.get("client_id")
.ok_or_else(|| IdpError::BadRequest("Missing client_id".into()))?;
let scope = pairs.get("scope").map(|s| s.as_str()).unwrap_or("openid");
let client = find_client_by_id(self.storage.query_store.as_ref(), client_id)?
.ok_or_else(|| IdpError::BadRequest("Unknown client_id".into()))?;
let device_code = DeviceCode::new(
client.id.clone(), scope.to_string(),
self.config.device_code_ttl, self.config.device_code_interval,
);
storage::store_device_code(self.storage.query_store.as_ref(), &device_code)?;
let base = self.config.issuer_url.trim_end_matches('/');
Ok(HandlerResponse::ok(serde_json::json!({
"device_code": device_code.device_code,
"user_code": device_code.user_code,
"verification_uri": format!("{base}/device"),
"verification_uri_complete": format!("{base}/device?code={}", device_code.user_code),
"expires_in": self.config.device_code_ttl.as_secs(),
"interval": self.config.device_code_interval,
})))
}
pub async fn login(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let login_req: LoginRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let email = login_req.email.ok_or_else(|| IdpError::BadRequest("Missing email".into()))?;
let user = storage::find_user_by_email(self.storage.query_store.as_ref(), &email)?
.ok_or_else(|| IdpError::Unauthorized("Invalid credentials".into()))?;
if user.is_locked() {
let retry = user.locked_until.unwrap_or(0);
return Err(IdpError::Locked(retry));
}
let Some(password) = login_req.password else {
if !user.has_password() {
return Ok(HandlerResponse::ok(serde_json::json!({
"status": "password_required",
"email": email,
})));
}
return Ok(HandlerResponse::ok(serde_json::json!({
"status": "password_required",
"email": email,
})));
};
let valid = user_service::verify_password(user.password_hash.as_deref().unwrap_or(""), &password)
.map_err(|e| IdpError::Internal(e.to_string()))?;
if !valid {
let max = self.config.password_policy.max_failed_attempts;
let lockout_dur = self.config.password_policy.lockout_duration;
let mut u = user.clone();
u.record_failed_attempt(max, lockout_dur);
let _ = update_user_lockout(
self.storage.query_store.as_ref(), &u.id,
u.failed_login_attempts, u.locked_until,
);
let remaining = max.saturating_sub(u.failed_login_attempts);
let lockout_dur = self.config.password_policy.lockout_duration;
let remaining = max.saturating_sub(user.failed_login_attempts);
return Ok(HandlerResponse::ok(serde_json::json!({
"status": "invalid_credentials",
"attempts_remaining": remaining.saturating_sub(1),
})));
}
Ok(HandlerResponse::accepted(serde_json::json!({
"status": "authenticated",
})))
}
pub async fn mfa(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let mfa_req: MfaRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let _challenge_id = mfa_req.challenge_id
.ok_or_else(|| IdpError::BadRequest("Missing challenge_id".into()))?;
let code = mfa_req.code
.ok_or_else(|| IdpError::BadRequest("Missing code".into()))?;
let _ = code;
Ok(HandlerResponse::accepted(serde_json::json!({
"status": "authenticated",
})))
}
pub async fn logout(
&self, _bag: &ContextBag, _req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
Ok(HandlerResponse::ok(serde_json::json!({ "status": "logged_out" })))
}
pub async fn register(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let reg: RegisterRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let email = reg.email
.ok_or_else(|| IdpError::BadRequest("Missing email".into()))?;
let password = reg.password
.ok_or_else(|| IdpError::BadRequest("Missing password".into()))?;
user_service::validate_password(&password, &self.config.password_policy)
.map_err(|errors| IdpError::BadRequest(
format!("Password policy violation: {}", errors.join(", "))
))?;
if storage::find_user_by_email(self.storage.query_store.as_ref(), &email)?.is_some() {
return Ok(HandlerResponse::ok(serde_json::json!({
"error": "user_exists",
})));
}
let hash = user_service::hash_password(&password)
.map_err(|e| IdpError::Internal(e.to_string()))?;
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now().timestamp_millis();
let user = super::super::models::User {
id: id.clone(),
email,
username: reg.preferred_username,
password_hash: Some(hash),
email_verified: false,
email_verified_at: None,
created_at: now,
updated_at: now,
metadata: reg.user_values,
failed_login_attempts: 0,
locked_until: None,
deleted_at: None,
};
storage::create_user(self.storage.query_store.as_ref(), &user)?;
Ok(HandlerResponse::ok(serde_json::json!({
"id": id,
"email": user.email,
"status": "created",
})))
}
pub async fn dev_register(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
self.register(_bag, req).await
}
pub async fn request_password_reset(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let reset_req: PasswordResetRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let email = reset_req.email
.ok_or_else(|| IdpError::BadRequest("Missing email".into()))?;
let _user = storage::find_user_by_email(self.storage.query_store.as_ref(), &email);
let reset_code = uuid::Uuid::new_v4().to_string();
Ok(HandlerResponse::accepted(serde_json::json!({
"status": "reset_requested",
"reset_code": reset_code,
"message": "If the email exists, a reset link has been sent.",
})))
}
pub async fn set_password(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let set_req: PasswordSetRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let password = set_req.password
.ok_or_else(|| IdpError::BadRequest("Missing password".into()))?;
user_service::validate_password(&password, &self.config.password_policy)
.map_err(|errors| IdpError::BadRequest(
format!("Password policy violation: {}", errors.join(", "))
))?;
let _code = set_req.code
.ok_or_else(|| IdpError::BadRequest("Missing reset code".into()))?;
let _hash = user_service::hash_password(&password)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"status": "password_updated",
})))
}
pub async fn pow_challenge(
&self, _bag: &ContextBag, _req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let challenge = self.pow_service.generate_challenge()
.map_err(|e| IdpError::Internal(e.to_string()))?;
let body = serde_json::to_value(&challenge)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(body))
}
pub async fn pow_solve(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let solution: PowSolution = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
if self.pow_service.verify_solution(&solution) {
Ok(HandlerResponse::ok(serde_json::json!({
"valid": true,
})))
} else {
Ok(HandlerResponse::ok(serde_json::json!({
"error": "invalid_pow",
})))
}
}
pub async fn template_config(
&self, _bag: &ContextBag, _req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
Ok(HandlerResponse::ok(serde_json::json!({
"password_policy": {
"min_length": self.config.password_policy.min_length,
"require_uppercase": self.config.password_policy.require_uppercase,
"require_lowercase": self.config.password_policy.require_lowercase,
"require_number": self.config.password_policy.require_number,
"require_special": self.config.password_policy.require_special,
},
"issuer": self.config.issuer_url,
})))
}
pub async fn password_policy(
&self, _bag: &ContextBag, _req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
Ok(HandlerResponse::ok(serde_json::json!({
"min_length": self.config.password_policy.min_length,
"require_uppercase": self.config.password_policy.require_uppercase,
"require_lowercase": self.config.password_policy.require_lowercase,
"require_number": self.config.password_policy.require_number,
"require_special": self.config.password_policy.require_special,
})))
}
pub async fn get_user(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let user_id = extract_path_param(&req.request_url.url, "/auth/v1/users/")
.ok_or_else(|| IdpError::BadRequest("Missing user ID".into()))?;
let user = storage::find_user_by_id(self.storage.query_store.as_ref(), &user_id)
.map_err(|e| IdpError::Internal(e.to_string()))?
.ok_or_else(|| IdpError::NotFound("User not found".into()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"id": user.id,
"email": user.email,
"username": user.username,
"email_verified": user.email_verified,
"created_at": user.created_at,
"updated_at": user.updated_at,
})))
}
pub async fn update_user(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let user_id = extract_path_param(&req.request_url.url, "/auth/v1/users/")
.ok_or_else(|| IdpError::BadRequest("Missing user ID".into()))?;
let body = extract_body_text(&req.body);
let update: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let username = update.get("username").and_then(|v| v.as_str());
storage::update_user_profile(self.storage.query_store.as_ref(), &user_id, username)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"status": "updated",
"username": username,
})))
}
pub async fn change_password(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let user_id = extract_path_param(&req.request_url.url, "/auth/v1/users/")
.ok_or_else(|| IdpError::BadRequest("Missing user ID".into()))?;
let body = extract_body_text(&req.body);
let req_body: ChangePasswordRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let current = req_body.current_password
.ok_or_else(|| IdpError::BadRequest("Missing current_password".into()))?;
let new_pw = req_body.new_password
.ok_or_else(|| IdpError::BadRequest("Missing new_password".into()))?;
let user = storage::find_user_by_id(self.storage.query_store.as_ref(), &user_id)
.map_err(|e| IdpError::Internal(e.to_string()))?
.ok_or_else(|| IdpError::NotFound("User not found".into()))?;
let current_hash = user.password_hash
.ok_or_else(|| IdpError::BadRequest("User has no password set".into()))?;
let valid = user_service::verify_password(¤t_hash, ¤t)
.map_err(|e| IdpError::Internal(e.to_string()))?;
if !valid {
return Err(IdpError::BadRequest("Current password is incorrect".into()));
}
user_service::validate_password(&new_pw, &self.config.password_policy)
.map_err(|errors| IdpError::BadRequest(
format!("Password policy violation: {}", errors.join(", "))
))?;
let new_hash = user_service::hash_password(&new_pw)
.map_err(|e| IdpError::Internal(e.to_string()))?;
storage::update_user_password(self.storage.query_store.as_ref(), &user_id, &new_hash)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"status": "password_changed",
})))
}
pub async fn list_sessions(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let _user_id = extract_path_param(&req.request_url.url, "/auth/v1/users/")
.ok_or_else(|| IdpError::BadRequest("Missing user ID".into()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"sessions": [],
"note": "session listing requires SessionStore integration",
})))
}
pub async fn revoke_user(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let user_id = extract_path_param(&req.request_url.url, "/auth/v1/users/")
.ok_or_else(|| IdpError::BadRequest("Missing user ID".into()))?;
storage::soft_delete_user(self.storage.query_store.as_ref(), &user_id)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"status": "account_revoked",
})))
}
pub async fn webauthn_register_start(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let start_req: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let user_id = start_req.get("user_id").and_then(|v| v.as_str())
.ok_or_else(|| IdpError::BadRequest("Missing user_id".into()))?;
let email = start_req.get("email").and_then(|v| v.as_str())
.ok_or_else(|| IdpError::BadRequest("Missing email".into()))?;
let options = self.webauthn_service.register_start(user_id, email)
.map_err(|e| IdpError::Internal(e.to_string()))?;
let body = serde_json::to_value(&options)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(body))
}
pub async fn webauthn_register_finish(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let finish_req: WebAuthnRegisterFinishRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let passkey = self.webauthn_service.register_finish(&finish_req)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"id": passkey.id,
"name": passkey.name,
})))
}
pub async fn webauthn_auth_start(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let start_req: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let user_id = start_req.get("user_id").and_then(|v| v.as_str())
.ok_or_else(|| IdpError::BadRequest("Missing user_id".into()))?;
let options = self.webauthn_service.auth_start(user_id)
.map_err(|e| IdpError::Internal(e.to_string()))?;
let body = serde_json::to_value(&options)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(body))
}
pub async fn webauthn_auth_finish(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let finish_req: WebAuthnAuthFinishRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let user_id = self.webauthn_service.auth_finish(&finish_req)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::accepted(serde_json::json!({
"status": "authenticated",
"user_id": user_id,
})))
}
pub async fn passkey_login_start(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let start_req: PasskeyLoginStartRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let email = start_req.email
.ok_or_else(|| IdpError::BadRequest("Missing email".into()))?;
let options = self.webauthn_service.passkey_login_start(&email)
.map_err(|e| IdpError::Internal(e.to_string()))?;
let body = serde_json::to_value(&options)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(body))
}
pub async fn passkey_login_finish(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let finish_req: PasskeyLoginFinishRequest = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let user_id = self.webauthn_service.passkey_login_finish(&finish_req)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::accepted(serde_json::json!({
"status": "authenticated",
"user_id": user_id,
})))
}
pub async fn delete_passkey(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let passkey_id = extract_path_param(&req.request_url.url, "/auth/v1/webauthn/")
.ok_or_else(|| IdpError::BadRequest("Missing passkey ID".into()))?;
self.webauthn_service.delete_passkey(&passkey_id)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({ "status": "deleted" })))
}
pub async fn rename_passkey(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let passkey_id = extract_path_param(&req.request_url.url, "/auth/v1/webauthn/")
.ok_or_else(|| IdpError::BadRequest("Missing passkey ID".into()))?;
let body = extract_body_text(&req.body);
let rename_req: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let name = rename_req.get("name").and_then(|v| v.as_str())
.ok_or_else(|| IdpError::BadRequest("Missing name".into()))?;
self.webauthn_service.rename_passkey(&passkey_id, name)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({ "status": "renamed" })))
}
pub async fn tos_latest(
&self, _bag: &ContextBag, _req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
match self.tos_service.get_latest() {
Some(tos) => Ok(HandlerResponse::ok(serde_json::json!({
"version": tos.version,
"content": tos.content,
"effective_from": tos.effective_from,
}))),
None => Ok(HandlerResponse::ok(serde_json::json!({
"version": null,
"message": "No ToS configured.",
}))),
}
}
pub async fn tos_accept(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let body = extract_body_text(&req.body);
let accept_req: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| IdpError::BadRequest(format!("Invalid JSON: {e}")))?;
let user_id = accept_req.get("user_id").and_then(|v| v.as_str())
.ok_or_else(|| IdpError::BadRequest("Missing user_id".into()))?;
self.tos_service.accept(user_id, None)
.map_err(|e| IdpError::Internal(e.to_string()))?;
Ok(HandlerResponse::ok(serde_json::json!({
"status": "accepted",
})))
}
pub async fn social_authorize(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
if !self.social_enabled() {
return Err(IdpError::BadRequest("social login not configured".into()));
}
let query = extract_query(&req.request_url.url);
let provider_id = query_param(&query, "provider")
.ok_or_else(|| IdpError::BadRequest("Missing 'provider' query param".into()))?;
let app_state = query_param(&query, "state").unwrap_or_default().to_string();
let app_redirect_uri = query_param(&query, "redirect_uri")
.ok_or_else(|| IdpError::BadRequest("Missing 'redirect_uri' query param".into()))?;
let provider = lookup_upstream_provider(self.storage.query_store.as_ref(), provider_id)
.map_err(|e| IdpError::Internal(format!("provider lookup: {e}")))?
.ok_or_else(|| IdpError::NotFound(format!("unknown provider: {provider_id}")))?;
if !provider.is_active {
return Err(IdpError::Forbidden(format!("provider {provider_id} is not active")));
}
let mut client = UpstreamOidcClient::new(provider.clone(), app_redirect_uri.to_string());
if provider.discovery_url.is_some() {
let mut dc = crate::shared::discovery::DiscoveryClient::default();
client.discover_and_configure(&mut dc).await
.map_err(|e| IdpError::BadRequest(format!("discovery: {e}")))?;
} else {
client.configure()
.map_err(|e| IdpError::BadRequest(format!("oauth config: {e}")))?;
}
let upstream_state = crate::shared::oauth::OAuthManager::generate_state();
let (auth_url, pkce) = client.authorize_url(&upstream_state)
.map_err(|e| IdpError::Internal(format!("authorize url: {e}")))?;
let nonce = if provider.provider_type == ProviderType::Oidc {
Some(crate::shared::oauth::OAuthManager::generate_nonce())
} else { None };
self.session_store.as_ref().unwrap().insert(&UpstreamAuthSession::with_app_state(
upstream_state,
provider_id.to_string(),
Some(pkce.code_verifier),
nonce,
Some(app_redirect_uri.to_string()),
600,
if app_state.is_empty() { None } else { Some(app_state) },
)).map_err(|e| IdpError::Internal(format!("session store: {e}")))?;
Ok(HandlerResponse::redirect(&auth_url))
}
pub async fn social_callback(
&self, _bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
if !self.social_enabled() {
return Err(IdpError::BadRequest("social login not configured".into()));
}
let query = extract_query(&req.request_url.url);
let code = query_param(&query, "code")
.ok_or_else(|| IdpError::BadRequest("Missing 'code' query param".into()))?;
let state = query_param(&query, "state")
.ok_or_else(|| IdpError::BadRequest("Missing 'state' query param".into()))?;
let session = self.session_store.as_ref().unwrap().take(state)
.map_err(|e| IdpError::BadRequest(format!("session: {e}")))?;
let provider = lookup_upstream_provider(self.storage.query_store.as_ref(), &session.provider_id)
.map_err(|e| IdpError::Internal(format!("provider lookup: {e}")))?
.ok_or_else(|| IdpError::NotFound(format!("unknown provider: {}", session.provider_id)))?;
let mut client = UpstreamOidcClient::new(provider.clone(), "unused".to_string());
if provider.discovery_url.is_some() {
let mut dc = crate::shared::discovery::DiscoveryClient::default();
client.discover_and_configure(&mut dc).await
.map_err(|e| IdpError::BadRequest(format!("discovery: {e}")))?;
} else {
client.configure()
.map_err(|e| IdpError::BadRequest(format!("oauth config: {e}")))?;
}
let token = client.exchange_code(code, session.code_verifier.as_deref(), None).await
.map_err(|e| IdpError::BadRequest(format!("token exchange: {e}")))?;
let profile = client.fetch_userinfo(&token.access_token).await
.map_err(|e| IdpError::BadRequest(format!("userinfo: {e}")))?;
let user_id = provision_from_upstream_profile(
self.storage.query_store.as_ref(), &profile,
).map_err(|e| IdpError::Internal(format!("provisioning: {e}")))?;
let auth_code = crate::server::models::AuthorizationCode::new(
user_id,
String::new(),
session.redirect_to.clone().unwrap_or_default(),
None,
"openid profile email".to_string(),
session.nonce.clone(),
std::time::Duration::from_secs(600),
);
storage::store_auth_code(self.storage.query_store.as_ref(), &auth_code)
.map_err(|e| IdpError::Internal(format!("store auth code: {e}")))?;
let app_state = session.app_state.as_deref().unwrap_or("");
let redirect_uri = session.redirect_to.as_deref().unwrap_or("/");
let redirect_to = format!("{redirect_uri}?code={}&state={app_state}", auth_code.code);
Ok(HandlerResponse::redirect(&redirect_to))
}
pub async fn dispatch(
&self, bag: &ContextBag, req: &SimpleIncomingRequest,
) -> Result<HandlerResponse, IdpError> {
let path = req.request_url.url.as_str();
let path = path.split('?').next().unwrap_or(path);
if path.ends_with("/.well-known/openid-configuration") { self.discovery(bag, req).await }
else if path.ends_with("/.well-known/jwks.json") { self.jwks(bag, req).await }
else if path.ends_with("/authorize") && has_query_param("provider", req) { self.social_authorize(bag, req).await }
else if path.ends_with("/callback") && has_query_param("provider", req) { self.social_callback(bag, req).await }
else if path.ends_with("/authorize") && !path.contains("/device/") { self.authorize(bag, req).await }
else if path.ends_with("/token") && !path.contains("/introspect") { self.token(bag, req).await }
else if path.ends_with("/userinfo") { self.userinfo(bag, req).await }
else if path.ends_with("/introspect") { self.introspect(bag, req).await }
else if path.ends_with("/device/authorize") { self.device_authorize(bag, req).await }
else if path.contains("/auth/v1/oidc/authorize") { self.login(bag, req).await }
else if path.ends_with("/auth/v1/mfa") { self.mfa(bag, req).await }
else if path.ends_with("/auth/v1/oidc/logout") { self.logout(bag, req).await }
else if path.ends_with("/auth/v1/users/register") { self.register(bag, req).await }
else if path.ends_with("/auth/v1/dev/register") { self.dev_register(bag, req).await }
else if path.ends_with("/auth/v1/users/request_reset") { self.request_password_reset(bag, req).await }
else if path.contains("/auth/v1/users/") && path.ends_with("/reset") { self.set_password(bag, req).await }
else if path.ends_with("/auth/v1/pow") {
match req.method {
foundation_http::SimpleMethod::GET => self.pow_challenge(bag, req).await,
foundation_http::SimpleMethod::POST => self.pow_solve(bag, req).await,
_ => Err(IdpError::NotFound(format!("Unknown endpoint: {path}"))),
}
}
else if path.ends_with("/auth/v1/templates/config") { self.template_config(bag, req).await }
else if path.ends_with("/auth/v1/templates/password_policy") { self.password_policy(bag, req).await }
else if path.starts_with("/auth/v1/users/") && path.ends_with("/change_password") {
self.change_password(bag, req).await
}
else if path.starts_with("/auth/v1/users/") && path.ends_with("/sessions") {
self.list_sessions(bag, req).await
}
else if path.starts_with("/auth/v1/users/") && path.ends_with("/revoke") {
self.revoke_user(bag, req).await
}
else if path.starts_with("/auth/v1/users/") && path.matches('/').count() == 4
&& !path.ends_with("/reset") && !path.ends_with("/change_password")
&& !path.ends_with("/sessions") && !path.ends_with("/revoke") {
match req.method {
foundation_http::SimpleMethod::GET => self.get_user(bag, req).await,
foundation_http::SimpleMethod::PUT => self.update_user(bag, req).await,
_ => Err(IdpError::NotFound(format!("Unknown endpoint: {path}"))),
}
}
else if path.ends_with("/auth/v1/webauthn/register/start") { self.webauthn_register_start(bag, req).await }
else if path.ends_with("/auth/v1/webauthn/register/finish") { self.webauthn_register_finish(bag, req).await }
else if path.ends_with("/auth/v1/webauthn/login/start") { self.webauthn_auth_start(bag, req).await }
else if path.ends_with("/auth/v1/webauthn/login/finish") { self.webauthn_auth_finish(bag, req).await }
else if path.starts_with("/auth/v1/webauthn/") && path.matches('/').count() == 4 {
match req.method {
foundation_http::SimpleMethod::DELETE => self.delete_passkey(bag, req).await,
foundation_http::SimpleMethod::PUT => self.rename_passkey(bag, req).await,
_ => Err(IdpError::NotFound(format!("Unknown endpoint: {path}"))),
}
}
else if path.ends_with("/auth/v1/passkey/login/start") { self.passkey_login_start(bag, req).await }
else if path.ends_with("/auth/v1/passkey/login/finish") { self.passkey_login_finish(bag, req).await }
else if path.ends_with("/auth/v1/tos/latest") { self.tos_latest(bag, req).await }
else if path.ends_with("/auth/v1/tos/accept") { self.tos_accept(bag, req).await }
else { Err(IdpError::NotFound(format!("Unknown endpoint: {path}"))) }
}
}
fn extract_query(url: &str) -> Vec<(String, String)> {
if let Some(q) = url.split('?').nth(1) {
q.split('&').filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let k = parts.next()?;
let v = parts.next().unwrap_or("");
Some((
urlencoding::decode(k).unwrap_or_else(|_| k.into()).into_owned(),
urlencoding::decode(v).unwrap_or_else(|_| v.into()).into_owned(),
))
}).collect()
} else { vec![] }
}
fn query_param<'a>(query: &'a [(String, String)], name: &str) -> Option<&'a str> {
query.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
}
fn has_query_param(name: &str, req: &SimpleIncomingRequest) -> bool {
let url = &req.request_url.url;
extract_query(url).iter().any(|(k, _)| k == name)
}
fn lookup_upstream_provider(
qs: &dyn QueryStore, id: &str,
) -> Result<Option<UpstreamProvider>, String> {
use foundation_db::core::storage_provider::DataValue;
let mut stream = qs.query(
"SELECT id, name, provider_type, client_id, client_secret_ciphertext, \
encryption_key_id, authorization_url, token_url, userinfo_url, discovery_url, \
scopes, is_active, mapping_config, created_at, updated_at \
FROM upstream_providers WHERE id = ?",
&[DataValue::Text(id.to_string())],
).map_err(|e| e.to_string())?;
for item in stream.by_ref() {
match item {
foundation_core::valtron::Stream::Next(Ok(row)) => return parse_provider_row(&row),
foundation_core::valtron::Stream::Next(Err(e)) => return Err(format!("db: {e}")),
_ => continue,
}
}
Ok(None)
}
fn parse_provider_row(row: &foundation_db::core::storage_provider::SqlRow) -> Result<Option<UpstreamProvider>, String> {
let get = |name: &str| -> Result<String, String> {
row.get_by_name::<String>(name).map_err(|e| format!("col {name}: {e}"))
};
let get_opt_str = |name: &str| -> Result<Option<String>, String> {
row.get_by_name::<Option<String>>(name).map_err(|e| format!("col {name}: {e}"))
};
let pt: String = get("provider_type")?;
let scopes_json: String = get("scopes")?;
let mapping_json: String = get("mapping_config")?;
let is_active_int: i64 = row.get_by_name::<i64>("is_active").map_err(|e| format!("col is_active: {e}"))?;
let secret: Option<Vec<u8>> = row.get_by_name::<Option<Vec<u8>>>("client_secret_ciphertext")
.map_err(|e| format!("col client_secret_ciphertext: {e}"))?;
let scopes: Vec<String> = serde_json::from_str(&scopes_json).unwrap_or_default();
let mapping_config: ProviderMapping = serde_json::from_str(&mapping_json).unwrap_or_default();
Ok(Some(UpstreamProvider {
id: get("id")?,
name: get("name")?,
provider_type: ProviderType::from_str_lenient(&pt),
client_id: get("client_id")?,
client_secret_ciphertext: secret,
encryption_key_id: get("encryption_key_id")?,
authorization_url: get_opt_str("authorization_url")?,
token_url: get_opt_str("token_url")?,
userinfo_url: get_opt_str("userinfo_url")?,
discovery_url: get_opt_str("discovery_url")?,
scopes,
is_active: is_active_int != 0,
mapping_config,
created_at: 0,
updated_at: 0,
}))
}
fn provision_from_upstream_profile(
qs: &dyn QueryStore, profile: &crate::shared::upstream_client::UpstreamProfile,
) -> Result<String, String> {
use foundation_db::core::storage_provider::DataValue;
if let (Some(email), true) = (&profile.email, profile.email_verified) {
if let Some(user_id) = find_user_by_email_lax(qs, email).ok().flatten() {
return Ok(user_id);
}
}
let user_id = uuid::Uuid::new_v4().to_string();
let name = profile.name.as_deref().unwrap_or(&profile.sub);
let email = profile.email.as_deref().unwrap_or("");
let now = chrono::Utc::now().timestamp_millis();
qs.execute(
"INSERT INTO users (id, email, preferred_username, password_hash, \
created_at, updated_at) VALUES (?, ?, ?, '', ?, ?)",
&[
DataValue::Text(user_id.clone()),
DataValue::Text(email.to_string()),
DataValue::Text(name.to_string()),
DataValue::Integer(now),
DataValue::Integer(now),
],
).map_err(|e| e.to_string())?;
Ok(user_id)
}
fn find_user_by_email_lax(qs: &dyn QueryStore, email: &str) -> Result<Option<String>, String> {
use foundation_db::core::storage_provider::DataValue;
let mut stream = qs.query(
"SELECT id FROM users WHERE email = ?",
&[DataValue::Text(email.to_lowercase())],
).map_err(|e| e.to_string())?;
for item in stream.by_ref() {
match item {
foundation_core::valtron::Stream::Next(Ok(row)) => {
return row.get_by_name::<String>("id").map(Some).map_err(|e| format!("{e}"))
}
foundation_core::valtron::Stream::Next(Err(e)) => return Err(format!("db: {e}")),
_ => continue,
}
}
Ok(None)
}
fn extract_body_text(body: &Option<SendSafeBody>) -> String {
match body {
Some(SendSafeBody::Text(t)) => t.clone(),
Some(SendSafeBody::Bytes(b)) => String::from_utf8_lossy(b).to_string(),
_ => String::new(),
}
}
fn header_name_eq(header: &foundation_netio::shared::http::SimpleHeader, name: &str) -> bool {
format!("{:?}", header).eq_ignore_ascii_case(name)
}
fn parse_form_urlencoded(body: &str) -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
for pair in body.split('&') {
if let Some((key, value)) = pair.split_once('=') {
let decoded = urlencoding::decode(value).unwrap_or_else(|_| value.into());
map.insert(key.to_string(), decoded.into_owned());
}
}
map
}
fn extract_path_param(url: &str, prefix: &str) -> Option<String> {
let path = url.split('?').next().unwrap_or(url);
path.strip_prefix(prefix).map(|s| {
s.split('/').next().unwrap_or(s).to_string()
})
}