1use 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#[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#[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#[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#[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#[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
74pub async fn authorize(
77 State(_state): State<ApiState>,
78 Query(params): Query<AuthorizeRequest>,
79) -> impl IntoResponse {
80 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 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
132pub async fn token(
135 State(state): State<ApiState>,
136 _headers: HeaderMap,
137 Json(req): Json<TokenRequest>,
138) -> ApiResponse<TokenResponse> {
139 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 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 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 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, 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 let response = TokenResponse {
221 access_token: format!("client_access_token_{}", chrono::Utc::now().timestamp()),
222 token_type: "Bearer".to_string(),
223 expires_in: 7200, refresh_token: None, 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#[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 tracing::info!("Token revoked: {}", &req.token[..10]);
256 ApiResponse::<()>::ok_with_message("Token revoked successfully")
257}
258
259#[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 let response = IntrospectResponse {
301 active: true, 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
315pub async fn get_client_info(
318 State(_state): State<ApiState>,
319 axum::extract::Path(client_id): axum::extract::Path<String>,
320) -> ApiResponse<ClientInfo> {
321 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#[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
364pub async fn token_exchange(
367 State(state): State<ApiState>,
368 Json(req): Json<TokenExchangeRequest>,
369) -> ApiResponse<TokenResponse> {
370 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 if req.subject_token.is_empty() {
380 return ApiResponse::error_typed("invalid_request", "subject_token is required");
381 }
382
383 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 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#[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
443pub async fn oidc_discovery(State(_state): State<ApiState>) -> Json<OidcDiscoveryDocument> {
446 let base_url = "https://auth.example.com"; 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#[derive(Debug, Serialize)]
514pub struct JwkSet {
515 pub keys: Vec<Jwk>,
516}
517
518#[derive(Debug, Serialize)]
520pub struct Jwk {
521 pub kty: String, pub kid: String, pub alg: String, #[serde(skip_serializing_if = "Option::is_none")]
525 pub n: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
527 pub e: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
529 pub crv: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
531 pub x: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
533 pub y: Option<String>, #[serde(rename = "use")]
535 pub use_: String, }
537
538pub async fn jwks(State(_state): State<ApiState>) -> Json<JwkSet> {
541 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#[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
584pub async fn userinfo(
587 State(state): State<ApiState>,
588 headers: HeaderMap,
589) -> ApiResponse<UserInfoResponse> {
590 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 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 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 let username = user_profile
621 .username
622 .clone()
623 .unwrap_or_else(|| claims.sub.clone());
624 let email = user_profile.email.clone();
625
626 let userinfo = UserInfoResponse {
628 sub: claims.sub.clone(),
629 name: Some(username.clone()),
630 given_name: None, family_name: None, email,
633 email_verified: Some(true), picture: None, phone_number: None, 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}