Skip to main content

auth_framework/api/
oauth2.rs

1//! OAuth 2.0 API Endpoints
2//!
3//! Handles OAuth 2.0 authorization code flow (RFC 6749), token exchange,
4//! token revocation (RFC 7009), and client metadata retrieval.
5
6use crate::api::{ApiResponse, ApiState, extract_bearer_token, validate_api_token};
7use crate::oauth2_server::AuthorizationRequest;
8// Re-export canonical types for consumers that imported them from api::oauth2
9pub use crate::oauth2_server::{
10    AuthorizationRequest as AuthorizeRequest, TokenRequest, TokenResponse,
11};
12use axum::{
13    Json,
14    extract::{Path, Query, State},
15    http::{HeaderMap, StatusCode},
16    response::{IntoResponse, Redirect},
17};
18use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21use url::Url;
22use uuid::Uuid;
23
24/// Compare two redirect URIs using parsed URL normalization (RFC 6749 §3.1.2.3).
25///
26/// Normalization handles scheme/host case, default-port elision, and path
27/// normalization so that trivial textual differences don't cause mismatches.
28fn redirect_uri_matches(candidate: &str, registered: &str) -> bool {
29    match (Url::parse(candidate), Url::parse(registered)) {
30        (Ok(a), Ok(b)) => {
31            // RFC 6749 §3.1.2: redirect URIs MUST NOT contain a fragment component.
32            if a.fragment().is_some() || b.fragment().is_some() {
33                return false;
34            }
35
36            a.scheme() == b.scheme()
37                && a.host_str() == b.host_str()
38                && a.port_or_known_default() == b.port_or_known_default()
39                && a.path() == b.path()
40                && a.query() == b.query()
41        }
42        // If either side isn't a valid URL, fall back to exact string match
43        // so we don't silently accept garbage.
44        _ => candidate == registered,
45    }
46}
47
48/// OAuth error response per [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2).
49///
50/// Use the constructor + chaining helpers to avoid specifying all four fields
51/// every time:
52///
53/// # Example
54/// ```rust
55/// use auth_framework::api::oauth2::OAuthError;
56///
57/// let err = OAuthError::new("invalid_request")
58///     .description("missing redirect_uri")
59///     .state("abc");
60/// assert_eq!(err.error, "invalid_request");
61/// assert_eq!(err.error_description.as_deref(), Some("missing redirect_uri"));
62/// ```
63#[derive(Debug, Serialize)]
64pub struct OAuthError {
65    pub error: String,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub error_description: Option<String>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub error_uri: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub state: Option<String>,
72}
73
74impl OAuthError {
75    /// Create a new error with the given error code (e.g. `"invalid_request"`).
76    pub fn new(error: impl Into<String>) -> Self {
77        Self {
78            error: error.into(),
79            error_description: None,
80            error_uri: None,
81            state: None,
82        }
83    }
84
85    /// Set the human-readable error description.
86    pub fn description(mut self, desc: impl Into<String>) -> Self {
87        self.error_description = Some(desc.into());
88        self
89    }
90
91    /// Set the `state` parameter (echoed back from the authorization request).
92    pub fn state(mut self, state: impl Into<String>) -> Self {
93        self.state = Some(state.into());
94        self
95    }
96
97    /// Set the `state` parameter from an `Option`.
98    pub fn maybe_state(mut self, state: Option<String>) -> Self {
99        self.state = state;
100        self
101    }
102
103    /// Set the error URI pointing to a page with more information.
104    pub fn error_uri(mut self, uri: impl Into<String>) -> Self {
105        self.error_uri = Some(uri.into());
106        self
107    }
108}
109
110/// Registered OAuth 2.0 client metadata.
111///
112/// Stored in KV at `oauth2_client:{client_id}` and returned by
113/// [`get_client_info`].
114///
115/// # Example
116/// ```rust
117/// use auth_framework::api::oauth2::ClientInfo;
118///
119/// let info = ClientInfo {
120///     client_id: "abc".into(),
121///     name: "My App".into(),
122///     description: "A demo app".into(),
123///     redirect_uris: vec!["https://example.com/cb".into()],
124///     scopes: vec!["openid".into()],
125/// };
126/// assert_eq!(info.client_id, "abc");
127/// ```
128#[derive(Debug, Serialize, Deserialize)]
129pub struct ClientInfo {
130    pub client_id: String,
131    pub name: String,
132    pub description: String,
133    pub redirect_uris: Vec<String>,
134    pub scopes: Vec<String>,
135}
136
137/// OAuth 2.0 token revocation request per [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009).
138///
139/// # Example
140/// ```rust
141/// use auth_framework::api::oauth2::RevokeRequest;
142///
143/// let req: RevokeRequest = serde_json::from_str(r#"{"token":"abc"}"#).unwrap();
144/// assert_eq!(req.token, "abc");
145/// assert!(req.token_type_hint.is_none());
146/// ```
147#[derive(Debug, Deserialize)]
148pub struct RevokeRequest {
149    pub token: String,
150    #[serde(default)]
151    pub token_type_hint: Option<String>, // "access_token" or "refresh_token"
152}
153
154/// OpenID Connect UserInfo response.
155///
156/// Contains claims about the authenticated user, filtered by the scopes
157/// granted to the access token.
158///
159/// # Example
160/// ```rust
161/// use auth_framework::api::oauth2::UserInfoResponse;
162///
163/// let info = UserInfoResponse {
164///     sub: "user-1".into(),
165///     name: Some("Alice".into()),
166///     email: None,
167///     picture: None,
168///     updated_at: None,
169/// };
170/// assert_eq!(info.sub, "user-1");
171/// ```
172#[derive(Debug, Serialize)]
173pub struct UserInfoResponse {
174    pub sub: String,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub name: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub email: Option<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub picture: Option<String>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub updated_at: Option<i64>,
183}
184
185/// GET /oauth/authorize
186/// OAuth 2.0 authorization endpoint — validates the client and redirect_uri, generates
187/// an authorization code, and redirects the user-agent back to the client (RFC 6749 §4.1.2).
188///
189/// SECURITY: The caller must supply their access token as `Authorization: Bearer <token>`.
190/// The authenticated user's identity is recorded in the authorization code so it can be
191/// used when the client exchanges the code for tokens.  Issuing codes without a verified
192/// user identity would allow any party that knows a valid client_id to obtain tokens.
193pub async fn authorize(
194    State(state): State<ApiState>,
195    headers: HeaderMap,
196    Query(params): Query<AuthorizationRequest>,
197) -> impl IntoResponse {
198    if params.response_type != "code" {
199        let error = OAuthError::new("unsupported_response_type")
200            .description("Only 'code' response type is supported")
201            .maybe_state(params.state);
202        return (StatusCode::BAD_REQUEST, Json(error)).into_response();
203    }
204
205    if params.client_id.is_empty() {
206        let error = OAuthError::new("invalid_request")
207            .description("client_id is required")
208            .maybe_state(params.state);
209        return (StatusCode::BAD_REQUEST, Json(error)).into_response();
210    }
211
212    if params.redirect_uri.is_empty() {
213        let error = OAuthError::new("invalid_request")
214            .description("redirect_uri is required")
215            .maybe_state(params.state);
216        return (StatusCode::BAD_REQUEST, Json(error)).into_response();
217    }
218
219    // SECURITY: Require the resource owner to be authenticated before issuing codes.
220    // The user must supply their Bearer access token.  Without this check, any caller
221    // that knows a registered client_id could obtain a code and exchange it for tokens.
222    let user_id = {
223        let token_str = match extract_bearer_token(&headers) {
224            Some(t) => t,
225            None => {
226                let error = OAuthError::new("unauthorized_client")
227                    .description(
228                        "User authentication required: supply your access token as \
229                         'Authorization: Bearer <token>'",
230                    )
231                    .maybe_state(params.state);
232                return (StatusCode::UNAUTHORIZED, Json(error)).into_response();
233            }
234        };
235        match validate_api_token(&state.auth_framework, &token_str).await {
236            Ok(auth_token) => auth_token.user_id,
237            Err(_) => {
238                let error = OAuthError::new("unauthorized_client")
239                    .description("Invalid or expired user access token")
240                    .maybe_state(params.state);
241                return (StatusCode::UNAUTHORIZED, Json(error)).into_response();
242            }
243        }
244    };
245
246    // SECURITY: Validate client_id and verify the redirect_uri is pre-registered.
247    // Redirecting to an unregistered URI would let an attacker steal the authorization code.
248    let client_key = format!("oauth2_client:{}", params.client_id);
249    match state.auth_framework.storage().get_kv(&client_key).await {
250        Ok(Some(data)) => {
251            let client_data: serde_json::Value = serde_json::from_slice(&data).unwrap_or_default();
252            let registered_uris: Vec<String> = client_data["redirect_uris"]
253                .as_array()
254                .map(|arr| {
255                    arr.iter()
256                        .filter_map(|v| v.as_str().map(String::from))
257                        .collect()
258                })
259                .unwrap_or_default();
260
261            if !registered_uris
262                .iter()
263                .any(|r| redirect_uri_matches(&params.redirect_uri, r))
264            {
265                tracing::warn!(
266                    client_id = %params.client_id,
267                    redirect_uri = %params.redirect_uri,
268                    "OAuth authorize: redirect_uri not registered for client"
269                );
270                let error = OAuthError::new("invalid_request")
271                    .description("redirect_uri is not registered for this client")
272                    .maybe_state(params.state);
273                return (StatusCode::BAD_REQUEST, Json(error)).into_response();
274            }
275
276            // S8: Public clients (no client_secret) MUST provide a PKCE code_challenge.
277            let is_public_client = client_data
278                .get("client_secret")
279                .and_then(|v| v.as_str())
280                .map_or(true, |s| s.is_empty());
281            if is_public_client && params.code_challenge.is_none() {
282                let error = OAuthError::new("invalid_request")
283                    .description("Public clients must use PKCE: code_challenge is required")
284                    .maybe_state(params.state);
285                return (StatusCode::BAD_REQUEST, Json(error)).into_response();
286            }
287        }
288        Ok(None) => {
289            tracing::warn!(client_id = %params.client_id, "OAuth authorize: unknown client_id");
290            let error = OAuthError::new("invalid_client")
291                .description("Unknown client_id")
292                .maybe_state(params.state);
293            return (StatusCode::BAD_REQUEST, Json(error)).into_response();
294        }
295        Err(e) => {
296            tracing::error!(
297                client_id = %params.client_id,
298                error = %e,
299                "OAuth authorize: storage error looking up client"
300            );
301            let error = OAuthError::new("server_error")
302                .description("Authorization server error")
303                .maybe_state(params.state);
304            return (StatusCode::INTERNAL_SERVER_ERROR, Json(error)).into_response();
305        }
306    }
307
308    let auth_code = format!("ac_{}", uuid::Uuid::new_v4().to_string().replace("-", ""));
309
310    // RFC 8707: Validate resource indicators if present.
311    if let Some(ref resources) = params.resource {
312        if let Err(e) = crate::server::oauth::resource_indicators::validate_resource_indicators(resources) {
313            let error = OAuthError::new("invalid_target")
314                .description(e.to_string())
315                .maybe_state(params.state);
316            return (StatusCode::BAD_REQUEST, Json(error)).into_response();
317        }
318    }
319
320    let code_data = serde_json::json!({
321        "client_id": params.client_id,
322        "redirect_uri": params.redirect_uri,
323        "scope": params.scope.clone().unwrap_or_else(|| "openid profile email".to_string()),
324        "state": params.state.clone(),
325        "code_challenge": params.code_challenge,
326        "code_challenge_method": params.code_challenge_method,
327        "user_id": user_id,
328        "resource": params.resource,
329        "created_at": chrono::Utc::now().to_rfc3339(),
330        "expires_at": (chrono::Utc::now() + chrono::Duration::minutes(10)).to_rfc3339(),
331        "used": false,
332    });
333
334    let storage_key = format!("oauth2_code:{}", auth_code);
335    let code_data_str = match serde_json::to_string(&code_data) {
336        Ok(s) => s,
337        Err(e) => {
338            tracing::error!("Failed to serialize OAuth authorization code data: {:?}", e);
339            let error = OAuthError::new("server_error")
340                .description("Authorization server internal error");
341            return (StatusCode::INTERNAL_SERVER_ERROR, Json(error)).into_response();
342        }
343    };
344
345    if let Err(e) = state
346        .auth_framework
347        .storage()
348        .store_kv(
349            &storage_key,
350            code_data_str.as_bytes(),
351            Some(std::time::Duration::from_secs(600)),
352        )
353        .await
354    {
355        tracing::error!("Failed to store OAuth authorization code: {:?}", e);
356        let error = OAuthError::new("server_error")
357            .description("Authorization server error")
358            .maybe_state(params.state);
359        return (StatusCode::INTERNAL_SERVER_ERROR, Json(error)).into_response();
360    }
361
362    // SECURITY: URL-encode the state value to prevent parameter injection.
363    // A raw state like "&extra=injected" would append unintended query parameters.
364    let encoded_state: Option<String> = params.state.as_deref().map(|st| {
365        st.bytes()
366            .flat_map(|b| {
367                if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
368                    vec![b as char]
369                } else {
370                    format!("%{:02X}", b).chars().collect()
371                }
372            })
373            .collect()
374    });
375
376    let mut redirect_url = params.redirect_uri;
377    redirect_url.push_str(&format!("?code={}", auth_code));
378    if let Some(ref st) = encoded_state {
379        redirect_url.push_str(&format!("&state={}", st));
380    }
381
382    tracing::info!(
383        client_id = %params.client_id,
384        user_id = %user_id,
385        "OAuth authorization code issued"
386    );
387    Redirect::to(&redirect_url).into_response()
388}
389
390/// POST /oauth/token — exchange an authorization code or refresh token for
391/// an access token (RFC 6749 §4.1.3, §6).
392///
393/// Supported grant types:
394/// - `authorization_code` — requires `code`, `client_id`, and optionally `code_verifier`
395/// - `refresh_token` — requires `refresh_token`
396///
397/// # Example
398/// ```rust,ignore
399/// // POST /oauth/token {"grant_type":"authorization_code","code":"ac_...","client_id":"..."}
400/// let response = token(State(api_state), Json(token_request)).await;
401/// assert!(response.success);
402/// ```
403pub async fn token(
404    State(state): State<ApiState>,
405    Json(req): Json<TokenRequest>,
406) -> ApiResponse<TokenResponse> {
407    match req.grant_type.as_str() {
408        "authorization_code" => handle_authorization_code_grant(state, req).await,
409        "refresh_token" => handle_refresh_token_grant(state, req).await,
410        _ => ApiResponse::error_typed(
411            "unsupported_grant_type",
412            "Supported grant types: authorization_code, refresh_token",
413        ),
414    }
415}
416
417async fn handle_authorization_code_grant(
418    state: ApiState,
419    req: TokenRequest,
420) -> ApiResponse<TokenResponse> {
421    let code = match req.code {
422        Some(c) => c,
423        None => {
424            return ApiResponse::validation_error_typed(
425                "code is required for authorization_code grant",
426            );
427        }
428    };
429
430    let client_id = match req.client_id {
431        Some(c) => c,
432        None => return ApiResponse::validation_error_typed("client_id is required"),
433    };
434
435    // Defense-in-depth: Use a consumed marker to mitigate the non-atomic get+delete
436    // race condition (TOCTOU). If a concurrent request already marked the code as
437    // consumed, reject this request immediately.
438    let consumed_key = format!("oauth2_code_consumed:{}", code);
439    if let Ok(Some(_)) = state.auth_framework.storage().get_kv(&consumed_key).await {
440        return ApiResponse::error_typed("invalid_grant", "Authorization code already used");
441    }
442    // Mark code as consumed BEFORE reading it, narrowing the race window.
443    if let Err(e) = state
444        .auth_framework
445        .storage()
446        .store_kv(
447            &consumed_key,
448            b"1",
449            Some(std::time::Duration::from_secs(600)),
450        )
451        .await
452    {
453        tracing::warn!("Failed to store code consumed marker: {:?}", e);
454    }
455
456    let storage_key = format!("oauth2_code:{}", code);
457    let code_data = match state.auth_framework.storage().get_kv(&storage_key).await {
458        Ok(Some(data)) => match serde_json::from_slice::<serde_json::Value>(&data) {
459            Ok(json) => json,
460            Err(e) => {
461                tracing::error!("Failed to parse stored authorization code data: {:?}", e);
462                return ApiResponse::error_typed("invalid_grant", "Invalid authorization code");
463            }
464        },
465        Ok(None) => {
466            return ApiResponse::error_typed(
467                "invalid_grant",
468                "Authorization code not found or expired",
469            );
470        }
471        Err(e) => {
472            tracing::error!("Failed to retrieve authorization code: {:?}", e);
473            return ApiResponse::error_typed(
474                "server_error",
475                "Failed to validate authorization code",
476            );
477        }
478    };
479
480    // Immediately delete the code to prevent reuse (single-use enforcement).
481    if let Err(e) = state.auth_framework.storage().delete_kv(&storage_key).await {
482        tracing::error!("Failed to delete authorization code from storage: {:?}", e);
483        return ApiResponse::error_typed("server_error", "Failed to process authorization code");
484    }
485
486    // Validate client_id matches
487    if code_data["client_id"].as_str() != Some(&client_id) {
488        return ApiResponse::error_typed("invalid_grant", "client_id mismatch");
489    }
490
491    // Validate redirect_uri if provided
492    if let Some(redirect_uri) = &req.redirect_uri {
493        let stored_uri = code_data["redirect_uri"].as_str().unwrap_or_default();
494        if !redirect_uri_matches(redirect_uri, stored_uri) {
495            return ApiResponse::error_typed("invalid_grant", "redirect_uri mismatch");
496        }
497    }
498
499    // Check if PKCE was used in authorization - if so, code_verifier is required
500    let stored_challenge = code_data["code_challenge"].as_str();
501    let challenge_method = code_data["code_challenge_method"]
502        .as_str()
503        .unwrap_or("plain");
504
505    if let Some(stored) = stored_challenge {
506        // PKCE was used in authorization, so code_verifier is required
507        let code_verifier = match &req.code_verifier {
508            Some(verifier) => verifier,
509            None => {
510                return ApiResponse::error_typed(
511                    "invalid_request",
512                    "code_verifier is required when PKCE challenge was provided",
513                );
514            }
515        };
516
517        let computed_challenge = match challenge_method {
518            "S256" => {
519                let mut hasher = Sha256::new();
520                hasher.update(code_verifier.as_bytes());
521                URL_SAFE_NO_PAD.encode(hasher.finalize())
522            }
523            "plain" => code_verifier.clone(),
524            _ => {
525                return ApiResponse::error_typed(
526                    "invalid_request",
527                    "Unsupported code_challenge_method",
528                );
529            }
530        };
531
532        if computed_challenge != stored {
533            return ApiResponse::error_typed("invalid_grant", "PKCE verification failed");
534        }
535    } else if req.code_verifier.is_some() {
536        // code_verifier provided but no challenge was used - this is suspicious
537        return ApiResponse::error_typed(
538            "invalid_request",
539            "code_verifier provided but no PKCE challenge was used in authorization",
540        );
541    } else if req.client_secret.is_none() {
542        // S8: Public clients (no client_secret) MUST use PKCE.
543        // If no code_challenge was stored and no client_secret is provided,
544        // the request is from an unauthenticated public client without PKCE.
545        return ApiResponse::error_typed(
546            "invalid_request",
547            "Public clients must use PKCE: provide code_challenge in authorization and code_verifier in token request",
548        );
549    }
550
551    // Code was already atomically deleted above (delete-first approach).
552    // No need to mark as used — the code is permanently consumed.
553
554    // RFC 8707: Validate resource indicators on the token request.
555    let authz_resources: Vec<String> = code_data["resource"]
556        .as_array()
557        .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
558        .unwrap_or_default();
559
560    if let Some(ref token_resources) = req.resource {
561        if let Err(e) = crate::server::oauth::resource_indicators::validate_resource_indicators(token_resources) {
562            return ApiResponse::error_typed("invalid_target", &e.to_string());
563        }
564        if let Err(e) = crate::server::oauth::resource_indicators::validate_token_resource_subset(token_resources, &authz_resources) {
565            return ApiResponse::error_typed("invalid_target", &e.to_string());
566        }
567    }
568
569    // Create access and refresh tokens
570    let scope = code_data["scope"]
571        .as_str()
572        .unwrap_or("openid profile email");
573    let scopes: Vec<String> = scope.split_whitespace().map(|s| s.to_string()).collect();
574
575    // Use the user_id that was recorded in the authorization code when the resource owner
576    // authenticated via the /oauth/authorize endpoint.  This ensures tokens are bound to the
577    // actual user rather than a fabricated identifier derived from the client_id.
578    let user_id = match code_data["user_id"].as_str() {
579        Some(uid) if !uid.is_empty() => uid.to_string(),
580        _ => {
581            tracing::error!("Authorization code missing user_id field");
582            return ApiResponse::error_typed("server_error", "Malformed authorization code");
583        }
584    };
585
586    let token = match state.auth_framework.token_manager().create_auth_token(
587        &user_id,
588        scopes.clone(),
589        "oauth2",
590        None,
591    ) {
592        Ok(token) => token,
593        Err(e) => {
594            tracing::error!("Failed to create access token: {:?}", e);
595            return ApiResponse::error_typed("server_error", "Failed to create access token");
596        }
597    };
598
599    // Persist a storage-backed refresh token so the refresh grant can validate and rotate it.
600    let refresh_token_value = uuid::Uuid::new_v4().to_string().replace("-", "");
601    let refresh_data = serde_json::json!({
602        "user_id": user_id,
603        "client_id": client_id,
604        "scopes": scope,
605    });
606    let refresh_key = format!("oauth2_refresh_token:{}", refresh_token_value);
607    if let Err(e) = state
608        .auth_framework
609        .storage()
610        .store_kv(
611            &refresh_key,
612            serde_json::to_string(&refresh_data)
613                .unwrap_or_default()
614                .as_bytes(),
615            Some(std::time::Duration::from_secs(30 * 24 * 3600)),
616        )
617        .await
618    {
619        tracing::warn!("Failed to store refresh token: {:?}", e);
620    }
621
622    let expires_in = (token.expires_at - token.issued_at).num_seconds().max(0) as u64;
623    let response = TokenResponse {
624        access_token: token.access_token,
625        token_type: "Bearer".to_string(),
626        expires_in,
627        refresh_token: Some(refresh_token_value),
628        scope: Some(scope.to_string()),
629        id_token: None,
630    };
631
632    tracing::info!("OAuth2 tokens issued for client: {}", client_id);
633    ApiResponse::success(response)
634}
635
636async fn handle_refresh_token_grant(
637    state: ApiState,
638    req: TokenRequest,
639) -> ApiResponse<TokenResponse> {
640    let refresh_token_str = match req.refresh_token {
641        Some(t) => t,
642        None => return ApiResponse::validation_error_typed("refresh_token is required"),
643    };
644
645    // Defense-in-depth: Use a consumed marker to mitigate the non-atomic get+delete
646    // race condition. If a concurrent request already consumed this refresh token, reject.
647    let consumed_key = format!("oauth2_refresh_consumed:{}", refresh_token_str);
648    if let Ok(Some(_)) = state.auth_framework.storage().get_kv(&consumed_key).await {
649        return ApiResponse::error_typed("invalid_grant", "Refresh token already consumed");
650    }
651    if let Err(e) = state
652        .auth_framework
653        .storage()
654        .store_kv(
655            &consumed_key,
656            b"1",
657            Some(std::time::Duration::from_secs(600)),
658        )
659        .await
660    {
661        tracing::warn!("Failed to store refresh consumed marker: {:?}", e);
662    }
663
664    // Validate the refresh token against persistent storage.
665    let refresh_key = format!("oauth2_refresh_token:{}", refresh_token_str);
666    let stored = match state.auth_framework.storage().get_kv(&refresh_key).await {
667        Ok(Some(data)) => match serde_json::from_slice::<serde_json::Value>(&data) {
668            Ok(v) => v,
669            Err(_) => return ApiResponse::error_typed("invalid_grant", "Invalid refresh token"),
670        },
671        Ok(None) => {
672            return ApiResponse::error_typed("invalid_grant", "Refresh token not found or expired");
673        }
674        Err(e) => {
675            tracing::error!("Failed to retrieve refresh token: {:?}", e);
676            return ApiResponse::error_typed("server_error", "Failed to validate refresh token");
677        }
678    };
679
680    let user_id = match stored["user_id"].as_str() {
681        Some(u) => u.to_string(),
682        None => return ApiResponse::error_typed("invalid_grant", "Malformed refresh token data"),
683    };
684    let scope = stored["scopes"]
685        .as_str()
686        .unwrap_or("openid profile email")
687        .to_string();
688    let scopes: Vec<String> = scope.split_whitespace().map(|s| s.to_string()).collect();
689
690    let token = match state
691        .auth_framework
692        .token_manager()
693        .create_auth_token(&user_id, scopes, "oauth2", None)
694    {
695        Ok(t) => t,
696        Err(e) => {
697            tracing::error!("Failed to create access token on refresh: {:?}", e);
698            return ApiResponse::error_typed("server_error", "Failed to issue access token");
699        }
700    };
701
702    // Issue a new refresh token (rotation).
703    // Store the new token BEFORE deleting the old one to avoid data loss on failure.
704    let new_refresh_token = uuid::Uuid::new_v4().to_string().replace("-", "");
705    let new_refresh_data = serde_json::json!({ "user_id": user_id, "scopes": scope });
706    let new_refresh_key = format!("oauth2_refresh_token:{}", new_refresh_token);
707    if let Err(e) = state
708        .auth_framework
709        .storage()
710        .store_kv(
711            &new_refresh_key,
712            serde_json::to_string(&new_refresh_data)
713                .unwrap_or_default()
714                .as_bytes(),
715            Some(std::time::Duration::from_secs(30 * 24 * 3600)),
716        )
717        .await
718    {
719        tracing::error!("Failed to store new refresh token: {:?}", e);
720        return ApiResponse::error_typed("server_error", "Failed to issue refresh token");
721    }
722
723    // Now that the new token is safely stored, delete the old one (single-use enforcement).
724    if let Err(e) = state.auth_framework.storage().delete_kv(&refresh_key).await {
725        tracing::warn!(
726            "Failed to delete old refresh token during rotation: {:?}",
727            e
728        );
729    }
730
731    let expires_in = (token.expires_at - token.issued_at).num_seconds().max(0) as u64;
732    let response = TokenResponse {
733        access_token: token.access_token,
734        token_type: "Bearer".to_string(),
735        expires_in,
736        refresh_token: Some(new_refresh_token),
737        scope: Some(scope),
738        id_token: None,
739    };
740
741    tracing::info!("OAuth2 token refreshed for user: {}", user_id);
742    ApiResponse::success(response)
743}
744
745/// POST /api/v1/oauth/revoke — revoke an access or refresh token.
746///
747/// Stores a revocation marker in KV (`oauth2_revoked_token:{token}`) and,
748/// for JWT tokens, also stores a JTI-based marker (`revoked_token:{jti}`)
749/// so that all authenticated endpoints reject the token immediately.
750///
751/// # Example
752/// ```rust,ignore
753/// let resp = revoke(State(state), Json(RevokeRequest {
754///     token: "access_token_value".into(),
755///     token_type_hint: Some("access_token".into()),
756/// })).await;
757/// assert!(resp.success);
758/// ```
759pub async fn revoke(
760    State(state): State<ApiState>,
761    Json(req): Json<RevokeRequest>,
762) -> ApiResponse<serde_json::Value> {
763    // Store the revoked token in a blacklist for immediate invalidation.
764    // Key by the raw token value so the userinfo endpoint can check it too.
765    let revoked_token_key = format!("oauth2_revoked_token:{}", req.token);
766    let revoked_data = serde_json::json!({
767        "token": req.token,
768        "revoked_at": chrono::Utc::now().to_rfc3339(),
769        "token_type_hint": req.token_type_hint
770    });
771
772    if let Err(e) = state
773        .auth_framework
774        .storage()
775        .store_kv(
776            &revoked_token_key,
777            serde_json::to_string(&revoked_data)
778                .unwrap_or_default()
779                .as_bytes(),
780            Some(std::time::Duration::from_secs(86400 * 7)),
781        )
782        .await
783    {
784        tracing::error!("Failed to store revoked token: {:?}", e);
785        return ApiResponse::error_typed("server_error", "Failed to revoke token");
786    }
787
788    // If the token is a JWT, also store revoked_token:{jti} so the authentication
789    // middleware (validate_api_token in api/mod.rs) blocks it on all protected
790    // endpoints. Without this step, the revocation would only be visible to the
791    // userinfo endpoint, leaving all other authenticated routes unprotected.
792    if let Ok(claims) = state
793        .auth_framework
794        .token_manager()
795        .validate_jwt_token(&req.token)
796    {
797        let now = chrono::Utc::now().timestamp();
798        let remaining_secs = (claims.exp - now + 60).max(60) as u64;
799        let jti_key = format!("revoked_token:{}", claims.jti);
800        if let Err(e) = state
801            .auth_framework
802            .storage()
803            .store_kv(
804                &jti_key,
805                b"1",
806                Some(std::time::Duration::from_secs(remaining_secs)),
807            )
808            .await
809        {
810            tracing::warn!("Failed to store JWT JTI revocation entry: {:?}", e);
811        } else {
812            tracing::info!("JWT token revoked via jti: {}", claims.jti);
813        }
814    }
815
816    tracing::info!(
817        "OAuth2 token revoked: {}",
818        &req.token[..10.min(req.token.len())]
819    );
820
821    ApiResponse::success(serde_json::json!({
822        "message": "Token revoked successfully"
823    }))
824}
825
826/// GET /api/v1/oauth/userinfo — return claims about the authenticated user.
827///
828/// The caller must supply a valid Bearer access token. If the token has been
829/// revoked, an `invalid_token` error is returned. Email is included only
830/// when the `email` scope is present in the token.
831///
832/// # Example
833/// ```rust,ignore
834/// // GET /api/v1/oauth/userinfo  Authorization: Bearer <token>
835/// let resp = userinfo(State(state), headers).await;
836/// assert_eq!(resp.data.unwrap().sub, "user-1");
837/// ```
838pub async fn userinfo(
839    State(state): State<ApiState>,
840    headers: HeaderMap,
841) -> ApiResponse<UserInfoResponse> {
842    // Extract and validate access token
843    let token = match extract_bearer_token(&headers) {
844        Some(t) => t,
845        None => {
846            return ApiResponse::error_typed("invalid_token", "Authorization header required");
847        }
848    };
849
850    // Check if token is revoked first
851    let revoked_token_key = format!("oauth2_revoked_token:{}", token);
852    if let Ok(Some(_)) = state
853        .auth_framework
854        .storage()
855        .get_kv(&revoked_token_key)
856        .await
857    {
858        return ApiResponse::error_typed("invalid_token", "Token has been revoked");
859    }
860
861    // Validate the access token
862    let claims = match state
863        .auth_framework
864        .token_manager()
865        .validate_jwt_token(&token)
866    {
867        Ok(c) => c,
868        Err(_) => {
869            return ApiResponse::error_typed("invalid_token", "Access token is invalid");
870        }
871    };
872
873    // Get user profile
874    let user_profile = match state.auth_framework.get_user_profile(&claims.sub).await {
875        Ok(profile) => profile,
876        Err(e) => {
877            tracing::error!("Failed to get user profile: {:?}", e);
878            return ApiResponse::error_typed("server_error", "Failed to retrieve user information");
879        }
880    };
881
882    let userinfo = UserInfoResponse {
883        sub: claims.sub.clone(),
884        name: user_profile.username.clone(),
885        // Only include email if the token's scopes include "email"
886        email: if claims.scope.split_whitespace().any(|s| s == "email") {
887            user_profile.email.clone()
888        } else {
889            None
890        },
891        picture: user_profile.picture.clone(),
892        updated_at: Some(chrono::Utc::now().timestamp()),
893    };
894
895    tracing::info!("OAuth2 UserInfo requested for user: {}", claims.sub);
896    ApiResponse::success(userinfo)
897}
898
899/// GET /oauth/clients/{client_id} — return the stored metadata for a
900/// registered OAuth 2.0 client.
901///
902/// # Example
903/// ```rust,ignore
904/// let resp = get_client_info(State(state), Path("my-client-id".into())).await;
905/// ```
906pub async fn get_client_info(
907    State(state): State<ApiState>,
908    Path(client_id): Path<String>,
909) -> impl IntoResponse {
910    let client_key = format!("oauth2_client:{}", client_id);
911    match state.auth_framework.storage().get_kv(&client_key).await {
912        Ok(Some(data)) => match serde_json::from_slice::<ClientInfo>(&data) {
913            Ok(client) => (
914                StatusCode::OK,
915                Json(serde_json::json!({ "success": true, "data": client })),
916            )
917                .into_response(),
918            Err(e) => {
919                tracing::error!(client_id = %client_id, error = %e, "Failed to deserialize client record");
920                (
921                    StatusCode::INTERNAL_SERVER_ERROR,
922                    Json(serde_json::json!({
923                        "success": false,
924                        "error": "server_error",
925                        "message": "Failed to read client record"
926                    })),
927                )
928                    .into_response()
929            }
930        },
931        Ok(None) => (
932            StatusCode::NOT_FOUND,
933            Json(serde_json::json!({
934                "success": false,
935                "error": "invalid_client",
936                "message": "Unknown client_id"
937            })),
938        )
939            .into_response(),
940        Err(e) => {
941            tracing::error!(client_id = %client_id, error = %e, "Storage error looking up client");
942            (
943                StatusCode::INTERNAL_SERVER_ERROR,
944                Json(serde_json::json!({
945                    "success": false,
946                    "error": "server_error",
947                    "message": "Authorization server error"
948                })),
949            )
950                .into_response()
951        }
952    }
953}
954
955// ---------------------------------------------------------------------------
956// OpenID Connect Discovery (RFC 8414 / OpenID Connect Discovery 1.0)
957// ---------------------------------------------------------------------------
958
959/// OpenID Connect Discovery endpoint (RFC 8414 / OpenID Connect Discovery 1.0).
960///
961/// Returns server authorization metadata so clients can auto-configure
962/// (issuer, endpoints, supported grants, signing algorithms, etc.).
963///
964/// # Example
965/// ```rust,ignore
966/// // GET /.well-known/openid-configuration
967/// let (status, Json(config)) = openid_configuration(State(state)).await;
968/// assert_eq!(status, StatusCode::OK);
969/// ```
970pub async fn openid_configuration(State(state): State<ApiState>) -> impl IntoResponse {
971    let issuer = {
972        let configured = state.auth_framework.config().issuer.clone();
973        if configured.is_empty() {
974            "https://auth.example.com".to_string()
975        } else {
976            configured
977        }
978    };
979
980    let config = serde_json::json!({
981        "issuer": issuer,
982        "authorization_endpoint": format!("{}/api/v1/oauth/authorize", issuer),
983        "token_endpoint": format!("{}/api/v1/oauth/token", issuer),
984        "userinfo_endpoint": format!("{}/api/v1/oauth/userinfo", issuer),
985        "revocation_endpoint": format!("{}/api/v1/oauth/revoke", issuer),
986        "introspection_endpoint": format!("{}/api/v1/oauth/introspect", issuer),
987        "jwks_uri": format!("{}/api/v1/.well-known/jwks.json", issuer),
988        "end_session_endpoint": format!("{}/api/v1/oauth/end_session", issuer),
989        "pushed_authorization_request_endpoint": format!("{}/api/v1/oauth/par", issuer),
990        "registration_endpoint": format!("{}/api/v1/oauth/register", issuer),
991        "response_types_supported": ["code"],
992        "grant_types_supported": [
993            "authorization_code",
994            "refresh_token"
995        ],
996        "subject_types_supported": ["public"],
997        "id_token_signing_alg_values_supported": ["HS256"],
998        "token_endpoint_auth_methods_supported": [
999            "client_secret_basic",
1000            "client_secret_post"
1001        ],
1002        "code_challenge_methods_supported": ["S256"],
1003        "scopes_supported": ["openid", "profile", "email"]
1004    });
1005
1006    (StatusCode::OK, Json(config))
1007}
1008
1009// ---------------------------------------------------------------------------
1010// JWKS endpoint (RFC 7517)
1011// ---------------------------------------------------------------------------
1012
1013/// JSON Web Key Set (JWKS) endpoint ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)).
1014///
1015/// Returns the public keys used to verify tokens. Currently returns an
1016/// empty key set because HS256 (symmetric) signing is used; clients
1017/// should use the introspection endpoint for token validation.
1018///
1019/// # Example
1020/// ```rust,ignore
1021/// // GET /.well-known/jwks.json
1022/// let (status, Json(jwks)) = jwks(State(state)).await;
1023/// assert!(jwks["keys"].as_array().unwrap().is_empty());
1024/// ```
1025pub async fn jwks(State(_state): State<ApiState>) -> impl IntoResponse {
1026    (StatusCode::OK, Json(serde_json::json!({ "keys": [] })))
1027}
1028
1029// ---------------------------------------------------------------------------
1030// OIDC RP-Initiated Logout (OpenID Connect RP-Initiated Logout 1.0)
1031// ---------------------------------------------------------------------------
1032
1033/// Request parameters for OpenID Connect RP-Initiated Logout.
1034///
1035/// # Example
1036/// ```rust
1037/// use auth_framework::api::oauth2::EndSessionRequest;
1038///
1039/// let req: EndSessionRequest = serde_json::from_str(
1040///     r#"{"id_token_hint":"eyJ...","state":"xyz"}"#
1041/// ).unwrap();
1042/// assert!(req.post_logout_redirect_uri.is_none());
1043/// ```
1044#[derive(Debug, Deserialize)]
1045pub struct EndSessionRequest {
1046    pub id_token_hint: Option<String>,
1047    pub post_logout_redirect_uri: Option<String>,
1048    pub state: Option<String>,
1049}
1050
1051/// OIDC RP-Initiated Logout (OpenID Connect RP-Initiated Logout 1.0).
1052///
1053/// Revokes the token from `id_token_hint` (if valid) and, when a
1054/// registered `post_logout_redirect_uri` is provided, redirects the
1055/// user-agent there.
1056///
1057/// # Example
1058/// ```rust,ignore
1059/// // GET /oauth/end_session?id_token_hint=eyJ...&state=xyz
1060/// let resp = end_session(State(state), Query(params)).await;
1061/// ```
1062pub async fn end_session(
1063    State(state): State<ApiState>,
1064    Query(params): Query<EndSessionRequest>,
1065) -> impl IntoResponse {
1066    // If an id_token_hint is provided, revoke it
1067    if let Some(ref token) = params.id_token_hint {
1068        if let Ok(claims) = state
1069            .auth_framework
1070            .token_manager()
1071            .validate_jwt_token(token)
1072        {
1073            let revoked_key = format!("oauth2_revoked_token:{}", token);
1074            if let Err(e) = state
1075                .auth_framework
1076                .storage()
1077                .store_kv(
1078                    &revoked_key,
1079                    b"revoked",
1080                    Some(std::time::Duration::from_secs(86400 * 7)),
1081                )
1082                .await
1083            {
1084                tracing::warn!("Failed to revoke token during OIDC end_session: {}", e);
1085            }
1086            tracing::info!("OIDC end_session: revoked token for user {}", claims.sub);
1087        }
1088    }
1089
1090    // Redirect to post_logout_redirect_uri only if it matches a registered URI for the
1091    // client identified by id_token_hint.  Without this check an attacker could craft a
1092    // link that redirects users to an arbitrary URL (open redirect / phishing).
1093    if let Some(ref redirect_uri) = params.post_logout_redirect_uri {
1094        // Look up the client's registered redirect URIs via the id_token_hint's `azp` or `aud` claim.
1095        let allowed = if let Some(ref token) = params.id_token_hint {
1096            // Try to extract client_id from the token (azp > aud)
1097            let client_id = state
1098                .auth_framework
1099                .token_manager()
1100                .validate_jwt_token(token)
1101                .ok()
1102                .and_then(|claims| {
1103                    // client_id claim identifies the OAuth client
1104                    if let Some(ref cid) = claims.client_id {
1105                        if !cid.is_empty() {
1106                            return Some(cid.clone());
1107                        }
1108                    }
1109                    // Fallback: aud may contain the client_id
1110                    if !claims.aud.is_empty() {
1111                        Some(claims.aud.clone())
1112                    } else {
1113                        None
1114                    }
1115                });
1116            if let Some(cid) = client_id {
1117                let client_key = format!("oauth2_client:{}", cid);
1118                match state.auth_framework.storage().get_kv(&client_key).await {
1119                    Ok(Some(data)) => {
1120                        let client: serde_json::Value =
1121                            serde_json::from_slice(&data).unwrap_or_default();
1122                        let uris: Vec<String> = client["redirect_uris"]
1123                            .as_array()
1124                            .map(|a| {
1125                                a.iter()
1126                                    .filter_map(|v| v.as_str().map(String::from))
1127                                    .collect()
1128                            })
1129                            .unwrap_or_default();
1130                        uris.iter().any(|r| redirect_uri_matches(redirect_uri, r))
1131                    }
1132                    _ => false,
1133                }
1134            } else {
1135                false
1136            }
1137        } else {
1138            false
1139        };
1140
1141        if allowed {
1142            if let Ok(mut parsed) = Url::parse(redirect_uri) {
1143                if let Some(ref st) = params.state {
1144                    parsed.query_pairs_mut().append_pair("state", st);
1145                }
1146                return Redirect::to(parsed.as_str()).into_response();
1147            }
1148        } else {
1149            tracing::warn!(
1150                "end_session: post_logout_redirect_uri rejected — not registered for the client"
1151            );
1152        }
1153    }
1154
1155    // No redirect — return a simple JSON acknowledgement
1156    (
1157        StatusCode::OK,
1158        Json(serde_json::json!({ "status": "logged_out" })),
1159    )
1160        .into_response()
1161}
1162
1163// ---------------------------------------------------------------------------
1164// Dynamic Client Registration (RFC 7591)
1165// ---------------------------------------------------------------------------
1166
1167/// Dynamic client registration request per [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591).
1168///
1169/// # Example
1170/// ```rust
1171/// use auth_framework::api::oauth2::ClientRegistrationRequest;
1172///
1173/// let req: ClientRegistrationRequest = serde_json::from_str(
1174///     r#"{"redirect_uris":["https://example.com/cb"]}"#
1175/// ).unwrap();
1176/// assert_eq!(req.redirect_uris.len(), 1);
1177/// ```
1178#[derive(Debug, Deserialize)]
1179pub struct ClientRegistrationRequest {
1180    pub redirect_uris: Vec<String>,
1181    #[serde(default)]
1182    pub client_name: Option<String>,
1183    #[serde(default)]
1184    pub token_endpoint_auth_method: Option<String>,
1185    #[serde(default)]
1186    pub grant_types: Option<Vec<String>>,
1187    #[serde(default)]
1188    pub response_types: Option<Vec<String>>,
1189    #[serde(default)]
1190    pub scope: Option<String>,
1191}
1192
1193/// POST /oauth/register — dynamically register a new OAuth 2.0 client
1194/// ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)).
1195///
1196/// Requires a valid Bearer token with `admin` role, or an Initial Access
1197/// Token stored in KV at `oauth2_initial_access_token`. Unauthenticated
1198/// callers are rejected to prevent resource exhaustion.
1199///
1200/// # Example
1201/// ```rust,ignore
1202/// let resp = register_client(
1203///     State(state),
1204///     headers,   // with Authorization: Bearer <admin-token>
1205///     Json(ClientRegistrationRequest {
1206///         redirect_uris: vec!["https://example.com/cb".into()],
1207///         client_name: Some("My App".into()),
1208///         ..Default::default()
1209///     }),
1210/// ).await;
1211/// ```
1212pub async fn register_client(
1213    State(state): State<ApiState>,
1214    headers: HeaderMap,
1215    Json(req): Json<ClientRegistrationRequest>,
1216) -> impl IntoResponse {
1217    // RFC 7591 §1.2: require authentication via Bearer token
1218    let token_str = match extract_bearer_token(&headers) {
1219        Some(t) => t,
1220        None => {
1221            return (
1222                StatusCode::UNAUTHORIZED,
1223                Json(serde_json::json!({
1224                    "error": "invalid_token",
1225                    "error_description": "A valid Bearer token is required for dynamic client registration"
1226                })),
1227            )
1228                .into_response();
1229        }
1230    };
1231
1232    // Option 1: check if this is the pre-configured Initial Access Token
1233    let is_initial_access_token = match state
1234        .auth_framework
1235        .storage()
1236        .get_kv("oauth2_initial_access_token")
1237        .await
1238    {
1239        Ok(Some(stored)) => {
1240            let stored_str = String::from_utf8_lossy(&stored);
1241            subtle::ConstantTimeEq::ct_eq(token_str.as_bytes(), stored_str.trim().as_bytes()).into()
1242        }
1243        _ => false,
1244    };
1245
1246    // Option 2: validate as a normal JWT and check for admin role
1247    let is_admin = if !is_initial_access_token {
1248        match validate_api_token(&state.auth_framework, &token_str).await {
1249            Ok(auth_token) => auth_token.roles.contains(&"admin".to_string()),
1250            Err(_) => false,
1251        }
1252    } else {
1253        false
1254    };
1255
1256    if !is_initial_access_token && !is_admin {
1257        return (
1258            StatusCode::FORBIDDEN,
1259            Json(serde_json::json!({
1260                "error": "insufficient_scope",
1261                "error_description": "Dynamic client registration requires admin privileges or a valid Initial Access Token"
1262            })),
1263        )
1264            .into_response();
1265    }
1266    // Validate all redirect_uris are well-formed URLs with safe schemes
1267    for uri in &req.redirect_uris {
1268        match Url::parse(uri) {
1269            Ok(parsed) => {
1270                match parsed.scheme() {
1271                    "https" => {}
1272                    "http" => {
1273                        // Allow plain HTTP only for loopback addresses (development)
1274                        if !matches!(parsed.host_str(), Some("localhost" | "127.0.0.1" | "[::1]")) {
1275                            return (
1276                                StatusCode::BAD_REQUEST,
1277                                Json(serde_json::json!({
1278                                    "error": "invalid_redirect_uri",
1279                                    "error_description": format!("Non-loopback HTTP redirect_uri not allowed: {}", uri)
1280                                })),
1281                            )
1282                                .into_response();
1283                        }
1284                    }
1285                    scheme => {
1286                        return (
1287                            StatusCode::BAD_REQUEST,
1288                            Json(serde_json::json!({
1289                                "error": "invalid_redirect_uri",
1290                                "error_description": format!("Disallowed URI scheme '{}' in redirect_uri: {}", scheme, uri)
1291                            })),
1292                        )
1293                            .into_response();
1294                    }
1295                }
1296            }
1297            Err(_) => {
1298                return (
1299                    StatusCode::BAD_REQUEST,
1300                    Json(serde_json::json!({
1301                        "error": "invalid_redirect_uri",
1302                        "error_description": format!("Invalid redirect_uri: {}", uri)
1303                    })),
1304                )
1305                    .into_response();
1306            }
1307        }
1308    }
1309
1310    if req.redirect_uris.is_empty() {
1311        return (
1312            StatusCode::BAD_REQUEST,
1313            Json(serde_json::json!({
1314                "error": "invalid_client_metadata",
1315                "error_description": "At least one redirect_uri is required"
1316            })),
1317        )
1318            .into_response();
1319    }
1320
1321    let client_id = Uuid::new_v4().to_string();
1322    let client_secret = Uuid::new_v4().to_string();
1323
1324    let client_data = serde_json::json!({
1325        "client_id": client_id,
1326        "client_secret": client_secret,
1327        "client_name": req.client_name,
1328        "redirect_uris": req.redirect_uris,
1329        "token_endpoint_auth_method": req.token_endpoint_auth_method.as_deref().unwrap_or("client_secret_basic"),
1330        "grant_types": req.grant_types.as_deref().unwrap_or(&["authorization_code".to_string()]),
1331        "response_types": req.response_types.as_deref().unwrap_or(&["code".to_string()]),
1332        "scope": req.scope.as_deref().unwrap_or("openid"),
1333    });
1334
1335    let key = format!("oauth2_client:{}", client_id);
1336    if let Err(e) = state
1337        .auth_framework
1338        .storage()
1339        .store_kv(&key, client_data.to_string().as_bytes(), None)
1340        .await
1341    {
1342        tracing::error!("Failed to store client registration: {}", e);
1343        return (
1344            StatusCode::INTERNAL_SERVER_ERROR,
1345            Json(serde_json::json!({
1346                "error": "server_error",
1347                "error_description": "Failed to register client"
1348            })),
1349        )
1350            .into_response();
1351    }
1352
1353    tracing::info!("Dynamic client registered: client_id={}", client_id);
1354
1355    (
1356        StatusCode::CREATED,
1357        Json(serde_json::json!({
1358            "client_id": client_id,
1359            "client_secret": client_secret,
1360            "client_name": req.client_name,
1361            "redirect_uris": req.redirect_uris,
1362            "token_endpoint_auth_method": req.token_endpoint_auth_method.as_deref().unwrap_or("client_secret_basic"),
1363            "grant_types": req.grant_types.as_deref().unwrap_or(&["authorization_code".to_string()]),
1364            "response_types": req.response_types.as_deref().unwrap_or(&["code".to_string()]),
1365            "scope": req.scope.as_deref().unwrap_or("openid"),
1366        })),
1367    )
1368        .into_response()
1369}
1370
1371// ---------------------------------------------------------------------------
1372// /users/me — alias for the authenticated user's profile
1373// ---------------------------------------------------------------------------
1374
1375/// GET /users/me — returns the authenticated user's profile.
1376///
1377/// Convenience alias that delegates to [`userinfo`].
1378///
1379/// # Example
1380/// ```rust,ignore
1381/// // GET /users/me  Authorization: Bearer <token>
1382/// let resp = users_me(state, headers).await;
1383/// ```
1384pub async fn users_me(state: State<ApiState>, headers: HeaderMap) -> impl IntoResponse {
1385    // Delegate to the existing userinfo handler
1386    userinfo(state, headers).await
1387}