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