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