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