AkurAI Build
Menu

Public repository / 1f023d15b3f586c38eb60898e38dc03c4e77bed2

AkurAI-IDP / src/routes/token.rs

Latest change 8e1c2af4737a1aec991c04ba9b8418755775cc6b - tokens: fix refresh audience swap; add role to access tokens by Ólafur Búi Ólafsson

use axum::{
    Json, Router,
    extract::Request,
    http::{StatusCode, header},
    response::IntoResponse,
    routing::post,
};
use serde::Deserialize;
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::config;
use crate::db::with_db;
use crate::lib::audit::{self, log_audit_event};
use crate::lib::clients;
use crate::lib::crypto::{generate_secure_token, sha256};
use crate::lib::jwt::sign_token;
use crate::lib::pkce::verify_code_challenge;

pub fn router() -> Router {
    Router::new().route("/token", post(token_endpoint))
}

#[derive(Deserialize, Default)]
struct TokenRequest {
    grant_type: Option<String>,
    code: Option<String>,
    redirect_uri: Option<String>,
    client_id: Option<String>,
    client_secret: Option<String>,
    code_verifier: Option<String>,
    refresh_token: Option<String>,
    organization_id: Option<String>,
    scope: Option<String>,
}

async fn token_endpoint(request: Request) -> impl IntoResponse {
    let ip = extract_ip(&request);

    // Extract client credentials from Basic auth header if present
    let (basic_client_id, basic_client_secret) = extract_basic_auth(&request);

    // Parse body as form or JSON
    let body_bytes = match axum::body::to_bytes(request.into_body(), 1024 * 64).await {
        Ok(b) => b,
        Err(_) => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_request",
                "Failed to read body",
            );
        }
    };

    let mut params: TokenRequest =
        serde_json::from_slice(&body_bytes).unwrap_or_else(|_| parse_form_urlencoded(&body_bytes));

    // Basic auth overrides body params for client credentials
    if let Some(id) = basic_client_id {
        params.client_id = Some(id);
    }
    if let Some(secret) = basic_client_secret {
        params.client_secret = Some(secret);
    }

    let grant_type = match &params.grant_type {
        Some(gt) => gt.as_str(),
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_request",
                "Missing grant_type",
            );
        }
    };

    match grant_type {
        "authorization_code" => handle_authorization_code(params, &ip).await,
        "refresh_token" => handle_refresh_token(params, &ip).await,
        "client_credentials" => handle_client_credentials(params, &ip).await,
        _ => token_error(
            StatusCode::BAD_REQUEST,
            "unsupported_grant_type",
            "Unsupported grant_type",
        ),
    }
}

