//! The full stack accepts Clerk session JWTs: a real RSA issuer, a real //! router, and `Authorization: Bearer ` straight into a //! protected endpoint — the exact shape `@clerk/nextjs`'s getToken() //! produces. use std::sync::Arc; use cm_api::AppState; use cm_auth::JwtVerifier; use cm_domain::{Workspace, WorkspaceId}; use cm_llm::ScriptedProvider; use cm_runtime::{Runtime, RuntimeConfig}; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use rsa::pkcs1::EncodeRsaPrivateKey; use rsa::traits::PublicKeyParts; use rsa::RsaPrivateKey; use serde_json::{json, Value}; fn b64url(bytes: &[u8]) -> String { use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; URL_SAFE_NO_PAD.encode(bytes) } #[tokio::test] async fn a_clerk_session_jwt_reaches_protected_endpoints() { let pool = cm_testkit::test_pool().await; // Real issuer with a real key. let mut rng = rand_core::OsRng; let key = RsaPrivateKey::new(&mut rng, 2048).unwrap(); let public = key.to_public_key(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let issuer = format!("http://{}", listener.local_addr().unwrap()); let jwks = json!({"keys": [{ "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "k1", "n": b64url(&public.n().to_bytes_be()), "e": b64url(&public.e().to_bytes_be()), }]}); let discovery = json!({ "issuer": issuer, "jwks_uri": format!("{issuer}/.well-known/jwks.json"), }); let idp = 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, idp).await.unwrap(); }); let ws = Workspace { id: WorkspaceId::new(), name: "Acme".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); // The app, Clerk-configured. let runtime = Runtime::new( pool.clone(), Arc::new(ScriptedProvider::from_toml("").unwrap()), RuntimeConfig::basic("scripted", 1024), ); let verifier = JwtVerifier::discover(&issuer).await.unwrap(); let app = cm_api::router(AppState::new(pool.clone(), runtime).with_auth_verifier(Arc::new(verifier))); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let base = format!("http://{}", listener.local_addr().unwrap()); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); // What getToken() returns in the browser. let now = time::OffsetDateTime::now_utc().unix_timestamp(); let mut header = Header::new(Algorithm::RS256); header.kid = Some("k1".into()); let pem = key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF).unwrap(); let session_jwt = encode( &header, &json!({ "iss": issuer, "sub": "user_2clerk", "iat": now, "exp": now + 60, "email": "casey@acme.test", "role": "org:admin", }), &EncodingKey::from_rsa_pem(pem.as_bytes()).unwrap(), ) .unwrap(); let client = reqwest::Client::new(); let me: Value = client .get(format!("{base}/api/user/me")) .bearer_auth(&session_jwt) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(me["email"], "casey@acme.test"); assert_eq!(me["role"], "owner"); // No token still means no entry. let anonymous = client .get(format!("{base}/api/user/me")) .send() .await .unwrap(); assert_eq!(anonymous.status(), 401); }