1use base64ct::{Base64UrlUnpadded, Encoding};
2use chrono::{DateTime, Utc};
3use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
4use rand::TryRngCore;
5use rand::rngs::OsRng;
6use serde::Serialize;
7use sha2::{Digest, Sha256};
8
9use crate::applications::Application;
10#[cfg(test)]
11use crate::applications::CreateApplicationParams;
12use crate::authorization::hash_authorization_code;
13use crate::db::Db;
14use crate::error::AuthError;
15use crate::signing_keys::SigningKey;
16#[cfg(test)]
17use crate::types::ClientType;
18use crate::types::{ApplicationId, AuthorizationCodeId, RefreshTokenId, TokenHash, UserId};
19
20#[derive(Debug)]
30pub enum TokenError {
31 InvalidRequest(String),
32 InvalidClient(String),
33 InvalidGrant(String),
34 UnsupportedGrantType,
35 ServerError(String),
36}
37
38#[derive(Debug, Serialize)]
44pub struct TokenResponse {
45 pub access_token: String,
46 pub token_type: &'static str,
47 pub expires_in: i64,
48 pub refresh_token: String,
49 pub id_token: String,
50}
51
52#[derive(Debug, Clone, sqlx::FromRow)]
57pub struct RefreshToken {
58 pub id: RefreshTokenId,
59 pub application_id: ApplicationId,
60 pub user_id: UserId,
61 pub token_hash: TokenHash,
62 pub scopes: String,
63 pub authorization_code_id: Option<AuthorizationCodeId>,
64 pub expires_at: DateTime<Utc>,
65 pub revoked_at: Option<DateTime<Utc>>,
66 pub created_at: DateTime<Utc>,
67}
68
69#[derive(Debug, Serialize)]
74struct AccessTokenJwtClaims {
75 sub: String,
76 iss: String,
77 aud: String,
78 exp: i64,
79 iat: i64,
80 scope: String,
81 email: String,
82 email_verified: bool,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 username: Option<String>,
85 roles: Vec<String>,
86 permissions: Vec<String>,
87}
88
89#[derive(Debug, Serialize)]
90struct IdTokenJwtClaims {
91 sub: String,
92 iss: String,
93 aud: String,
94 exp: i64,
95 iat: i64,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 nonce: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 at_hash: Option<String>,
100 auth_time: i64,
101}
102
103pub fn verify_pkce_s256(code_verifier: &str, code_challenge: &str) -> bool {
111 let digest = Sha256::digest(code_verifier.as_bytes());
112 let computed = Base64UrlUnpadded::encode_string(&digest);
113 computed == code_challenge
114}
115
116pub fn compute_at_hash(access_token_jwt: &str) -> String {
124 let digest = Sha256::digest(access_token_jwt.as_bytes());
125 Base64UrlUnpadded::encode_string(&digest[..16])
126}
127
128#[allow(clippy::too_many_arguments)]
138pub fn mint_access_token(
139 sub: UserId,
140 issuer: &str,
141 audience: &str,
142 scope: &str,
143 kid: &str,
144 private_key_pem: &str,
145 ttl_secs: i64,
146 email: &str,
147 email_verified: bool,
148 username: Option<&str>,
149 roles: &[String],
150 permissions: &[String],
151) -> Result<String, AuthError> {
152 let now = Utc::now().timestamp();
153 let claims = AccessTokenJwtClaims {
154 sub: sub.to_string(),
155 iss: issuer.to_owned(),
156 aud: audience.to_owned(),
157 exp: now + ttl_secs,
158 iat: now,
159 scope: scope.to_owned(),
160 email: email.to_owned(),
161 email_verified,
162 username: username.map(|u| u.to_owned()),
163 roles: roles.to_vec(),
164 permissions: permissions.to_vec(),
165 };
166 let mut header = Header::new(Algorithm::RS256);
167 header.kid = Some(kid.to_owned());
168 header.typ = Some("at+jwt".into());
169
170 let key = EncodingKey::from_rsa_pem(private_key_pem.as_bytes())
171 .map_err(|e| AuthError::SigningKey(e.to_string()))?;
172 encode(&header, &claims, &key).map_err(|e| AuthError::SigningKey(e.to_string()))
173}
174
175#[allow(clippy::too_many_arguments)]
180pub fn mint_id_token(
181 sub: UserId,
182 issuer: &str,
183 audience: &str,
184 nonce: Option<&str>,
185 at_hash: &str,
186 auth_time: i64,
187 kid: &str,
188 private_key_pem: &str,
189 ttl_secs: i64,
190) -> Result<String, AuthError> {
191 let now = Utc::now().timestamp();
192 let claims = IdTokenJwtClaims {
193 sub: sub.to_string(),
194 iss: issuer.to_owned(),
195 aud: audience.to_owned(),
196 exp: now + ttl_secs,
197 iat: now,
198 nonce: nonce.map(|s| s.to_owned()),
199 at_hash: Some(at_hash.to_owned()),
200 auth_time,
201 };
202 let mut header = Header::new(Algorithm::RS256);
203 header.kid = Some(kid.to_owned());
204 header.typ = Some("JWT".into());
205
206 let key = EncodingKey::from_rsa_pem(private_key_pem.as_bytes())
207 .map_err(|e| AuthError::SigningKey(e.to_string()))?;
208 encode(&header, &claims, &key).map_err(|e| AuthError::SigningKey(e.to_string()))
209}
210
211pub fn generate_refresh_token() -> String {
217 let mut bytes = [0u8; 32];
218 OsRng
219 .try_fill_bytes(&mut bytes)
220 .expect("OS RNG unavailable");
221 Base64UrlUnpadded::encode_string(&bytes)
222}
223
224pub fn hash_refresh_token(raw: &str) -> TokenHash {
226 let digest = Sha256::digest(raw.as_bytes());
227 TokenHash::new_unchecked(format!("{digest:x}"))
228}
229
230impl Db {
235 pub async fn create_refresh_token(
237 &self,
238 application_id: ApplicationId,
239 user_id: UserId,
240 token_hash: &TokenHash,
241 scopes: &[String],
242 authorization_code_id: Option<AuthorizationCodeId>,
243 ) -> Result<RefreshToken, AuthError> {
244 let id = RefreshTokenId::new();
245 let scopes_json = serde_json::to_string(scopes).expect("Vec<String> serializes to JSON");
246 let now = Utc::now();
247 let now_str = now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
248 let expires_at = now + chrono::Duration::days(30);
249 let expires_str = expires_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
250
251 sqlx::query(
252 "INSERT INTO allowthem_refresh_tokens \
253 (id, application_id, user_id, token_hash, scopes, \
254 authorization_code_id, expires_at, created_at) \
255 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
256 )
257 .bind(id)
258 .bind(application_id)
259 .bind(user_id)
260 .bind(token_hash)
261 .bind(&scopes_json)
262 .bind(authorization_code_id)
263 .bind(&expires_str)
264 .bind(&now_str)
265 .execute(self.pool())
266 .await?;
267
268 sqlx::query_as::<_, RefreshToken>(
269 "SELECT id, application_id, user_id, token_hash, scopes, \
270 authorization_code_id, expires_at, revoked_at, created_at \
271 FROM allowthem_refresh_tokens WHERE id = ?",
272 )
273 .bind(id)
274 .fetch_one(self.pool())
275 .await
276 .map_err(AuthError::Database)
277 }
278
279 pub async fn revoke_refresh_tokens_by_auth_code(
283 &self,
284 authorization_code_id: AuthorizationCodeId,
285 ) -> Result<u64, AuthError> {
286 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
287 let result = sqlx::query(
288 "UPDATE allowthem_refresh_tokens \
289 SET revoked_at = ? \
290 WHERE authorization_code_id = ? AND revoked_at IS NULL",
291 )
292 .bind(&now)
293 .bind(authorization_code_id)
294 .execute(self.pool())
295 .await?;
296
297 Ok(result.rows_affected())
298 }
299
300 pub async fn get_refresh_token_by_hash(
305 &self,
306 token_hash: &TokenHash,
307 ) -> Result<Option<RefreshToken>, AuthError> {
308 sqlx::query_as::<_, RefreshToken>(
309 "SELECT id, application_id, user_id, token_hash, scopes, \
310 authorization_code_id, expires_at, revoked_at, created_at \
311 FROM allowthem_refresh_tokens WHERE token_hash = ?",
312 )
313 .bind(token_hash)
314 .fetch_optional(self.pool())
315 .await
316 .map_err(AuthError::Database)
317 }
318
319 pub async fn revoke_refresh_token(&self, id: RefreshTokenId) -> Result<(), AuthError> {
324 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
325 sqlx::query("UPDATE allowthem_refresh_tokens SET revoked_at = ? WHERE id = ?")
326 .bind(&now)
327 .bind(id)
328 .execute(self.pool())
329 .await?;
330 Ok(())
331 }
332}
333
334#[allow(clippy::too_many_arguments)]
350pub async fn exchange_authorization_code(
351 db: &Db,
352 code: &str,
353 redirect_uri: &str,
354 code_verifier: &str,
355 application: &Application,
356 issuer: &str,
357 signing_key: &SigningKey,
358 private_key_pem: &str,
359) -> Result<TokenResponse, TokenError> {
360 let code_hash = hash_authorization_code(code);
362 let auth_code = db
363 .get_authorization_code_by_hash(&code_hash)
364 .await
365 .map_err(|e| TokenError::ServerError(e.to_string()))?
366 .ok_or_else(|| TokenError::InvalidGrant("invalid authorization code".into()))?;
367
368 if auth_code.used_at.is_some() {
370 let _ = db.revoke_refresh_tokens_by_auth_code(auth_code.id).await;
371 return Err(TokenError::InvalidGrant(
372 "authorization code already used".into(),
373 ));
374 }
375
376 db.mark_authorization_code_used(auth_code.id)
378 .await
379 .map_err(|e| TokenError::ServerError(e.to_string()))?;
380
381 if auth_code.expires_at < Utc::now() {
383 return Err(TokenError::InvalidGrant(
384 "authorization code expired".into(),
385 ));
386 }
387
388 if auth_code.application_id != application.id {
390 return Err(TokenError::InvalidGrant(
391 "code was issued to a different client".into(),
392 ));
393 }
394
395 if auth_code.redirect_uri != redirect_uri {
397 return Err(TokenError::InvalidGrant("redirect_uri mismatch".into()));
398 }
399
400 if !verify_pkce_s256(code_verifier, &auth_code.code_challenge) {
402 return Err(TokenError::InvalidGrant("PKCE verification failed".into()));
403 }
404
405 let scopes: Vec<String> = serde_json::from_str(&auth_code.scopes)
407 .map_err(|e| TokenError::ServerError(e.to_string()))?;
408 let scopes_str = scopes.join(" ");
409
410 let user = db
412 .get_user(auth_code.user_id)
413 .await
414 .map_err(|e| TokenError::ServerError(e.to_string()))?;
415 let user_roles = db
416 .get_user_roles(&auth_code.user_id)
417 .await
418 .map_err(|e| TokenError::ServerError(e.to_string()))?;
419 let user_perms = db
420 .get_user_permissions(&auth_code.user_id)
421 .await
422 .map_err(|e| TokenError::ServerError(e.to_string()))?;
423 let role_names: Vec<String> = user_roles
424 .iter()
425 .map(|r| r.name.as_str().to_owned())
426 .collect();
427 let perm_names: Vec<String> = user_perms
428 .iter()
429 .map(|p| p.name.as_str().to_owned())
430 .collect();
431
432 let kid = signing_key.id.to_string();
434 let access_token = mint_access_token(
435 auth_code.user_id,
436 issuer,
437 application.client_id.as_str(),
438 &scopes_str,
439 &kid,
440 private_key_pem,
441 3600,
442 user.email.as_str(),
443 user.email_verified,
444 user.username.as_ref().map(|u| u.as_str()),
445 &role_names,
446 &perm_names,
447 )
448 .map_err(|e| TokenError::ServerError(e.to_string()))?;
449
450 let at_hash = compute_at_hash(&access_token);
452 let auth_time = auth_code.created_at.timestamp();
453
454 let id_token = mint_id_token(
456 auth_code.user_id,
457 issuer,
458 application.client_id.as_str(),
459 auth_code.nonce.as_deref(),
460 &at_hash,
461 auth_time,
462 &kid,
463 private_key_pem,
464 3600,
465 )
466 .map_err(|e| TokenError::ServerError(e.to_string()))?;
467
468 let raw_refresh = generate_refresh_token();
470 let refresh_hash = hash_refresh_token(&raw_refresh);
471 db.create_refresh_token(
472 application.id,
473 auth_code.user_id,
474 &refresh_hash,
475 &scopes,
476 Some(auth_code.id),
477 )
478 .await
479 .map_err(|e| TokenError::ServerError(e.to_string()))?;
480
481 Ok(TokenResponse {
482 access_token,
483 token_type: "Bearer",
484 expires_in: 3600,
485 refresh_token: raw_refresh,
486 id_token,
487 })
488}
489
490pub async fn exchange_refresh_token(
500 db: &Db,
501 raw_token: &str,
502 requested_scopes: Option<&str>,
503 application: &Application,
504 issuer: &str,
505 signing_key: &SigningKey,
506 private_key_pem: &str,
507) -> Result<TokenResponse, TokenError> {
508 let hash = hash_refresh_token(raw_token);
510 let stored = db
511 .get_refresh_token_by_hash(&hash)
512 .await
513 .map_err(|e| TokenError::ServerError(e.to_string()))?
514 .ok_or_else(|| TokenError::InvalidGrant("invalid refresh token".into()))?;
515
516 if stored.revoked_at.is_some() {
518 return Err(TokenError::InvalidGrant(
519 "refresh token has been revoked".into(),
520 ));
521 }
522
523 if stored.expires_at < Utc::now() {
525 return Err(TokenError::InvalidGrant("refresh token has expired".into()));
526 }
527
528 if stored.application_id != application.id {
530 return Err(TokenError::InvalidGrant(
531 "refresh token was issued to a different client".into(),
532 ));
533 }
534
535 let original_scopes: Vec<String> =
537 serde_json::from_str(&stored.scopes).map_err(|e| TokenError::ServerError(e.to_string()))?;
538
539 let effective_scopes = match requested_scopes {
540 Some(s) if !s.is_empty() => {
541 let requested: Vec<&str> = s.split_whitespace().collect();
542 for scope in &requested {
543 if !original_scopes.iter().any(|orig| orig == scope) {
544 return Err(TokenError::InvalidGrant(
545 "requested scope exceeds original grant".into(),
546 ));
547 }
548 }
549 requested.iter().map(|s| s.to_string()).collect::<Vec<_>>()
550 }
551 _ => original_scopes.clone(),
552 };
553
554 let scopes_str = effective_scopes.join(" ");
555
556 db.revoke_refresh_token(stored.id)
558 .await
559 .map_err(|e| TokenError::ServerError(e.to_string()))?;
560
561 let user = db
563 .get_user(stored.user_id)
564 .await
565 .map_err(|e| TokenError::ServerError(e.to_string()))?;
566 let user_roles = db
567 .get_user_roles(&stored.user_id)
568 .await
569 .map_err(|e| TokenError::ServerError(e.to_string()))?;
570 let user_perms = db
571 .get_user_permissions(&stored.user_id)
572 .await
573 .map_err(|e| TokenError::ServerError(e.to_string()))?;
574 let role_names: Vec<String> = user_roles
575 .iter()
576 .map(|r| r.name.as_str().to_owned())
577 .collect();
578 let perm_names: Vec<String> = user_perms
579 .iter()
580 .map(|p| p.name.as_str().to_owned())
581 .collect();
582
583 let kid = signing_key.id.to_string();
585 let access_token = mint_access_token(
586 stored.user_id,
587 issuer,
588 application.client_id.as_str(),
589 &scopes_str,
590 &kid,
591 private_key_pem,
592 3600,
593 user.email.as_str(),
594 user.email_verified,
595 user.username.as_ref().map(|u| u.as_str()),
596 &role_names,
597 &perm_names,
598 )
599 .map_err(|e| TokenError::ServerError(e.to_string()))?;
600
601 let at_hash = compute_at_hash(&access_token);
603 let auth_time = stored.created_at.timestamp();
604 let id_token = mint_id_token(
605 stored.user_id,
606 issuer,
607 application.client_id.as_str(),
608 None,
609 &at_hash,
610 auth_time,
611 &kid,
612 private_key_pem,
613 3600,
614 )
615 .map_err(|e| TokenError::ServerError(e.to_string()))?;
616
617 let new_raw = generate_refresh_token();
619 let new_hash = hash_refresh_token(&new_raw);
620 db.create_refresh_token(
621 application.id,
622 stored.user_id,
623 &new_hash,
624 &effective_scopes,
625 stored.authorization_code_id,
626 )
627 .await
628 .map_err(|e| TokenError::ServerError(e.to_string()))?;
629
630 Ok(TokenResponse {
631 access_token,
632 token_type: "Bearer",
633 expires_in: 3600,
634 refresh_token: new_raw,
635 id_token,
636 })
637}
638
639#[cfg(test)]
644mod tests {
645 use super::*;
646 use crate::signing_keys::decrypt_private_key;
647 use crate::types::Email;
648 use jsonwebtoken::Algorithm;
649 use sqlx::SqlitePool;
650 use sqlx::sqlite::SqliteConnectOptions;
651 use std::str::FromStr;
652
653 const ENC_KEY: [u8; 32] = [0x42; 32];
654 const ISSUER: &str = "https://auth.example.com";
655
656 async fn test_db() -> Db {
657 let opts = SqliteConnectOptions::from_str("sqlite::memory:")
658 .unwrap()
659 .pragma("foreign_keys", "ON");
660 let pool = SqlitePool::connect_with(opts).await.unwrap();
661 Db::new(pool).await.unwrap()
662 }
663
664 #[test]
667 fn verify_pkce_s256_valid() {
668 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
669 let digest = Sha256::digest(verifier.as_bytes());
670 let challenge = Base64UrlUnpadded::encode_string(&digest);
671 assert!(verify_pkce_s256(verifier, &challenge));
672 }
673
674 #[test]
675 fn verify_pkce_s256_wrong_verifier() {
676 let verifier = "correct_verifier";
677 let digest = Sha256::digest(verifier.as_bytes());
678 let challenge = Base64UrlUnpadded::encode_string(&digest);
679 assert!(!verify_pkce_s256("wrong_verifier", &challenge));
680 }
681
682 #[test]
683 fn verify_pkce_s256_empty_verifier() {
684 let verifier = "actual_verifier";
685 let digest = Sha256::digest(verifier.as_bytes());
686 let challenge = Base64UrlUnpadded::encode_string(&digest);
687 assert!(!verify_pkce_s256("", &challenge));
688 }
689
690 #[test]
693 fn compute_at_hash_deterministic() {
694 let input = "eyJhbGciOiJSUzI1NiJ9.test.sig";
695 let h1 = compute_at_hash(input);
696 let h2 = compute_at_hash(input);
697 assert_eq!(h1, h2);
698 }
699
700 #[test]
701 fn compute_at_hash_known_value() {
702 let hash = compute_at_hash("test");
706 let digest = Sha256::digest(b"test");
707 let expected = Base64UrlUnpadded::encode_string(&digest[..16]);
708 assert_eq!(hash, expected);
709 }
710
711 #[test]
714 fn refresh_token_is_43_chars() {
715 let token = generate_refresh_token();
716 assert_eq!(token.len(), 43, "32 bytes base64url = 43 chars");
717 }
718
719 #[test]
720 fn refresh_token_is_url_safe() {
721 let token = generate_refresh_token();
722 assert!(
723 token
724 .chars()
725 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
726 "token must be URL-safe: got {token}"
727 );
728 }
729
730 #[test]
731 fn hash_refresh_token_deterministic() {
732 let token = generate_refresh_token();
733 let h1 = hash_refresh_token(&token);
734 let h2 = hash_refresh_token(&token);
735 assert_eq!(format!("{h1:?}"), format!("{h2:?}"));
736 }
737
738 #[tokio::test]
741 async fn mint_access_token_roundtrip() {
742 let db = test_db().await;
743 let key = db.create_signing_key(&ENC_KEY).await.unwrap();
744 db.activate_signing_key(key.id).await.unwrap();
745 let pem = decrypt_private_key(&key, &ENC_KEY).unwrap();
746 let kid = key.id.to_string();
747
748 let user_id = UserId::new();
749 let token = mint_access_token(
750 user_id,
751 ISSUER,
752 "ath_test_client",
753 "openid profile",
754 &kid,
755 &pem,
756 3600,
757 "test@example.com",
758 true,
759 Some("testuser"),
760 &["admin".to_string()],
761 &["posts:write".to_string()],
762 )
763 .unwrap();
764
765 let claims = db.validate_access_token(&token, ISSUER).await.unwrap();
767 assert_eq!(claims.sub, user_id);
768 assert_eq!(claims.email, "test@example.com");
769 assert!(claims.email_verified);
770 assert_eq!(claims.username.as_deref(), Some("testuser"));
771 assert_eq!(claims.roles, vec!["admin"]);
772 assert_eq!(claims.permissions, vec!["posts:write"]);
773 assert_eq!(claims.scope, "openid profile");
774 assert_eq!(claims.iss, ISSUER);
775 }
776
777 #[tokio::test]
778 async fn mint_id_token_contains_nonce() {
779 let db = test_db().await;
780 let key = db.create_signing_key(&ENC_KEY).await.unwrap();
781 let pem = decrypt_private_key(&key, &ENC_KEY).unwrap();
782 let kid = key.id.to_string();
783
784 let user_id = UserId::new();
785 let token = mint_id_token(
786 user_id,
787 ISSUER,
788 "ath_test_client",
789 Some("test_nonce_123"),
790 "test_at_hash",
791 1234567890,
792 &kid,
793 &pem,
794 3600,
795 )
796 .unwrap();
797
798 let parts: Vec<&str> = token.splitn(3, '.').collect();
800 let payload = base64ct::Base64UrlUnpadded::decode_vec(parts[1]).unwrap();
801 let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
802 assert_eq!(claims["nonce"], "test_nonce_123");
803 assert_eq!(claims["at_hash"], "test_at_hash");
804 assert_eq!(claims["auth_time"], 1234567890);
805 }
806
807 #[tokio::test]
808 async fn mint_id_token_omits_nonce_when_none() {
809 let db = test_db().await;
810 let key = db.create_signing_key(&ENC_KEY).await.unwrap();
811 let pem = decrypt_private_key(&key, &ENC_KEY).unwrap();
812 let kid = key.id.to_string();
813
814 let user_id = UserId::new();
815 let token = mint_id_token(
816 user_id,
817 ISSUER,
818 "ath_test_client",
819 None,
820 "hash",
821 0,
822 &kid,
823 &pem,
824 3600,
825 )
826 .unwrap();
827
828 let parts: Vec<&str> = token.splitn(3, '.').collect();
829 let payload = base64ct::Base64UrlUnpadded::decode_vec(parts[1]).unwrap();
830 let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
831 assert!(claims.get("nonce").is_none());
832 }
833
834 async fn setup_exchange(db: &Db) -> (Application, SigningKey, String, String, String, String) {
838 let email = Email::new("exchange@example.com".into()).unwrap();
839 let user = db
840 .create_user(email, "password123", None, None)
841 .await
842 .unwrap();
843
844 let (app, _secret) = db
845 .create_application(CreateApplicationParams {
846 name: "ExchangeApp".to_string(),
847 client_type: ClientType::Confidential,
848 redirect_uris: vec!["https://example.com/callback".to_string()],
849 is_trusted: false,
850 created_by: Some(user.id),
851 logo_url: None,
852 primary_color: None,
853 accent_hex: None,
854 accent_ink: None,
855 forced_mode: None,
856 font_css_url: None,
857 font_family: None,
858 splash_text: None,
859 splash_image_url: None,
860 splash_primitive: None,
861 splash_url: None,
862 shader_cell_scale: None,
863 })
864 .await
865 .unwrap();
866
867 let key = db.create_signing_key(&ENC_KEY).await.unwrap();
868 db.activate_signing_key(key.id).await.unwrap();
869 let pem = decrypt_private_key(&key, &ENC_KEY).unwrap();
870
871 let code_verifier = "test_verifier_string_with_enough_entropy_1234567890";
873 let digest = Sha256::digest(code_verifier.as_bytes());
874 let code_challenge = Base64UrlUnpadded::encode_string(&digest);
875
876 let raw_code = crate::authorization::generate_authorization_code();
877 let code_hash = hash_authorization_code(&raw_code);
878 db.create_authorization_code(
879 app.id,
880 user.id,
881 &code_hash,
882 "https://example.com/callback",
883 &["openid".to_string(), "profile".to_string()],
884 &code_challenge,
885 "S256",
886 Some("test_nonce"),
887 )
888 .await
889 .unwrap();
890
891 (
892 app,
893 key,
894 pem,
895 raw_code,
896 code_verifier.to_string(),
897 "https://example.com/callback".to_string(),
898 )
899 }
900
901 #[tokio::test]
902 async fn exchange_valid_code() {
903 let db = test_db().await;
904 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
905
906 let resp = exchange_authorization_code(
907 &db,
908 &raw_code,
909 &redirect_uri,
910 &verifier,
911 &app,
912 ISSUER,
913 &key,
914 &pem,
915 )
916 .await
917 .unwrap();
918
919 assert_eq!(resp.token_type, "Bearer");
920 assert_eq!(resp.expires_in, 3600);
921 assert!(!resp.access_token.is_empty());
922 assert!(!resp.refresh_token.is_empty());
923 assert!(!resp.id_token.is_empty());
924
925 let claims = db
927 .validate_access_token(&resp.access_token, ISSUER)
928 .await
929 .unwrap();
930 assert_eq!(claims.scope, "openid profile");
931 }
932
933 #[tokio::test]
934 async fn exchange_used_code_triggers_revocation() {
935 let db = test_db().await;
936 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
937
938 let _resp = exchange_authorization_code(
940 &db,
941 &raw_code,
942 &redirect_uri,
943 &verifier,
944 &app,
945 ISSUER,
946 &key,
947 &pem,
948 )
949 .await
950 .unwrap();
951
952 let err = exchange_authorization_code(
954 &db,
955 &raw_code,
956 &redirect_uri,
957 &verifier,
958 &app,
959 ISSUER,
960 &key,
961 &pem,
962 )
963 .await
964 .unwrap_err();
965 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("already used")));
966 }
967
968 #[tokio::test]
969 async fn exchange_wrong_redirect_uri() {
970 let db = test_db().await;
971 let (app, key, pem, raw_code, verifier, _) = setup_exchange(&db).await;
972
973 let err = exchange_authorization_code(
974 &db,
975 &raw_code,
976 "https://evil.example.com/callback",
977 &verifier,
978 &app,
979 ISSUER,
980 &key,
981 &pem,
982 )
983 .await
984 .unwrap_err();
985 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("redirect_uri")));
986 }
987
988 #[tokio::test]
989 async fn exchange_bad_pkce() {
990 let db = test_db().await;
991 let (app, key, pem, raw_code, _, redirect_uri) = setup_exchange(&db).await;
992
993 let err = exchange_authorization_code(
994 &db,
995 &raw_code,
996 &redirect_uri,
997 "wrong_verifier",
998 &app,
999 ISSUER,
1000 &key,
1001 &pem,
1002 )
1003 .await
1004 .unwrap_err();
1005 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("PKCE")));
1006 }
1007
1008 #[tokio::test]
1009 async fn exchange_invalid_code() {
1010 let db = test_db().await;
1011 let (app, key, pem, _, verifier, redirect_uri) = setup_exchange(&db).await;
1012
1013 let err = exchange_authorization_code(
1014 &db,
1015 "nonexistent_code",
1016 &redirect_uri,
1017 &verifier,
1018 &app,
1019 ISSUER,
1020 &key,
1021 &pem,
1022 )
1023 .await
1024 .unwrap_err();
1025 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("invalid")));
1026 }
1027
1028 #[tokio::test]
1029 async fn exchange_expired_code() {
1030 let db = test_db().await;
1031 let email = Email::new("expired@example.com".into()).unwrap();
1032 let user = db
1033 .create_user(email, "password123", None, None)
1034 .await
1035 .unwrap();
1036
1037 let (app, _) = db
1038 .create_application(CreateApplicationParams {
1039 name: "ExpiredApp".to_string(),
1040 client_type: ClientType::Confidential,
1041 redirect_uris: vec!["https://example.com/callback".to_string()],
1042 is_trusted: false,
1043 created_by: Some(user.id),
1044 logo_url: None,
1045 primary_color: None,
1046 accent_hex: None,
1047 accent_ink: None,
1048 forced_mode: None,
1049 font_css_url: None,
1050 font_family: None,
1051 splash_text: None,
1052 splash_image_url: None,
1053 splash_primitive: None,
1054 splash_url: None,
1055 shader_cell_scale: None,
1056 })
1057 .await
1058 .unwrap();
1059
1060 let key = db.create_signing_key(&ENC_KEY).await.unwrap();
1061 db.activate_signing_key(key.id).await.unwrap();
1062 let pem = decrypt_private_key(&key, &ENC_KEY).unwrap();
1063
1064 let code_verifier = "test_verifier_expired";
1065 let digest = Sha256::digest(code_verifier.as_bytes());
1066 let code_challenge = Base64UrlUnpadded::encode_string(&digest);
1067
1068 let raw_code = crate::authorization::generate_authorization_code();
1069 let code_hash = hash_authorization_code(&raw_code);
1070 db.create_authorization_code(
1071 app.id,
1072 user.id,
1073 &code_hash,
1074 "https://example.com/callback",
1075 &["openid".to_string()],
1076 &code_challenge,
1077 "S256",
1078 None,
1079 )
1080 .await
1081 .unwrap();
1082
1083 sqlx::query(
1085 "UPDATE allowthem_authorization_codes SET expires_at = '2020-01-01T00:00:00.000Z' WHERE code_hash = ?",
1086 )
1087 .bind(&code_hash)
1088 .execute(db.pool())
1089 .await
1090 .unwrap();
1091
1092 let err = exchange_authorization_code(
1093 &db,
1094 &raw_code,
1095 "https://example.com/callback",
1096 code_verifier,
1097 &app,
1098 ISSUER,
1099 &key,
1100 &pem,
1101 )
1102 .await
1103 .unwrap_err();
1104 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("expired")));
1105 }
1106
1107 #[tokio::test]
1108 async fn exchange_wrong_client() {
1109 let db = test_db().await;
1110 let (_, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1111
1112 let email_b = Email::new("other@example.com".into()).unwrap();
1113 let user_b = db
1114 .create_user(email_b, "password123", None, None)
1115 .await
1116 .unwrap();
1117 let (app_b, _) = db
1118 .create_application(CreateApplicationParams {
1119 name: "OtherApp".to_string(),
1120 client_type: ClientType::Confidential,
1121 redirect_uris: vec!["https://other.example.com/callback".to_string()],
1122 is_trusted: false,
1123 created_by: Some(user_b.id),
1124 logo_url: None,
1125 primary_color: None,
1126 accent_hex: None,
1127 accent_ink: None,
1128 forced_mode: None,
1129 font_css_url: None,
1130 font_family: None,
1131 splash_text: None,
1132 splash_image_url: None,
1133 splash_primitive: None,
1134 splash_url: None,
1135 shader_cell_scale: None,
1136 })
1137 .await
1138 .unwrap();
1139
1140 let err = exchange_authorization_code(
1141 &db,
1142 &raw_code,
1143 &redirect_uri,
1144 &verifier,
1145 &app_b,
1146 ISSUER,
1147 &key,
1148 &pem,
1149 )
1150 .await
1151 .unwrap_err();
1152 assert!(
1153 matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("different client"))
1154 );
1155 }
1156
1157 #[tokio::test]
1158 async fn access_token_has_correct_typ_header() {
1159 let db = test_db().await;
1160 let key = db.create_signing_key(&ENC_KEY).await.unwrap();
1161 let pem = decrypt_private_key(&key, &ENC_KEY).unwrap();
1162
1163 let token = mint_access_token(
1164 UserId::new(),
1165 ISSUER,
1166 "client",
1167 "openid",
1168 &key.id.to_string(),
1169 &pem,
1170 3600,
1171 "t@example.com",
1172 true,
1173 None,
1174 &[],
1175 &[],
1176 )
1177 .unwrap();
1178
1179 let header = jsonwebtoken::decode_header(&token).unwrap();
1180 assert_eq!(header.typ.as_deref(), Some("at+jwt"));
1181 assert_eq!(header.alg, Algorithm::RS256);
1182 assert!(header.kid.is_some());
1183 }
1184
1185 #[tokio::test]
1186 async fn id_token_has_correct_typ_header() {
1187 let db = test_db().await;
1188 let key = db.create_signing_key(&ENC_KEY).await.unwrap();
1189 let pem = decrypt_private_key(&key, &ENC_KEY).unwrap();
1190
1191 let token = mint_id_token(
1192 UserId::new(),
1193 ISSUER,
1194 "client",
1195 None,
1196 "hash",
1197 0,
1198 &key.id.to_string(),
1199 &pem,
1200 3600,
1201 )
1202 .unwrap();
1203
1204 let header = jsonwebtoken::decode_header(&token).unwrap();
1205 assert_eq!(header.typ.as_deref(), Some("JWT"));
1206 assert_eq!(header.alg, Algorithm::RS256);
1207 }
1208
1209 #[tokio::test]
1210 async fn exchange_id_token_at_hash_matches_access_token() {
1211 let db = test_db().await;
1212 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1213
1214 let resp = exchange_authorization_code(
1215 &db,
1216 &raw_code,
1217 &redirect_uri,
1218 &verifier,
1219 &app,
1220 ISSUER,
1221 &key,
1222 &pem,
1223 )
1224 .await
1225 .unwrap();
1226
1227 let parts: Vec<&str> = resp.id_token.splitn(3, '.').collect();
1228 let payload = base64ct::Base64UrlUnpadded::decode_vec(parts[1]).unwrap();
1229 let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
1230
1231 let expected = compute_at_hash(&resp.access_token);
1232 assert_eq!(claims["at_hash"].as_str().unwrap(), expected);
1233 }
1234
1235 #[tokio::test]
1236 async fn exchange_creates_refresh_token_in_db() {
1237 let db = test_db().await;
1238 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1239
1240 let resp = exchange_authorization_code(
1241 &db,
1242 &raw_code,
1243 &redirect_uri,
1244 &verifier,
1245 &app,
1246 ISSUER,
1247 &key,
1248 &pem,
1249 )
1250 .await
1251 .unwrap();
1252
1253 let refresh_hash = hash_refresh_token(&resp.refresh_token);
1254 let count: (i64,) =
1255 sqlx::query_as("SELECT COUNT(*) FROM allowthem_refresh_tokens WHERE token_hash = ?")
1256 .bind(&refresh_hash)
1257 .fetch_one(db.pool())
1258 .await
1259 .unwrap();
1260 assert_eq!(count.0, 1, "refresh token should be stored in DB");
1261 }
1262
1263 #[tokio::test]
1266 async fn get_refresh_token_by_hash_returns_token() {
1267 let db = test_db().await;
1268 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1269
1270 let resp = exchange_authorization_code(
1271 &db,
1272 &raw_code,
1273 &redirect_uri,
1274 &verifier,
1275 &app,
1276 ISSUER,
1277 &key,
1278 &pem,
1279 )
1280 .await
1281 .unwrap();
1282
1283 let hash = hash_refresh_token(&resp.refresh_token);
1284 let stored = db.get_refresh_token_by_hash(&hash).await.unwrap().unwrap();
1285 assert_eq!(stored.application_id, app.id);
1286 assert_eq!(stored.revoked_at, None);
1287 }
1288
1289 #[tokio::test]
1290 async fn get_refresh_token_by_hash_returns_none_for_unknown() {
1291 let db = test_db().await;
1292 let unknown = hash_refresh_token("nonexistent_raw_token");
1293 let result = db.get_refresh_token_by_hash(&unknown).await.unwrap();
1294 assert!(result.is_none());
1295 }
1296
1297 #[tokio::test]
1298 async fn revoke_refresh_token_sets_revoked_at() {
1299 let db = test_db().await;
1300 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1301
1302 let resp = exchange_authorization_code(
1303 &db,
1304 &raw_code,
1305 &redirect_uri,
1306 &verifier,
1307 &app,
1308 ISSUER,
1309 &key,
1310 &pem,
1311 )
1312 .await
1313 .unwrap();
1314
1315 let hash = hash_refresh_token(&resp.refresh_token);
1316 let stored = db.get_refresh_token_by_hash(&hash).await.unwrap().unwrap();
1317 assert!(stored.revoked_at.is_none());
1318
1319 db.revoke_refresh_token(stored.id).await.unwrap();
1320
1321 let after = db.get_refresh_token_by_hash(&hash).await.unwrap().unwrap();
1322 assert!(after.revoked_at.is_some());
1323 }
1324
1325 #[tokio::test]
1326 async fn revoke_refresh_token_idempotent() {
1327 let db = test_db().await;
1328 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1329
1330 let resp = exchange_authorization_code(
1331 &db,
1332 &raw_code,
1333 &redirect_uri,
1334 &verifier,
1335 &app,
1336 ISSUER,
1337 &key,
1338 &pem,
1339 )
1340 .await
1341 .unwrap();
1342
1343 let hash = hash_refresh_token(&resp.refresh_token);
1344 let stored = db.get_refresh_token_by_hash(&hash).await.unwrap().unwrap();
1345
1346 db.revoke_refresh_token(stored.id).await.unwrap();
1347 db.revoke_refresh_token(stored.id).await.unwrap();
1348 }
1349
1350 #[tokio::test]
1353 async fn exchange_refresh_token_valid() {
1354 let db = test_db().await;
1355 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1356
1357 let initial = exchange_authorization_code(
1358 &db,
1359 &raw_code,
1360 &redirect_uri,
1361 &verifier,
1362 &app,
1363 ISSUER,
1364 &key,
1365 &pem,
1366 )
1367 .await
1368 .unwrap();
1369
1370 let resp =
1371 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1372 .await
1373 .unwrap();
1374
1375 assert!(!resp.access_token.is_empty());
1376 assert!(!resp.refresh_token.is_empty());
1377 assert_ne!(resp.refresh_token, initial.refresh_token);
1378 assert_eq!(resp.token_type, "Bearer");
1379 assert_eq!(resp.expires_in, 3600);
1380
1381 let claims = db
1382 .validate_access_token(&resp.access_token, ISSUER)
1383 .await
1384 .unwrap();
1385 assert_eq!(claims.scope, "openid profile");
1386 }
1387
1388 #[tokio::test]
1389 async fn exchange_refresh_token_revokes_old_token() {
1390 let db = test_db().await;
1391 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1392
1393 let initial = exchange_authorization_code(
1394 &db,
1395 &raw_code,
1396 &redirect_uri,
1397 &verifier,
1398 &app,
1399 ISSUER,
1400 &key,
1401 &pem,
1402 )
1403 .await
1404 .unwrap();
1405
1406 let old_hash = hash_refresh_token(&initial.refresh_token);
1407 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1408 .await
1409 .unwrap();
1410
1411 let old_stored = db
1412 .get_refresh_token_by_hash(&old_hash)
1413 .await
1414 .unwrap()
1415 .unwrap();
1416 assert!(
1417 old_stored.revoked_at.is_some(),
1418 "old token should be revoked"
1419 );
1420 }
1421
1422 #[tokio::test]
1423 async fn exchange_refresh_token_revoked_token_fails() {
1424 let db = test_db().await;
1425 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1426
1427 let initial = exchange_authorization_code(
1428 &db,
1429 &raw_code,
1430 &redirect_uri,
1431 &verifier,
1432 &app,
1433 ISSUER,
1434 &key,
1435 &pem,
1436 )
1437 .await
1438 .unwrap();
1439
1440 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1442 .await
1443 .unwrap();
1444
1445 let err =
1447 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1448 .await
1449 .unwrap_err();
1450 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("revoked")));
1451 }
1452
1453 #[tokio::test]
1454 async fn exchange_refresh_token_expired_fails() {
1455 let db = test_db().await;
1456 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1457
1458 let initial = exchange_authorization_code(
1459 &db,
1460 &raw_code,
1461 &redirect_uri,
1462 &verifier,
1463 &app,
1464 ISSUER,
1465 &key,
1466 &pem,
1467 )
1468 .await
1469 .unwrap();
1470
1471 let hash = hash_refresh_token(&initial.refresh_token);
1472 sqlx::query(
1473 "UPDATE allowthem_refresh_tokens SET expires_at = '2020-01-01T00:00:00.000Z' WHERE token_hash = ?",
1474 )
1475 .bind(&hash)
1476 .execute(db.pool())
1477 .await
1478 .unwrap();
1479
1480 let err =
1481 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1482 .await
1483 .unwrap_err();
1484 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("expired")));
1485 }
1486
1487 #[tokio::test]
1488 async fn exchange_refresh_token_wrong_client_fails() {
1489 let db = test_db().await;
1490 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1491
1492 let initial = exchange_authorization_code(
1493 &db,
1494 &raw_code,
1495 &redirect_uri,
1496 &verifier,
1497 &app,
1498 ISSUER,
1499 &key,
1500 &pem,
1501 )
1502 .await
1503 .unwrap();
1504
1505 let email_b = Email::new("other_refresh@example.com".into()).unwrap();
1506 let user_b = db
1507 .create_user(email_b, "password123", None, None)
1508 .await
1509 .unwrap();
1510 let (app_b, _) = db
1511 .create_application(CreateApplicationParams {
1512 name: "OtherRefreshApp".to_string(),
1513 client_type: ClientType::Confidential,
1514 redirect_uris: vec!["https://other.example.com/callback".to_string()],
1515 is_trusted: false,
1516 created_by: Some(user_b.id),
1517 logo_url: None,
1518 primary_color: None,
1519 accent_hex: None,
1520 accent_ink: None,
1521 forced_mode: None,
1522 font_css_url: None,
1523 font_family: None,
1524 splash_text: None,
1525 splash_image_url: None,
1526 splash_primitive: None,
1527 splash_url: None,
1528 shader_cell_scale: None,
1529 })
1530 .await
1531 .unwrap();
1532
1533 let err = exchange_refresh_token(
1534 &db,
1535 &initial.refresh_token,
1536 None,
1537 &app_b,
1538 ISSUER,
1539 &key,
1540 &pem,
1541 )
1542 .await
1543 .unwrap_err();
1544 assert!(
1545 matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("different client"))
1546 );
1547 }
1548
1549 #[tokio::test]
1550 async fn exchange_refresh_token_scope_subset_succeeds() {
1551 let db = test_db().await;
1552 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1553
1554 let initial = exchange_authorization_code(
1555 &db,
1556 &raw_code,
1557 &redirect_uri,
1558 &verifier,
1559 &app,
1560 ISSUER,
1561 &key,
1562 &pem,
1563 )
1564 .await
1565 .unwrap();
1566
1567 let resp = exchange_refresh_token(
1568 &db,
1569 &initial.refresh_token,
1570 Some("openid"),
1571 &app,
1572 ISSUER,
1573 &key,
1574 &pem,
1575 )
1576 .await
1577 .unwrap();
1578
1579 let claims = db
1580 .validate_access_token(&resp.access_token, ISSUER)
1581 .await
1582 .unwrap();
1583 assert_eq!(claims.scope, "openid");
1584 }
1585
1586 #[tokio::test]
1587 async fn exchange_refresh_token_scope_escalation_fails() {
1588 let db = test_db().await;
1589 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1590
1591 let initial = exchange_authorization_code(
1592 &db,
1593 &raw_code,
1594 &redirect_uri,
1595 &verifier,
1596 &app,
1597 ISSUER,
1598 &key,
1599 &pem,
1600 )
1601 .await
1602 .unwrap();
1603
1604 let err = exchange_refresh_token(
1605 &db,
1606 &initial.refresh_token,
1607 Some("openid admin"),
1608 &app,
1609 ISSUER,
1610 &key,
1611 &pem,
1612 )
1613 .await
1614 .unwrap_err();
1615 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("exceeds")));
1616 }
1617
1618 #[tokio::test]
1619 async fn exchange_refresh_token_no_scope_uses_original() {
1620 let db = test_db().await;
1621 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1622
1623 let initial = exchange_authorization_code(
1624 &db,
1625 &raw_code,
1626 &redirect_uri,
1627 &verifier,
1628 &app,
1629 ISSUER,
1630 &key,
1631 &pem,
1632 )
1633 .await
1634 .unwrap();
1635
1636 let resp =
1637 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1638 .await
1639 .unwrap();
1640
1641 let claims = db
1642 .validate_access_token(&resp.access_token, ISSUER)
1643 .await
1644 .unwrap();
1645 assert_eq!(claims.scope, "openid profile");
1646 }
1647
1648 #[tokio::test]
1649 async fn exchange_refresh_token_invalid_hash_fails() {
1650 let db = test_db().await;
1651 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1652 let _ = exchange_authorization_code(
1653 &db,
1654 &raw_code,
1655 &redirect_uri,
1656 &verifier,
1657 &app,
1658 ISSUER,
1659 &key,
1660 &pem,
1661 )
1662 .await
1663 .unwrap();
1664
1665 let err = exchange_refresh_token(
1666 &db,
1667 "totally_invalid_garbage_token",
1668 None,
1669 &app,
1670 ISSUER,
1671 &key,
1672 &pem,
1673 )
1674 .await
1675 .unwrap_err();
1676 assert!(matches!(err, TokenError::InvalidGrant(ref msg) if msg.contains("invalid")));
1677 }
1678
1679 #[tokio::test]
1680 async fn exchange_refresh_token_propagates_authorization_code_id() {
1681 let db = test_db().await;
1682 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1683
1684 let initial = exchange_authorization_code(
1685 &db,
1686 &raw_code,
1687 &redirect_uri,
1688 &verifier,
1689 &app,
1690 ISSUER,
1691 &key,
1692 &pem,
1693 )
1694 .await
1695 .unwrap();
1696
1697 let first_hash = hash_refresh_token(&initial.refresh_token);
1698 let first_stored = db
1699 .get_refresh_token_by_hash(&first_hash)
1700 .await
1701 .unwrap()
1702 .unwrap();
1703 let original_auth_code_id = first_stored.authorization_code_id;
1704
1705 let rotated =
1706 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1707 .await
1708 .unwrap();
1709
1710 let second_hash = hash_refresh_token(&rotated.refresh_token);
1711 let second_stored = db
1712 .get_refresh_token_by_hash(&second_hash)
1713 .await
1714 .unwrap()
1715 .unwrap();
1716
1717 assert_eq!(
1718 second_stored.authorization_code_id, original_auth_code_id,
1719 "authorization_code_id must propagate through rotation"
1720 );
1721 }
1722
1723 #[tokio::test]
1724 async fn exchange_refresh_token_chained_rotation_succeeds() {
1725 let db = test_db().await;
1726 let (app, key, pem, raw_code, verifier, redirect_uri) = setup_exchange(&db).await;
1727
1728 let initial = exchange_authorization_code(
1729 &db,
1730 &raw_code,
1731 &redirect_uri,
1732 &verifier,
1733 &app,
1734 ISSUER,
1735 &key,
1736 &pem,
1737 )
1738 .await
1739 .unwrap();
1740
1741 let second =
1742 exchange_refresh_token(&db, &initial.refresh_token, None, &app, ISSUER, &key, &pem)
1743 .await
1744 .unwrap();
1745 assert_ne!(second.refresh_token, initial.refresh_token);
1746
1747 let third =
1748 exchange_refresh_token(&db, &second.refresh_token, None, &app, ISSUER, &key, &pem)
1749 .await
1750 .unwrap();
1751 assert_ne!(third.refresh_token, second.refresh_token);
1752
1753 let first_hash = hash_refresh_token(&initial.refresh_token);
1754 let first_stored = db
1755 .get_refresh_token_by_hash(&first_hash)
1756 .await
1757 .unwrap()
1758 .unwrap();
1759 assert!(
1760 first_stored.revoked_at.is_some(),
1761 "first token must be revoked"
1762 );
1763
1764 let second_hash = hash_refresh_token(&second.refresh_token);
1765 let second_stored = db
1766 .get_refresh_token_by_hash(&second_hash)
1767 .await
1768 .unwrap()
1769 .unwrap();
1770 assert!(
1771 second_stored.revoked_at.is_some(),
1772 "second token must be revoked"
1773 );
1774 }
1775}