//! Clerk / OIDC session-token auth, tested with REAL crypto against a //! REAL issuer: a fresh RSA keypair, a live HTTP server publishing the //! OIDC discovery document and JWKS, and RS256 tokens shaped exactly like //! Clerk session JWTs. No mocks — this is the verification path itself. use std::sync::Arc; use cm_auth::{AuthError, AuthService, JwtVerifier}; use cm_domain::Role; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use rsa::pkcs1::EncodeRsaPrivateKey; use rsa::traits::PublicKeyParts; use rsa::RsaPrivateKey; use serde_json::json; struct Issuer { url: String, encoding_key: EncodingKey, other_key: EncodingKey, } fn b64url(bytes: &[u8]) -> String { use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; URL_SAFE_NO_PAD.encode(bytes) } /// Generates a real RSA key, publishes its JWKS + discovery doc on a live /// local server, and returns signing keys (the real one and an imposter). async fn spawn_issuer() -> Issuer { let mut rng = rand_core::OsRng; let key = RsaPrivateKey::new(&mut rng, 2048).expect("keygen"); let imposter = RsaPrivateKey::new(&mut rng, 2048).expect("keygen"); let public = key.to_public_key(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let url = format!("http://{addr}"); let jwks = json!({ "keys": [{ "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "tc-test-key", "n": b64url(&public.n().to_bytes_be()), "e": b64url(&public.e().to_bytes_be()), }] }); let discovery = json!({ "issuer": url, "jwks_uri": format!("{url}/.well-known/jwks.json"), }); let app = axum::Router::new() .route( "/.well-known/openid-configuration", axum::routing::get({ let doc = discovery.clone(); move || { let doc = doc.clone(); async move { axum::Json(doc) } } }), ) .route( "/.well-known/jwks.json", axum::routing::get({ let doc = jwks.clone(); move || { let doc = doc.clone(); async move { axum::Json(doc) } } }), ); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); let pem = key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF).unwrap(); let imposter_pem = imposter.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF).unwrap(); Issuer { url, encoding_key: EncodingKey::from_rsa_pem(pem.as_bytes()).unwrap(), other_key: EncodingKey::from_rsa_pem(imposter_pem.as_bytes()).unwrap(), } } /// A Clerk-shaped session token: iss, sub `user_…`, short exp, plus the /// custom claims our docs tell operators to add (email, role). fn token( issuer: &Issuer, key: &EncodingKey, sub: &str, email: &str, role: &str, exp_offset: i64, ) -> String { let now = time::OffsetDateTime::now_utc().unix_timestamp(); let mut header = Header::new(Algorithm::RS256); header.kid = Some("tc-test-key".into()); encode( &header, &json!({ "iss": issuer.url, "sub": sub, "iat": now, "exp": now + exp_offset, "email": email, "role": role, }), key, ) .unwrap() } #[tokio::test] async fn clerk_tokens_authenticate_with_jit_provisioning_and_role_mapping() { let pool = cm_testkit::test_pool().await; let issuer = spawn_issuer().await; let ws = cm_domain::Workspace { id: cm_domain::WorkspaceId::new(), name: "Acme".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); let verifier = JwtVerifier::discover(&issuer.url).await.expect("discovery"); let auth = AuthService::new(pool.clone()).with_verifier(Arc::new(verifier)); // An org admin signs in for the first time: JIT-provisioned as Owner. let admin = token( &issuer, &issuer.encoding_key, "user_2adm", "ada@acme.test", "org:admin", 60, ); let authed = auth.authenticate(&admin).await.expect("admin verifies"); assert_eq!(authed.role, Role::Owner); assert_eq!(authed.workspace_id, ws.id); // Same subject again: the SAME user, not a duplicate. let again = auth.authenticate(&admin).await.unwrap(); assert_eq!(again.user_id, authed.user_id); let count: i64 = sqlx::query_scalar("SELECT count(*) FROM users WHERE auth_subject = 'user_2adm'") .fetch_one(&pool) .await .unwrap(); assert_eq!(count, 1); // A plain member maps to Member. let member = token( &issuer, &issuer.encoding_key, "user_2mem", "mel@acme.test", "org:member", 60, ); let authed = auth.authenticate(&member).await.unwrap(); assert_eq!(authed.role, Role::Member); // Expired tokens are refused. let expired = token( &issuer, &issuer.encoding_key, "user_2adm", "ada@acme.test", "org:admin", -10, ); assert!(matches!( auth.authenticate(&expired).await, Err(AuthError::Unauthenticated) )); // A token signed by a DIFFERENT key — even with perfect claims — fails. let forged = token( &issuer, &issuer.other_key, "user_2adm", "ada@acme.test", "org:admin", 60, ); assert!(matches!( auth.authenticate(&forged).await, Err(AuthError::Unauthenticated) )); // Garbage is refused without panicking. assert!(auth.authenticate("not.a.jwt").await.is_err()); assert!(auth.authenticate("xyz").await.is_err()); } #[tokio::test] async fn the_wrong_issuer_is_refused() { let pool = cm_testkit::test_pool().await; let real = spawn_issuer().await; let evil = spawn_issuer().await; let ws = cm_domain::Workspace { id: cm_domain::WorkspaceId::new(), name: "Acme".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); let verifier = JwtVerifier::discover(&real.url).await.unwrap(); let auth = AuthService::new(pool.clone()).with_verifier(Arc::new(verifier)); // Signed correctly by the EVIL issuer's own key, claiming its own iss: // the verifier pinned to the real issuer must refuse it. let foreign = token( &evil, &evil.encoding_key, "user_2evil", "e@vil.test", "org:admin", 60, ); assert!(matches!( auth.authenticate(&foreign).await, Err(AuthError::Unauthenticated) )); }