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