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