async fn handle_authorization_code(params: TokenRequest, ip: &str) -> axum::response::Response {
    let code = match &params.code {
        Some(c) => c.clone(),
        None => return token_error(StatusCode::BAD_REQUEST, "invalid_request", "Missing code"),
    };
    let client_id = match &params.client_id {
        Some(c) => c.clone(),
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_request",
                "Missing client_id",
            );
        }
    };
    let redirect_uri = match &params.redirect_uri {
        Some(r) => r.clone(),
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_request",
                "Missing redirect_uri",
            );
        }
    };

    let now = now_secs();

    // Look up auth code
    let auth_code = with_db(|conn| {
        conn.query_row(
            "SELECT code, client_id, user_id, organization_id, scopes, redirect_uri,
                    code_challenge, code_challenge_method, nonce, expires_at, used
             FROM auth_codes WHERE code = ?1",
            rusqlite::params![code],
            |row| {
                Ok(AuthCode {
                    code: row.get(0)?,
                    client_id: row.get(1)?,
                    user_id: row.get(2)?,
                    organization_id: row.get(3)?,
                    scopes: row.get(4)?,
                    redirect_uri: row.get(5)?,
                    code_challenge: row.get(6)?,
                    code_challenge_method: row.get(7)?,
                    nonce: row.get(8)?,
                    expires_at: row.get(9)?,
                    used: row.get::<_, i64>(10)? != 0,
                })
            },
        )
        .ok()
    });

    let auth_code = match auth_code {
        Some(ac) => ac,
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_grant",
                "Invalid authorization code",
            );
        }
    };

    // Validate
    if auth_code.used {
        // Code replay — revoke all tokens for this code's user+client
        with_db(|conn| {
            conn.execute(
                "UPDATE refresh_tokens SET revoked = 1 WHERE client_id = ?1 AND user_id = ?2",
                rusqlite::params![auth_code.client_id, auth_code.user_id],
            )
            .ok();
        });
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Authorization code already used",
        );
    }
    if auth_code.expires_at < now {
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Authorization code expired",
        );
    }
    if auth_code.client_id != client_id {
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Client ID mismatch",
        );
    }
    if auth_code.redirect_uri != redirect_uri {
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Redirect URI mismatch",
        );
    }

    // Verify client secret
    if !verify_client_secret(&client_id, params.client_secret.as_deref()) {
        return token_error(
            StatusCode::UNAUTHORIZED,
            "invalid_client",
            "Invalid client credentials",
        );
    }

    // Public clients prove possession only through PKCE.
    if auth_code.code_challenge.is_none() && is_public_client(&client_id) {
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Public clients require an S256 PKCE challenge",
        );
    }

    // PKCE verification
    if let Some(ref challenge) = auth_code.code_challenge {
        let method = auth_code.code_challenge_method.as_deref().unwrap_or("S256");
        let verifier = match &params.code_verifier {
            Some(v) => v,
            None => {
                return token_error(
                    StatusCode::BAD_REQUEST,
                    "invalid_request",
                    "Missing code_verifier",
                );
            }
        };
        if !verify_code_challenge(verifier, challenge, method) {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_grant",
                "PKCE verification failed",
            );
        }
    }

    // Mark code as used
    with_db(|conn| {
        conn.execute(
            "UPDATE auth_codes SET used = 1 WHERE code = ?1",
            rusqlite::params![code],
        )
        .ok();
    });

    let user = match load_user(&auth_code.user_id, auth_code.organization_id.as_deref()) {
        Some(u) => u,
        None => return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "User not found"),
    };

    let scopes = &auth_code.scopes;
    let base_url = &config::get().base_url;

    // Claims intentionally describe only the selected organization. Other
    // memberships stay authoritative in the IDP and are never copied into a
    // bearer token.
    let id_token_claims = json!({
        "iss": base_url,
        "sub": user.id,
        "aud": client_id,
        "exp": now + 3600,
        "iat": now,
        "claims_version": crate::lib::organizations::CLAIMS_VERSION,
        "authorization_version": user.authorization_version,
        "email": user.email,
        "email_verified": user.email_verified,
        "organization_id": user.tenant_id,
        "tenant_id": user.tenant_id,
        "role": user.role,
        "nonce": auth_code.nonce,
    });

    let access_token_claims =
        user_access_claims(base_url, &user, client_audience(&client_id), scopes, now);

    let (kid, private_key) = match get_active_signing_key() {
        Some(k) => k,
        None => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "No signing key available",
            );
        }
    };

    let access_token = match sign_token(&access_token_claims, &private_key, &kid, 3600) {
        Ok(t) => t,
        Err(_) => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "Failed to sign token",
            );
        }
    };

    let id_token = match sign_token(&id_token_claims, &private_key, &kid, 3600) {
        Ok(t) => t,
        Err(_) => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "Failed to sign token",
            );
        }
    };
    // Issue refresh token if offline_access scope.
    let refresh_token = if scopes.contains("offline_access") {
        let rt = generate_secure_token(32);
        let rt_hash = sha256(&rt);
        let expires = now + 30 * 24 * 3600;
        with_db(|conn| {
            conn.execute(
                "INSERT INTO refresh_tokens
                 (token_hash, client_id, user_id, organization_id, scopes, expires_at, revoked)
                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)",
                rusqlite::params![rt_hash, client_id, user.id, user.tenant_id, scopes, expires],
            )
            .ok();
        });
        Some(rt)
    } else {
        None
    };

    // Audit
    let audit_meta = json!({"grant_type": "authorization_code", "client_id": client_id});
    with_db(|conn| {
        log_audit_event(
            conn,
            Some(&user.tenant_id),
            Some(&user.id),
            audit::TOKEN_ISSUED,
            Some(ip),
            Some(&audit_meta),
        );
    });

    let mut response = json!({
        "access_token": access_token,
        "token_type": "Bearer",
        "expires_in": 3600,
        "id_token": id_token,
        "scope": scopes,
    });
    if let Some(rt) = refresh_token {
        response["refresh_token"] = json!(rt);
    }

    (StatusCode::OK, Json(response)).into_response()
}

