this repo has no description
1use axum::{Form, Json};
2use axum::extract::State;
3use axum::http::{HeaderMap, StatusCode};
4use chrono::Utc;
5use serde::{Deserialize, Serialize};
6use crate::state::{AppState, RateLimitKind};
7use crate::oauth::{OAuthError, db};
8use super::helpers::extract_token_claims;
9
10#[derive(Debug, Deserialize)]
11pub struct RevokeRequest {
12 pub token: Option<String>,
13 #[serde(default)]
14 pub token_type_hint: Option<String>,
15}
16
17pub async fn revoke_token(
18 State(state): State<AppState>,
19 headers: HeaderMap,
20 Form(request): Form<RevokeRequest>,
21) -> Result<StatusCode, OAuthError> {
22 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
23 if !state.check_rate_limit(RateLimitKind::OAuthIntrospect, &client_ip).await {
24 tracing::warn!(ip = %client_ip, "OAuth revoke rate limit exceeded");
25 return Err(OAuthError::RateLimited);
26 }
27 if let Some(token) = &request.token {
28 if let Some((db_id, _)) = db::get_token_by_refresh_token(&state.db, token).await? {
29 db::delete_token_family(&state.db, db_id).await?;
30 } else {
31 db::delete_token(&state.db, token).await?;
32 }
33 }
34 Ok(StatusCode::OK)
35}
36
37#[derive(Debug, Deserialize)]
38pub struct IntrospectRequest {
39 pub token: String,
40 #[serde(default)]
41 pub token_type_hint: Option<String>,
42}
43
44#[derive(Debug, Serialize)]
45pub struct IntrospectResponse {
46 pub active: bool,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub scope: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub client_id: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub username: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub token_type: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub exp: Option<i64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub iat: Option<i64>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub nbf: Option<i64>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub sub: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub aud: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub iss: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub jti: Option<String>,
69}
70
71pub async fn introspect_token(
72 State(state): State<AppState>,
73 headers: HeaderMap,
74 Form(request): Form<IntrospectRequest>,
75) -> Result<Json<IntrospectResponse>, OAuthError> {
76 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
77 if !state.check_rate_limit(RateLimitKind::OAuthIntrospect, &client_ip).await {
78 tracing::warn!(ip = %client_ip, "OAuth introspect rate limit exceeded");
79 return Err(OAuthError::RateLimited);
80 }
81 let inactive_response = IntrospectResponse {
82 active: false,
83 scope: None,
84 client_id: None,
85 username: None,
86 token_type: None,
87 exp: None,
88 iat: None,
89 nbf: None,
90 sub: None,
91 aud: None,
92 iss: None,
93 jti: None,
94 };
95 let token_info = match extract_token_claims(&request.token) {
96 Ok(info) => info,
97 Err(_) => return Ok(Json(inactive_response)),
98 };
99 let token_data = match db::get_token_by_id(&state.db, &token_info.jti).await {
100 Ok(Some(data)) => data,
101 _ => return Ok(Json(inactive_response)),
102 };
103 if token_data.expires_at < Utc::now() {
104 return Ok(Json(inactive_response));
105 }
106 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
107 let issuer = format!("https://{}", pds_hostname);
108 Ok(Json(IntrospectResponse {
109 active: true,
110 scope: token_data.scope,
111 client_id: Some(token_data.client_id),
112 username: None,
113 token_type: if token_data.parameters.dpop_jkt.is_some() {
114 Some("DPoP".to_string())
115 } else {
116 Some("Bearer".to_string())
117 },
118 exp: Some(token_info.exp),
119 iat: Some(token_info.iat),
120 nbf: Some(token_info.iat),
121 sub: Some(token_data.did),
122 aud: Some(issuer.clone()),
123 iss: Some(issuer),
124 jti: Some(token_info.jti),
125 }))
126}