this repo has no description
1use axum::http::HeaderMap;
2use axum::Json;
3use chrono::{Duration, Utc};
4use crate::config::AuthConfig;
5use crate::state::AppState;
6use crate::oauth::{
7 ClientAuth, OAuthError, RefreshToken, TokenData, TokenId,
8 client::{ClientMetadataCache, verify_client_auth},
9 db,
10 dpop::DPoPVerifier,
11};
12use super::types::{TokenRequest, TokenResponse};
13use super::helpers::{create_access_token, verify_pkce};
14
15const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 3600;
16const REFRESH_TOKEN_EXPIRY_DAYS: i64 = 60;
17
18pub async fn handle_authorization_code_grant(
19 state: AppState,
20 _headers: HeaderMap,
21 request: TokenRequest,
22 dpop_proof: Option<String>,
23) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
24 let code = request
25 .code
26 .ok_or_else(|| OAuthError::InvalidRequest("code is required".to_string()))?;
27 let code_verifier = request
28 .code_verifier
29 .ok_or_else(|| OAuthError::InvalidRequest("code_verifier is required".to_string()))?;
30 let auth_request = db::consume_authorization_request_by_code(&state.db, &code)
31 .await?
32 .ok_or_else(|| OAuthError::InvalidGrant("Invalid or expired code".to_string()))?;
33 if auth_request.expires_at < Utc::now() {
34 return Err(OAuthError::InvalidGrant("Authorization code has expired".to_string()));
35 }
36 if let Some(request_client_id) = &request.client_id {
37 if request_client_id != &auth_request.client_id {
38 return Err(OAuthError::InvalidGrant("client_id mismatch".to_string()));
39 }
40 }
41 let did = auth_request
42 .did
43 .ok_or_else(|| OAuthError::InvalidGrant("Authorization not completed".to_string()))?;
44 let client_metadata_cache = ClientMetadataCache::new(3600);
45 let client_metadata = client_metadata_cache
46 .get(&auth_request.client_id)
47 .await?;
48 let client_auth = auth_request.client_auth.clone().unwrap_or(ClientAuth::None);
49 verify_client_auth(&client_metadata_cache, &client_metadata, &client_auth).await?;
50 verify_pkce(&auth_request.parameters.code_challenge, &code_verifier)?;
51 if let Some(redirect_uri) = &request.redirect_uri {
52 if redirect_uri != &auth_request.parameters.redirect_uri {
53 return Err(OAuthError::InvalidGrant("redirect_uri mismatch".to_string()));
54 }
55 }
56 let dpop_jkt = if let Some(proof) = &dpop_proof {
57 let config = AuthConfig::get();
58 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
59 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
60 let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
61 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?;
62 if !db::check_and_record_dpop_jti(&state.db, &result.jti).await? {
63 return Err(OAuthError::InvalidDpopProof(
64 "DPoP proof has already been used".to_string(),
65 ));
66 }
67 if let Some(expected_jkt) = &auth_request.parameters.dpop_jkt {
68 if &result.jkt != expected_jkt {
69 return Err(OAuthError::InvalidDpopProof(
70 "DPoP key binding mismatch".to_string(),
71 ));
72 }
73 }
74 Some(result.jkt)
75 } else if auth_request.parameters.dpop_jkt.is_some() {
76 return Err(OAuthError::InvalidRequest(
77 "DPoP proof required for this authorization".to_string(),
78 ));
79 } else {
80 None
81 };
82 let token_id = TokenId::generate();
83 let refresh_token = RefreshToken::generate();
84 let now = Utc::now();
85 let access_token = create_access_token(&token_id.0, &did, dpop_jkt.as_deref())?;
86 let token_data = TokenData {
87 did: did.clone(),
88 token_id: token_id.0.clone(),
89 created_at: now,
90 updated_at: now,
91 expires_at: now + Duration::days(REFRESH_TOKEN_EXPIRY_DAYS),
92 client_id: auth_request.client_id.clone(),
93 client_auth: auth_request.client_auth.unwrap_or(ClientAuth::None),
94 device_id: auth_request.device_id,
95 parameters: auth_request.parameters.clone(),
96 details: None,
97 code: None,
98 current_refresh_token: Some(refresh_token.0.clone()),
99 scope: auth_request.parameters.scope.clone(),
100 };
101 db::create_token(&state.db, &token_data).await?;
102 tokio::spawn({
103 let pool = state.db.clone();
104 let did_clone = did.clone();
105 async move {
106 if let Err(e) = db::enforce_token_limit_for_user(&pool, &did_clone).await {
107 tracing::warn!("Failed to enforce token limit for user: {:?}", e);
108 }
109 }
110 });
111 let mut response_headers = HeaderMap::new();
112 let config = AuthConfig::get();
113 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
114 response_headers.insert(
115 "DPoP-Nonce",
116 verifier.generate_nonce().parse().unwrap(),
117 );
118 Ok((
119 response_headers,
120 Json(TokenResponse {
121 access_token,
122 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
123 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
124 refresh_token: Some(refresh_token.0),
125 scope: auth_request.parameters.scope,
126 sub: Some(did),
127 }),
128 ))
129}
130
131pub async fn handle_refresh_token_grant(
132 state: AppState,
133 _headers: HeaderMap,
134 request: TokenRequest,
135 dpop_proof: Option<String>,
136) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
137 let refresh_token_str = request
138 .refresh_token
139 .ok_or_else(|| OAuthError::InvalidRequest("refresh_token is required".to_string()))?;
140 if let Some(token_id) = db::check_refresh_token_used(&state.db, &refresh_token_str).await? {
141 db::delete_token_family(&state.db, token_id).await?;
142 return Err(OAuthError::InvalidGrant(
143 "Refresh token reuse detected, token family revoked".to_string(),
144 ));
145 }
146 let (db_id, token_data) = db::get_token_by_refresh_token(&state.db, &refresh_token_str)
147 .await?
148 .ok_or_else(|| OAuthError::InvalidGrant("Invalid refresh token".to_string()))?;
149 if token_data.expires_at < Utc::now() {
150 db::delete_token_family(&state.db, db_id).await?;
151 return Err(OAuthError::InvalidGrant("Refresh token has expired".to_string()));
152 }
153 let dpop_jkt = if let Some(proof) = &dpop_proof {
154 let config = AuthConfig::get();
155 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
156 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
157 let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
158 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?;
159 if !db::check_and_record_dpop_jti(&state.db, &result.jti).await? {
160 return Err(OAuthError::InvalidDpopProof(
161 "DPoP proof has already been used".to_string(),
162 ));
163 }
164 if let Some(expected_jkt) = &token_data.parameters.dpop_jkt {
165 if &result.jkt != expected_jkt {
166 return Err(OAuthError::InvalidDpopProof(
167 "DPoP key binding mismatch".to_string(),
168 ));
169 }
170 }
171 Some(result.jkt)
172 } else if token_data.parameters.dpop_jkt.is_some() {
173 return Err(OAuthError::InvalidRequest(
174 "DPoP proof required".to_string(),
175 ));
176 } else {
177 None
178 };
179 let new_token_id = TokenId::generate();
180 let new_refresh_token = RefreshToken::generate();
181 let new_expires_at = Utc::now() + Duration::days(REFRESH_TOKEN_EXPIRY_DAYS);
182 db::rotate_token(
183 &state.db,
184 db_id,
185 &new_token_id.0,
186 &new_refresh_token.0,
187 new_expires_at,
188 )
189 .await?;
190 let access_token = create_access_token(&new_token_id.0, &token_data.did, dpop_jkt.as_deref())?;
191 let mut response_headers = HeaderMap::new();
192 let config = AuthConfig::get();
193 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
194 response_headers.insert(
195 "DPoP-Nonce",
196 verifier.generate_nonce().parse().unwrap(),
197 );
198 Ok((
199 response_headers,
200 Json(TokenResponse {
201 access_token,
202 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
203 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
204 refresh_token: Some(new_refresh_token.0),
205 scope: token_data.scope,
206 sub: Some(token_data.did),
207 }),
208 ))
209}