async fn handle_refresh_token(params: TokenRequest, ip: &str) -> axum::response::Response {
    let rt = match &params.refresh_token {
        Some(t) => t.clone(),
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_request",
                "Missing refresh_token",
            );
        }
    };
    let client_id = match &params.client_id {
        Some(c) => c.clone(),
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_request",
                "Missing client_id",
            );
        }
    };

    if !verify_client_secret(&client_id, params.client_secret.as_deref()) {
        return token_error(
            StatusCode::UNAUTHORIZED,
            "invalid_client",
            "Invalid client credentials",
        );
    }

    let rt_hash = sha256(&rt);
    let now = now_secs();

    // Look up refresh token
    let stored = with_db(|conn| {
        conn.query_row(
            "SELECT token_hash, client_id, user_id, organization_id, scopes, expires_at, revoked
             FROM refresh_tokens WHERE token_hash = ?1",
            rusqlite::params![rt_hash],
            |row| {
                Ok(StoredRefreshToken {
                    token_hash: row.get(0)?,
                    client_id: row.get(1)?,
                    user_id: row.get(2)?,
                    organization_id: row.get(3)?,
                    scopes: row.get(4)?,
                    expires_at: row.get(5)?,
                    revoked: row.get::<_, i64>(6)? != 0,
                })
            },
        )
        .ok()
    });

    let stored = match stored {
        Some(s) => s,
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_grant",
                "Invalid refresh token",
            );
        }
    };

    if stored.revoked {
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Refresh token revoked",
        );
    }
    if stored.expires_at < now {
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Refresh token expired",
        );
    }
    if stored.client_id != client_id {
        return token_error(
            StatusCode::BAD_REQUEST,
            "invalid_grant",
            "Client ID mismatch",
        );
    }

    // Rotate: revoke old, issue new
    let new_rt = generate_secure_token(32);
    let new_rt_hash = sha256(&new_rt);
    let new_expires = now + 30 * 24 * 3600;

    let rotation = with_db(|conn| {
        conn.execute(
            "UPDATE refresh_tokens SET revoked = 1
             WHERE token_hash = ?1 AND client_id = ?2",
            rusqlite::params![stored.token_hash, client_id],
        )?;
        conn.execute(
            "INSERT INTO refresh_tokens
             (token_hash, client_id, user_id, organization_id, scopes, expires_at, revoked)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)",
            rusqlite::params![
                new_rt_hash,
                client_id,
                stored.user_id,
                stored.organization_id,
                stored.scopes,
                new_expires
            ],
        )?;
        Ok::<_, rusqlite::Error>(())
    });
    if rotation.is_err() {
        return token_error(
            StatusCode::INTERNAL_SERVER_ERROR,
            "server_error",
            "Token rotation failed",
        );
    }

    let user = match load_user(&stored.user_id, stored.organization_id.as_deref()) {
        Some(u) => u,
        None => return token_error(StatusCode::BAD_REQUEST, "invalid_grant", "User not found"),
    };

    let base_url = &config::get().base_url;

    let access_token_claims = user_access_claims(
        base_url,
        &user,
        client_audience(&client_id),
        &stored.scopes,
        now,
    );

    let id_token_claims = json!({
        "iss": base_url,
        "sub": user.id,
        "aud": client_id,
        "exp": now + 3600,
        "iat": now,
        "claims_version": crate::lib::organizations::CLAIMS_VERSION,
        "authorization_version": user.authorization_version,
        "email": user.email,
        "email_verified": user.email_verified,
        "organization_id": user.tenant_id,
        "tenant_id": user.tenant_id,
        "role": user.role,
    });

    let (kid, private_key) = match get_active_signing_key() {
        Some(k) => k,
        None => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "No signing key",
            );
        }
    };

    let access_token = match sign_token(&access_token_claims, &private_key, &kid, 3600) {
        Ok(t) => t,
        Err(_) => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "Sign failed",
            );
        }
    };
    let id_token = match sign_token(&id_token_claims, &private_key, &kid, 3600) {
        Ok(t) => t,
        Err(_) => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "Sign failed",
            );
        }
    };

    let audit_meta = json!({"grant_type": "refresh_token", "client_id": client_id});
    with_db(|conn| {
        log_audit_event(
            conn,
            Some(&user.tenant_id),
            Some(&user.id),
            audit::TOKEN_ISSUED,
            Some(ip),
            Some(&audit_meta),
        );
    });

    let response = json!({
        "access_token": access_token,
        "token_type": "Bearer",
        "expires_in": 3600,
        "refresh_token": new_rt,
        "id_token": id_token,
        "scope": stored.scopes,
    });

    (StatusCode::OK, Json(response)).into_response()
}

