auth_framework/api/
oauth.rs

1//! OAuth 2.0 API Endpoints
2//!
3//! Handles OAuth 2.0 authorization, token exchange, and related operations
4
5use crate::api::{ApiResponse, ApiState};
6use axum::{
7    Json,
8    extract::{Query, State},
9    http::{HeaderMap, StatusCode},
10    response::{IntoResponse, Redirect},
11};
12use serde::{Deserialize, Serialize};
13
14/// OAuth authorization request parameters
15#[derive(Debug, Deserialize)]
16pub struct AuthorizeRequest {
17    pub response_type: String,
18    pub client_id: String,
19    pub redirect_uri: String,
20    pub scope: Option<String>,
21    pub state: Option<String>,
22    pub code_challenge: Option<String>,
23    pub code_challenge_method: Option<String>,
24}
25
26/// OAuth token request
27#[derive(Debug, Deserialize)]
28pub struct TokenRequest {
29    pub grant_type: String,
30    pub code: Option<String>,
31    pub client_id: String,
32    pub client_secret: Option<String>,
33    pub redirect_uri: Option<String>,
34    pub refresh_token: Option<String>,
35    pub code_verifier: Option<String>,
36}
37
38/// OAuth token response
39#[derive(Debug, Serialize)]
40pub struct TokenResponse {
41    pub access_token: String,
42    pub token_type: String,
43    pub expires_in: u64,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub refresh_token: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub scope: Option<String>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub id_token: Option<String>,
50}
51
52/// OAuth error response
53#[derive(Debug, Serialize)]
54pub struct OAuthError {
55    pub error: String,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub error_description: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub error_uri: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub state: Option<String>,
62}
63
64/// Client information
65#[derive(Debug, Serialize)]
66pub struct ClientInfo {
67    pub client_id: String,
68    pub name: String,
69    pub description: String,
70    pub redirect_uris: Vec<String>,
71    pub scopes: Vec<String>,
72}
73
74/// GET /oauth/authorize
75/// OAuth 2.0 authorization endpoint
76pub async fn authorize(
77    State(_state): State<ApiState>,
78    Query(params): Query<AuthorizeRequest>,
79) -> impl IntoResponse {
80    // Validate required parameters
81    if params.response_type != "code" {
82        let error = OAuthError {
83            error: "unsupported_response_type".to_string(),
84            error_description: Some("Only 'code' response type is supported".to_string()),
85            error_uri: None,
86            state: params.state,
87        };
88        return (StatusCode::BAD_REQUEST, Json(error)).into_response();
89    }
90
91    if params.client_id.is_empty() {
92        let error = OAuthError {
93            error: "invalid_request".to_string(),
94            error_description: Some("client_id is required".to_string()),
95            error_uri: None,
96            state: params.state,
97        };
98        return (StatusCode::BAD_REQUEST, Json(error)).into_response();
99    }
100
101    if params.redirect_uri.is_empty() {
102        let error = OAuthError {
103            error: "invalid_request".to_string(),
104            error_description: Some("redirect_uri is required".to_string()),
105            error_uri: None,
106            state: params.state,
107        };
108        return (StatusCode::BAD_REQUEST, Json(error)).into_response();
109    }
110
111    // In a real implementation:
112    // 1. Validate client_id exists
113    // 2. Validate redirect_uri is registered for client
114    // 3. Check if user is authenticated
115    // 4. Show consent screen if needed
116    // 5. Generate authorization code
117    // 6. Redirect with code
118
119    // For now, simulate successful authorization
120    let auth_code = format!("auth_code_{}", chrono::Utc::now().timestamp());
121    let mut redirect_url = params.redirect_uri;
122
123    redirect_url.push_str(&format!("?code={}", auth_code));
124    if let Some(state) = params.state {
125        redirect_url.push_str(&format!("&state={}", state));
126    }
127
128    tracing::info!("OAuth authorization for client: {}", params.client_id);
129    Redirect::to(&redirect_url).into_response()
130}
131
132/// POST /oauth/token
133/// OAuth 2.0 token endpoint
134pub async fn token(
135    State(state): State<ApiState>,
136    _headers: HeaderMap,
137    Json(req): Json<TokenRequest>,
138) -> ApiResponse<TokenResponse> {
139    // Validate grant type
140    match req.grant_type.as_str() {
141        "authorization_code" => handle_authorization_code_grant(state, req).await,
142        "refresh_token" => handle_refresh_token_grant(state, req).await,
143        "client_credentials" => handle_client_credentials_grant(state, req).await,
144        _ => ApiResponse::error_typed(
145            "unsupported_grant_type",
146            format!("Unsupported grant type: {}", req.grant_type),
147        ),
148    }
149}
150
151async fn handle_authorization_code_grant(
152    _state: ApiState,
153    req: TokenRequest,
154) -> ApiResponse<TokenResponse> {
155    // Validate required parameters
156    if req.code.is_none() {
157        return ApiResponse::error_typed("invalid_request", "authorization code is required");
158    }
159
160    if req.redirect_uri.is_none() {
161        return ApiResponse::error_typed("invalid_request", "redirect_uri is required");
162    }
163
164    // In a real implementation:
165    // 1. Validate authorization code
166    // 2. Verify client credentials
167    // 3. Validate redirect_uri matches
168    // 4. Validate PKCE if used
169    // 5. Generate access token and refresh token
170
171    let response = TokenResponse {
172        access_token: format!("access_token_{}", chrono::Utc::now().timestamp()),
173        token_type: "Bearer".to_string(),
174        expires_in: 3600,
175        refresh_token: Some(format!("refresh_token_{}", chrono::Utc::now().timestamp())),
176        scope: Some("read write".to_string()),
177        id_token: None,
178    };
179
180    tracing::info!("Authorization code exchanged for client: {}", req.client_id);
181    ApiResponse::<TokenResponse>::success(response)
182}
183
184async fn handle_refresh_token_grant(
185    _state: ApiState,
186    req: TokenRequest,
187) -> ApiResponse<TokenResponse> {
188    if req.refresh_token.is_none() {
189        return ApiResponse::error_typed("invalid_request", "refresh_token is required");
190    }
191
192    // In a real implementation:
193    // 1. Validate refresh token
194    // 2. Verify client credentials
195    // 3. Generate new access token
196    // 4. Optionally rotate refresh token
197
198    let response = TokenResponse {
199        access_token: format!("new_access_token_{}", chrono::Utc::now().timestamp()),
200        token_type: "Bearer".to_string(),
201        expires_in: 3600,
202        refresh_token: req.refresh_token, // Reuse existing refresh token
203        scope: Some("read write".to_string()),
204        id_token: None,
205    };
206
207    tracing::info!("Refresh token used for client: {}", req.client_id);
208    ApiResponse::<TokenResponse>::success(response)
209}
210
211async fn handle_client_credentials_grant(
212    _state: ApiState,
213    req: TokenRequest,
214) -> ApiResponse<TokenResponse> {
215    // In a real implementation:
216    // 1. Validate client credentials
217    // 2. Check client is authorized for client_credentials grant
218    // 3. Generate access token (no refresh token for client credentials)
219
220    let response = TokenResponse {
221        access_token: format!("client_access_token_{}", chrono::Utc::now().timestamp()),
222        token_type: "Bearer".to_string(),
223        expires_in: 7200,    // 2 hours for client credentials
224        refresh_token: None, // No refresh token for client credentials
225        scope: Some("api:read api:write".to_string()),
226        id_token: None,
227    };
228
229    tracing::info!("Client credentials grant for client: {}", req.client_id);
230    ApiResponse::<TokenResponse>::success(response)
231}
232
233/// POST /oauth/revoke
234/// Token revocation endpoint
235#[derive(Debug, Deserialize)]
236pub struct RevokeRequest {
237    pub token: String,
238    pub token_type_hint: Option<String>,
239}
240
241pub async fn revoke_token(
242    State(_state): State<ApiState>,
243    Json(req): Json<RevokeRequest>,
244) -> ApiResponse<()> {
245    if req.token.is_empty() {
246        return ApiResponse::validation_error_typed("token is required");
247    }
248
249    // In a real implementation:
250    // 1. Validate client credentials
251    // 2. Identify token type (access or refresh)
252    // 3. Revoke the token
253    // 4. If refresh token, revoke associated access tokens
254
255    tracing::info!("Token revoked: {}", &req.token[..10]);
256    ApiResponse::<()>::ok_with_message("Token revoked successfully")
257}
258
259/// POST /oauth/introspect
260/// Token introspection endpoint (RFC 7662)
261#[derive(Debug, Deserialize)]
262pub struct IntrospectRequest {
263    pub token: String,
264    pub token_type_hint: Option<String>,
265}
266
267#[derive(Debug, Serialize)]
268pub struct IntrospectResponse {
269    pub active: bool,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub scope: Option<String>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub client_id: Option<String>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub username: Option<String>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub token_type: Option<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub exp: Option<u64>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub iat: Option<u64>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub sub: Option<String>,
284}
285
286pub async fn introspect_token(
287    State(_state): State<ApiState>,
288    Json(req): Json<IntrospectRequest>,
289) -> ApiResponse<IntrospectResponse> {
290    if req.token.is_empty() {
291        return ApiResponse::validation_error_typed("token is required");
292    }
293
294    // In a real implementation:
295    // 1. Validate client credentials
296    // 2. Look up token in storage
297    // 3. Check if token is active and not expired
298    // 4. Return token metadata
299
300    let response = IntrospectResponse {
301        active: true, // Placeholder
302        scope: Some("read write".to_string()),
303        client_id: Some("example_client".to_string()),
304        username: Some("user@example.com".to_string()),
305        token_type: Some("Bearer".to_string()),
306        exp: Some(chrono::Utc::now().timestamp() as u64 + 3600),
307        iat: Some(chrono::Utc::now().timestamp() as u64),
308        sub: Some("user_123".to_string()),
309    };
310
311    tracing::info!("Token introspected: {}", &req.token[..10]);
312    ApiResponse::<IntrospectResponse>::success(response)
313}
314
315/// GET /oauth/clients/{client_id}
316/// Get OAuth client information
317pub async fn get_client_info(
318    State(_state): State<ApiState>,
319    axum::extract::Path(client_id): axum::extract::Path<String>,
320) -> ApiResponse<ClientInfo> {
321    // In a real implementation, fetch client from storage
322    let client = ClientInfo {
323        client_id: client_id.clone(),
324        name: format!("Client {}", client_id),
325        description: "OAuth 2.0 client application".to_string(),
326        redirect_uris: vec![
327            "https://example.com/callback".to_string(),
328            "https://app.example.com/auth/callback".to_string(),
329        ],
330        scopes: vec![
331            "read".to_string(),
332            "write".to_string(),
333            "profile".to_string(),
334        ],
335    };
336
337    ApiResponse::<ClientInfo>::success(client)
338}
339
340// ================================================================================================
341// Advanced OAuth2 Features
342// ================================================================================================
343
344/// Token Exchange Request (RFC 8693)
345#[derive(Debug, Deserialize)]
346pub struct TokenExchangeRequest {
347    pub grant_type: String,
348    pub subject_token: String,
349    pub subject_token_type: String,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub actor_token: Option<String>,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub actor_token_type: Option<String>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub requested_token_type: Option<String>,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub scope: Option<String>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub resource: Option<String>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub audience: Option<String>,
362}
363
364/// POST /oauth/token-exchange
365/// RFC 8693 Token Exchange endpoint
366pub async fn token_exchange(
367    State(state): State<ApiState>,
368    Json(req): Json<TokenExchangeRequest>,
369) -> ApiResponse<TokenResponse> {
370    // Validate grant type
371    if req.grant_type != "urn:ietf:params:oauth:grant-type:token-exchange" {
372        return ApiResponse::error_typed(
373            "unsupported_grant_type",
374            "Must be 'urn:ietf:params:oauth:grant-type:token-exchange'",
375        );
376    }
377
378    // Validate subject token
379    if req.subject_token.is_empty() {
380        return ApiResponse::error_typed("invalid_request", "subject_token is required");
381    }
382
383    // Validate the subject token
384    let token_result = state
385        .auth_framework
386        .token_manager()
387        .validate_jwt_token(&req.subject_token);
388
389    let claims = match token_result {
390        Ok(c) => c,
391        Err(_) => {
392            return ApiResponse::error_typed("invalid_token", "Subject token is invalid");
393        }
394    };
395
396    // Create a new token for the exchange
397    let new_token = match state.auth_framework.token_manager().create_auth_token(
398        &claims.sub,
399        claims.roles.unwrap_or_default(),
400        "jwt",
401        None,
402    ) {
403        Ok(token) => token,
404        Err(e) => {
405            tracing::error!("Failed to create exchanged token: {:?}", e);
406            return ApiResponse::error_typed("server_error", "Failed to exchange token");
407        }
408    };
409
410    let response = TokenResponse {
411        access_token: new_token.access_token,
412        token_type: "Bearer".to_string(),
413        expires_in: 3600,
414        refresh_token: new_token.refresh_token,
415        scope: req.scope.clone(),
416        id_token: None,
417    };
418
419    tracing::info!("Token exchanged for user: {}", claims.sub);
420    ApiResponse::success(response)
421}
422
423/// OIDC Discovery Document
424#[derive(Debug, Serialize)]
425pub struct OidcDiscoveryDocument {
426    pub issuer: String,
427    pub authorization_endpoint: String,
428    pub token_endpoint: String,
429    pub userinfo_endpoint: String,
430    pub jwks_uri: String,
431    pub registration_endpoint: Option<String>,
432    pub scopes_supported: Vec<String>,
433    pub response_types_supported: Vec<String>,
434    pub response_modes_supported: Vec<String>,
435    pub grant_types_supported: Vec<String>,
436    pub subject_types_supported: Vec<String>,
437    pub id_token_signing_alg_values_supported: Vec<String>,
438    pub token_endpoint_auth_methods_supported: Vec<String>,
439    pub claims_supported: Vec<String>,
440    pub code_challenge_methods_supported: Vec<String>,
441}
442
443/// GET /.well-known/openid-configuration
444/// OIDC Discovery endpoint
445pub async fn oidc_discovery(State(_state): State<ApiState>) -> Json<OidcDiscoveryDocument> {
446    // TODO: Get base URL from configuration
447    let base_url = "https://auth.example.com"; // Should come from config
448
449    let discovery = OidcDiscoveryDocument {
450        issuer: base_url.to_string(),
451        authorization_endpoint: format!("{}/oauth/authorize", base_url),
452        token_endpoint: format!("{}/oauth/token", base_url),
453        userinfo_endpoint: format!("{}/oidc/userinfo", base_url),
454        jwks_uri: format!("{}/.well-known/jwks.json", base_url),
455        registration_endpoint: None,
456        scopes_supported: vec![
457            "openid".to_string(),
458            "profile".to_string(),
459            "email".to_string(),
460            "address".to_string(),
461            "phone".to_string(),
462            "offline_access".to_string(),
463        ],
464        response_types_supported: vec![
465            "code".to_string(),
466            "id_token".to_string(),
467            "id_token token".to_string(),
468            "code id_token".to_string(),
469            "code token".to_string(),
470            "code id_token token".to_string(),
471        ],
472        response_modes_supported: vec![
473            "query".to_string(),
474            "fragment".to_string(),
475            "form_post".to_string(),
476        ],
477        grant_types_supported: vec![
478            "authorization_code".to_string(),
479            "refresh_token".to_string(),
480            "urn:ietf:params:oauth:grant-type:token-exchange".to_string(),
481        ],
482        subject_types_supported: vec!["public".to_string()],
483        id_token_signing_alg_values_supported: vec!["RS256".to_string(), "HS256".to_string()],
484        token_endpoint_auth_methods_supported: vec![
485            "client_secret_basic".to_string(),
486            "client_secret_post".to_string(),
487            "none".to_string(),
488        ],
489        claims_supported: vec![
490            "sub".to_string(),
491            "iss".to_string(),
492            "aud".to_string(),
493            "exp".to_string(),
494            "iat".to_string(),
495            "name".to_string(),
496            "given_name".to_string(),
497            "family_name".to_string(),
498            "email".to_string(),
499            "email_verified".to_string(),
500            "picture".to_string(),
501            "phone_number".to_string(),
502            "phone_number_verified".to_string(),
503            "address".to_string(),
504            "updated_at".to_string(),
505        ],
506        code_challenge_methods_supported: vec!["S256".to_string(), "plain".to_string()],
507    };
508
509    Json(discovery)
510}
511
512/// JSON Web Key Set
513#[derive(Debug, Serialize)]
514pub struct JwkSet {
515    pub keys: Vec<Jwk>,
516}
517
518/// JSON Web Key
519#[derive(Debug, Serialize)]
520pub struct Jwk {
521    pub kty: String, // Key type (RSA, EC, etc.)
522    pub kid: String, // Key ID
523    pub alg: String, // Algorithm
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub n: Option<String>, // RSA modulus
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub e: Option<String>, // RSA exponent
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub crv: Option<String>, // EC curve
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub x: Option<String>, // EC x coordinate
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub y: Option<String>, // EC y coordinate
534    #[serde(rename = "use")]
535    pub use_: String, // Key use (sig, enc)
536}
537
538/// GET /.well-known/jwks.json
539/// JSON Web Key Set endpoint
540pub async fn jwks(State(_state): State<ApiState>) -> Json<JwkSet> {
541    // TODO: Integrate with actual key manager to get real public keys
542    // This is a placeholder with example RSA key structure
543    let jwks = JwkSet {
544        keys: vec![Jwk {
545            kty: "RSA".to_string(),
546            kid: "rsa-key-1".to_string(),
547            alg: "RS256".to_string(),
548            n: Some("placeholder_modulus_base64url_encoded".to_string()),
549            e: Some("AQAB".to_string()),
550            crv: None,
551            x: None,
552            y: None,
553            use_: "sig".to_string(),
554        }],
555    };
556
557    Json(jwks)
558}
559
560/// UserInfo response
561#[derive(Debug, Serialize)]
562pub struct UserInfoResponse {
563    pub sub: String,
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub name: Option<String>,
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub given_name: Option<String>,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub family_name: Option<String>,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub email: Option<String>,
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub email_verified: Option<bool>,
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub picture: Option<String>,
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub phone_number: Option<String>,
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub phone_number_verified: Option<bool>,
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub updated_at: Option<i64>,
582}
583
584/// GET /oidc/userinfo
585/// OIDC UserInfo endpoint
586pub async fn userinfo(
587    State(state): State<ApiState>,
588    headers: HeaderMap,
589) -> ApiResponse<UserInfoResponse> {
590    // Extract and validate access token from Authorization header
591    let token = match crate::api::extract_bearer_token(&headers) {
592        Some(t) => t,
593        None => {
594            return ApiResponse::error_typed("invalid_token", "Authorization header required");
595        }
596    };
597
598    // Validate the access token
599    let claims = match state
600        .auth_framework
601        .token_manager()
602        .validate_jwt_token(&token)
603    {
604        Ok(c) => c,
605        Err(_) => {
606            return ApiResponse::error_typed("invalid_token", "Access token is invalid");
607        }
608    };
609
610    // Get user profile
611    let user_profile = match state.auth_framework.get_user_profile(&claims.sub).await {
612        Ok(profile) => profile,
613        Err(e) => {
614            tracing::error!("Failed to get user profile: {:?}", e);
615            return ApiResponse::error_typed("server_error", "Failed to retrieve user information");
616        }
617    };
618
619    // Extract user data from profile
620    let username = user_profile
621        .username
622        .clone()
623        .unwrap_or_else(|| claims.sub.clone());
624    let email = user_profile.email.clone();
625
626    // Build UserInfo response with available data
627    let userinfo = UserInfoResponse {
628        sub: claims.sub.clone(),
629        name: Some(username.clone()),
630        given_name: None,  // TODO: Parse from full name if available
631        family_name: None, // TODO: Parse from full name if available
632        email,
633        email_verified: Some(true), // TODO: Get from user profile
634        picture: None,              // TODO: Get from user profile
635        phone_number: None,         // TODO: Get from user profile
636        phone_number_verified: None,
637        updated_at: Some(chrono::Utc::now().timestamp()),
638    };
639
640    tracing::info!("UserInfo requested for user: {}", claims.sub);
641    ApiResponse::success(userinfo)
642}