Skip to main content

allowthem_core/
token_issuance.rs

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