async fn handle_client_credentials(params: TokenRequest, ip: &str) -> axum::response::Response {
    let client_id = match &params.client_id {
        Some(c) => c.clone(),
        None => {
            return token_error(
                StatusCode::BAD_REQUEST,
                "invalid_request",
                "Missing client_id",
            );
        }
    };

    if is_public_client(&client_id)
        || !verify_client_secret(&client_id, params.client_secret.as_deref())
    {
        return token_error(
            StatusCode::UNAUTHORIZED,
            "invalid_client",
            "Invalid client credentials",
        );
    }

    // Verify client supports client_credentials grant
    let client = with_db(|conn| {
        conn.query_row(
            "SELECT id, tenant_id, grant_types, scopes FROM clients WHERE id = ?1",
            rusqlite::params![client_id],
            |row| {
                Ok(ClientInfo {
                    id: row.get(0)?,
                    tenant_id: row.get(1)?,
                    grant_types: row.get(2)?,
                    scopes: row.get(3)?,
                })
            },
        )
        .ok()
    });

    let client = match client {
        Some(c) => c,
        None => {
            return token_error(
                StatusCode::UNAUTHORIZED,
                "invalid_client",
                "Client not found",
            );
        }
    };

    let grant_types = crate::lib::parse_json_or_space_separated(&client.grant_types);
    if !grant_types.iter().any(|g| g == "client_credentials") {
        return token_error(
            StatusCode::BAD_REQUEST,
            "unauthorized_client",
            "Grant type not allowed",
        );
    }

    let requested_scope = params.scope.as_deref().unwrap_or(&client.scopes);
    let now = now_secs();
    let base_url = &config::get().base_url;

    let access_token_claims = json!({
        "iss": base_url,
        "sub": client_id,
        "client_id": client_id,
        "aud": client_audience(&client_id),
        "exp": now + 3600,
        "iat": now,
        "scope": requested_scope,
        "organization_id": client.tenant_id,
        "tenant_id": client.tenant_id,
        "token_type": "client_credentials",
    });

    let (kid, private_key) = match get_active_signing_key() {
        Some(k) => k,
        None => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "No signing key",
            );
        }
    };

    let access_token = match sign_token(&access_token_claims, &private_key, &kid, 3600) {
        Ok(t) => t,
        Err(_) => {
            return token_error(
                StatusCode::INTERNAL_SERVER_ERROR,
                "server_error",
                "Sign failed",
            );
        }
    };

    let audit_meta = json!({"grant_type": "client_credentials", "client_id": client_id});
    with_db(|conn| {
        log_audit_event(
            conn,
            Some(&client.tenant_id),
            None,
            audit::TOKEN_ISSUED,
            Some(ip),
            Some(&audit_meta),
        );
    });

    let response = json!({
        "access_token": access_token,
        "token_type": "Bearer",
        "expires_in": 3600,
        "scope": requested_scope,
    });

    (StatusCode::OK, Json(response)).into_response()
}
#[allow(dead_code)]
struct AuthCode {
    code: String,
    client_id: String,
    user_id: String,
    organization_id: Option<String>,
    scopes: String,
    redirect_uri: String,
    code_challenge: Option<String>,
    code_challenge_method: Option<String>,
    nonce: Option<String>,
    expires_at: i64,
    used: bool,
}

