1use crate::api::{ApiResponse, ApiState, extract_bearer_token, validate_api_token};
7use crate::oauth2_server::AuthorizationRequest;
8pub 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
24fn redirect_uri_matches(candidate: &str, registered: &str) -> bool {
29 match (Url::parse(candidate), Url::parse(registered)) {
30 (Ok(a), Ok(b)) => {
31 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 _ => candidate == registered,
45 }
46}
47
48#[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 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 pub fn description(mut self, desc: impl Into<String>) -> Self {
87 self.error_description = Some(desc.into());
88 self
89 }
90
91 pub fn state(mut self, state: impl Into<String>) -> Self {
93 self.state = Some(state.into());
94 self
95 }
96
97 pub fn maybe_state(mut self, state: Option<String>) -> Self {
99 self.state = state;
100 self
101 }
102
103 pub fn error_uri(mut self, uri: impl Into<String>) -> Self {
105 self.error_uri = Some(uri.into());
106 self
107 }
108}
109
110#[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#[derive(Debug, Deserialize)]
148pub struct RevokeRequest {
149 pub token: String,
150 #[serde(default)]
151 pub token_type_hint: Option<String>, }
153
154#[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
185pub 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 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 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(¶ms.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 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 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 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
390pub 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 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 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 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 if code_data["client_id"].as_str() != Some(&client_id) {
488 return ApiResponse::error_typed("invalid_grant", "client_id mismatch");
489 }
490
491 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 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 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 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 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 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 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 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 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 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 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 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 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
745pub async fn revoke(
760 State(state): State<ApiState>,
761 Json(req): Json<RevokeRequest>,
762) -> ApiResponse<serde_json::Value> {
763 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 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
826pub async fn userinfo(
839 State(state): State<ApiState>,
840 headers: HeaderMap,
841) -> ApiResponse<UserInfoResponse> {
842 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 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 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 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 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
899pub 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
955pub 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
1009pub async fn jwks(State(_state): State<ApiState>) -> impl IntoResponse {
1026 (StatusCode::OK, Json(serde_json::json!({ "keys": [] })))
1027}
1028
1029#[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
1051pub async fn end_session(
1063 State(state): State<ApiState>,
1064 Query(params): Query<EndSessionRequest>,
1065) -> impl IntoResponse {
1066 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 if let Some(ref redirect_uri) = params.post_logout_redirect_uri {
1094 let allowed = if let Some(ref token) = params.id_token_hint {
1096 let client_id = state
1098 .auth_framework
1099 .token_manager()
1100 .validate_jwt_token(token)
1101 .ok()
1102 .and_then(|claims| {
1103 if let Some(ref cid) = claims.client_id {
1105 if !cid.is_empty() {
1106 return Some(cid.clone());
1107 }
1108 }
1109 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 (
1157 StatusCode::OK,
1158 Json(serde_json::json!({ "status": "logged_out" })),
1159 )
1160 .into_response()
1161}
1162
1163#[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
1193pub async fn register_client(
1213 State(state): State<ApiState>,
1214 headers: HeaderMap,
1215 Json(req): Json<ClientRegistrationRequest>,
1216) -> impl IntoResponse {
1217 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 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 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 for uri in &req.redirect_uris {
1268 match Url::parse(uri) {
1269 Ok(parsed) => {
1270 match parsed.scheme() {
1271 "https" => {}
1272 "http" => {
1273 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
1371pub async fn users_me(state: State<ApiState>, headers: HeaderMap) -> impl IntoResponse {
1385 userinfo(state, headers).await
1387}