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