struct StoredRefreshToken {
    token_hash: String,
    client_id: String,
    user_id: String,
    organization_id: Option<String>,
    scopes: String,
    expires_at: i64,
    revoked: bool,
}

#[allow(dead_code)]
struct ClientInfo {
    id: String,
    tenant_id: String,
    grant_types: String,
    scopes: String,
}

/// Access-token claims for a user. `role` is the member's role in the
/// selected organization so resource servers can gate admin actions.
fn user_access_claims(
    base_url: &str,
    user: &UserInfo,
    audience: serde_json::Value,
    scopes: &str,
    now: i64,
) -> serde_json::Value {
    json!({
        "iss": base_url,
        "sub": user.id,
        "aud": audience,
        "exp": now + 3600,
        "iat": now,
        "scope": scopes,
        "claims_version": crate::lib::organizations::CLAIMS_VERSION,
        "authorization_version": user.authorization_version,
        "organization_id": user.tenant_id,
        "tenant_id": user.tenant_id,
        "role": user.role,
    })
}

struct UserInfo {
    id: String,
    tenant_id: String,
    email: String,
    email_verified: bool,
    role: String,
    authorization_version: i64,
}

fn now_secs() -> i64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64
}

fn load_user(user_id: &str, organization_id: Option<&str>) -> Option<UserInfo> {
    with_db(|conn| {
        let organization_id = organization_id.unwrap_or("");
        conn.query_row(
            "SELECT u.id, u.tenant_id, u.email, u.email_verified,
                    m.role, u.authorization_version, m.organization_id
             FROM users u
             JOIN organization_memberships m
               ON m.organization_id = CASE
                    WHEN ?2 = '' THEN u.tenant_id ELSE ?2 END
              AND m.user_id = u.id AND m.status = 'active'
             WHERE u.id = ?1",
            rusqlite::params![user_id, organization_id],
            |row| {
                Ok(UserInfo {
                    id: row.get(0)?,
                    tenant_id: row.get(6)?,
                    email: row.get(2)?,
                    email_verified: row.get::<_, i64>(3)? != 0,
                    role: row.get(4)?,
                    authorization_version: row.get(5)?,
                })
            },
        )
        .ok()
    })
}
fn verify_client_secret(client_id: &str, secret: Option<&str>) -> bool {
    with_db(|conn| clients::authenticate(conn, client_id, secret))
}

fn is_public_client(client_id: &str) -> bool {
    with_db(|conn| clients::load_policy(conn, client_id)).is_some_and(|p| p.is_public())
}

fn client_audience(client_id: &str) -> serde_json::Value {
    with_db(|conn| clients::access_token_audience(conn, client_id))
}

