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