Files
clawmates/crates/cm-auth/tests/live_clerk.rs
T
Omar SobhandClaude Fable 5 b9fdec9173 Clerk deployment smoke: validated against a real instance, both halves
Last open item from the roadmap + post-1.0 list. Run against the live
Clerk instance closing-seasnail-39.clerk.accounts.dev.

- Backend (crates/cm-auth/tests/live_clerk.rs, CM_LIVE_CLERK=1): pulls
  REAL discovery + JWKS from the live instance, mints a REAL session JWT
  via Clerk's Backend API (create user -> open session -> session token),
  and runs it through AuthService::authenticate — verify + JIT provision
  (keyed on the real sub), duplicate-subject suppression, tamper
  rejection against the live JWKS. Decodes the instance domain from the
  publishable key; cleans up the test user after. PASSING
- Frontend: built with AUTH_MODE=clerk + real keys, next start serves
  Clerk's <SignIn /> at /login wired to the instance (instance domain +
  data-clerk attributes present in the HTML). Both halves confirmed
  end to end against production Clerk
- docs/clerk.md: documented the smoke procedure for both halves

166 Rust tests (+6 live, key-gated). Keys used via env only, never
stored — rotate them (they passed through chat).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:39:26 -05:00

134 lines
4.5 KiB
Rust

//! Deployment smoke against a REAL Clerk instance (CM_LIVE_CLERK=1 +
//! CLERK_SECRET_KEY + CLERK_PUBLISHABLE_KEY): discovery and JWKS come
//! from Clerk's live infrastructure, the session JWT is minted by
//! Clerk's Backend API for a real user, and `AuthService::authenticate`
//! verifies it and JIT-provisions — the exact production path.
use std::sync::Arc;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use cm_auth::{AuthService, JwtVerifier};
use cm_domain::{Role, Workspace, WorkspaceId};
use serde_json::{json, Value};
const BAPI: &str = "https://api.clerk.com/v1";
fn live() -> Option<(String, String)> {
if std::env::var("CM_LIVE_CLERK").as_deref() != Ok("1") {
eprintln!("skipped: set CM_LIVE_CLERK=1 to run");
return None;
}
let (Ok(secret), Ok(publishable)) = (
std::env::var("CLERK_SECRET_KEY"),
std::env::var("CLERK_PUBLISHABLE_KEY"),
) else {
eprintln!("skipped: CLERK_SECRET_KEY / CLERK_PUBLISHABLE_KEY not set");
return None;
};
// The publishable key embeds the instance domain:
// pk_test_<base64("<slug>.clerk.accounts.dev$")>.
let encoded = publishable
.trim_start_matches("pk_test_")
.trim_start_matches("pk_live_");
let domain = STANDARD
.decode(encoded)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
.map(|s| s.trim_end_matches('$').to_owned())
.expect("publishable key decodes to the instance domain");
Some((secret, format!("https://{domain}")))
}
#[tokio::test]
async fn a_real_clerk_session_token_authenticates_and_provisions() {
let Some((secret, issuer)) = live() else {
return;
};
let pool = cm_testkit::test_pool().await;
let ws = Workspace {
id: WorkspaceId::new(),
name: "Acme".into(),
plan: "team".into(),
};
cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
// Real discovery + JWKS from the live instance.
let verifier = JwtVerifier::discover(&issuer)
.await
.expect("clerk discovery");
let auth = AuthService::new(pool.clone()).with_verifier(Arc::new(verifier));
// Mint a REAL session token via Clerk's Backend API.
let client = reqwest::Client::new();
let email = format!("smoke+{}@clawmates.work", uuid::Uuid::now_v7().simple());
let user: Value = client
.post(format!("{BAPI}/users"))
.bearer_auth(&secret)
.json(&json!({
"email_address": [email],
"skip_password_requirement": true,
"first_name": "Smoke",
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let user_id = user["id"].as_str().expect("user created").to_owned();
let session: Value = client
.post(format!("{BAPI}/sessions"))
.bearer_auth(&secret)
.json(&json!({"user_id": user_id}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let session_id = session["id"].as_str().expect("session created");
let token: Value = client
.post(format!("{BAPI}/sessions/{session_id}/tokens"))
.bearer_auth(&secret)
.json(&json!({}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let jwt = token["jwt"].as_str().expect("session jwt minted");
// THE smoke: the production verify path accepts Clerk's real JWT and
// JIT-provisions the user (no custom claims configured on a fresh
// instance, so email synthesizes and role defaults to Member).
let authed = auth.authenticate(jwt).await.expect("real token verifies");
assert_eq!(authed.workspace_id, ws.id);
assert_eq!(authed.role, Role::Member);
let subject: String = sqlx::query_scalar("SELECT auth_subject FROM users WHERE id = $1")
.bind(authed.user_id.as_uuid())
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(subject, user_id);
// Same token again: same user, no duplicate.
let again = auth.authenticate(jwt).await.unwrap();
assert_eq!(again.user_id, authed.user_id);
// A tampered token is refused even with the real JWKS.
let mut forged = jwt.to_owned();
forged.replace_range(forged.len() - 4.., "AAAA");
assert!(auth.authenticate(&forged).await.is_err());
// Clean the instance up.
client
.delete(format!("{BAPI}/users/{user_id}"))
.bearer_auth(&secret)
.send()
.await
.unwrap();
}