fn get_active_signing_key() -> Option<(String, String)> {
    with_db(|conn| {
        conn.query_row(
            "SELECT kid, private_key_enc FROM signing_keys WHERE active = 1 ORDER BY created_at DESC LIMIT 1",
            [],
            |row| Ok((row.get(0)?, row.get(1)?)),
        )
        .ok()
    })
}

fn extract_basic_auth(request: &Request) -> (Option<String>, Option<String>) {
    let header = match request.headers().get(header::AUTHORIZATION) {
        Some(h) => h,
        None => return (None, None),
    };
    let value = match header.to_str().ok() {
        Some(v) => v,
        None => return (None, None),
    };
    let encoded = match value.strip_prefix("Basic ") {
        Some(e) => e,
        None => return (None, None),
    };
    use base64::Engine;
    let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded) {
        Ok(d) => d,
        Err(_) => return (None, None),
    };
    let decoded_str = match String::from_utf8(decoded) {
        Ok(s) => s,
        Err(_) => return (None, None),
    };
    match decoded_str.split_once(':') {
        Some((id, secret)) => (Some(id.to_string()), Some(secret.to_string())),
        None => (None, None),
    }
}

fn extract_ip(request: &Request) -> String {
    // Try X-Forwarded-For first, then peer addr
    request
        .headers()
        .get("x-forwarded-for")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.split(',').next())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "unknown".to_string())
}

fn parse_form_urlencoded(bytes: &[u8]) -> TokenRequest {
    let s = std::str::from_utf8(bytes).unwrap_or("");
    let mut params = TokenRequest::default();
    for pair in s.split('&') {
        if let Some((key, value)) = pair.split_once('=') {
            let value = urldecode(value);
            match key {
                "grant_type" => params.grant_type = Some(value),
                "code" => params.code = Some(value),
                "redirect_uri" => params.redirect_uri = Some(value),
                "client_id" => params.client_id = Some(value),
                "client_secret" => params.client_secret = Some(value),
                "code_verifier" => params.code_verifier = Some(value),
                "refresh_token" => params.refresh_token = Some(value),
                "organization_id" => params.organization_id = Some(value),
                _ => {}
            }
        }
    }
    params
}

fn urldecode(s: &str) -> String {
    let s = s.replace('+', " ");
    let mut result = Vec::with_capacity(s.len());
    let mut chars = s.bytes();
    while let Some(b) = chars.next() {
        if b == b'%' {
            let hi = chars.next().unwrap_or(b'0');
            let lo = chars.next().unwrap_or(b'0');
            let hex = [hi, lo];
            if let Ok(val) = u8::from_str_radix(std::str::from_utf8(&hex).unwrap_or("00"), 16) {
                result.push(val);
            }
        } else {
            result.push(b);
        }
    }
    String::from_utf8(result).unwrap_or_default()
}

fn token_error(status: StatusCode, error: &str, description: &str) -> axum::response::Response {
    let body = json!({
        "error": error,
        "error_description": description,
    });
    (status, Json(body)).into_response()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn user() -> UserInfo {
        UserInfo {
            id: "u1".into(),
            tenant_id: "org".into(),
            email: "a@x.is".into(),
            email_verified: true,
            role: "admin".into(),
            authorization_version: 3,
        }
    }

    #[test]
    fn access_claims_carry_audience_role_and_organization() {
        let aud = json!(["akurai-agent-mail", "https://mail.olibuijr.com/mcp"]);
        let claims =
            user_access_claims("https://auth.example", &user(), aud.clone(), "openid", 100);
        assert_eq!(claims["aud"], aud);
        assert_eq!(claims["role"], "admin");
        assert_eq!(claims["organization_id"], "org");
        assert_eq!(claims["sub"], "u1");
        assert_eq!(claims["exp"], 3700);
        assert!(claims.get("client_id").is_none());
    }
}