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