Authentication System
User authentication is handled through Clerk, with JWT verification performed at the edge using cached JWKS.
Key Files
crates/clerk-auth/src/verify.rs- JWT verificationcrates/clerk-auth/src/middleware.rs- Auth middlewarecrates/clerk-auth/src/config.rs- Clerk configurationcrates/clerk-auth/src/jwks.rs- JWKS fetching and caching
Authentication Flow
Mobile App / Web Client
│
│ Request with JWT
│ (Cookie: __session OR Authorization: Bearer {token})
│
▼
┌───────────────────────────────────────────────────────────────┐
│ bf_resident Worker │
├───────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ClerkAuthMiddleware │ │
│ │ │ │
│ │ 1. Extract token from cookie or header │ │
│ │ 2. Decode JWT header to get `kid` │ │
│ │ 3. Fetch JWKS from Clerk (cached) │ │
│ │ 4. Find key matching `kid` │ │
│ │ 5. Verify RS256 signature │ │
│ │ 6. Validate claims (exp, iat, sub, azp) │ │
│ │ 7. Add ClerkAuth to request extensions │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Role Middleware │ │
│ │ │ │
│ │ • require_admin() │ │
│ │ • require_property_manager() │ │
│ │ • require_installer() │ │
│ │ • require_tenant() │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Handler │
│ │
└───────────────────────────────────────────────────────────────┘
Token Extraction
Tokens can be provided via cookie or header:
// crates/clerk-auth/src/middleware.rs
fn extract_token(request: &Request) -> Option<String> {
// First try cookie
if let Some(token) = get_cookie(request, "__session") {
return Some(token.to_string());
}
// Then try Authorization header
if let Some(header) = get_header(request, "Authorization") {
if header.starts_with("Bearer ") {
return Some(header.replace("Bearer ", ""));
}
}
None
}
JWT Verification
ClerkVerifier
// crates/clerk-auth/src/verify.rs
pub struct ClerkVerifier {
config: ClerkConfig,
jwks_fetcher: JwksFetcher,
}
impl ClerkVerifier {
pub async fn verify(&self, token: &str) -> Result<ClerkClaims, VerificationError> {
// 1. Decode header to get key ID
let header = decode_header(token)?;
let kid = header.kid.ok_or(VerificationError::MissingKid)?;
// 2. Fetch JWKS (cached)
let jwks = self.jwks_fetcher.fetch(&self.config.jwks_url).await?;
// 3. Find matching key
let jwk = match jwks.find_key(&kid) {
Some(key) => key,
None => {
// Key rotation: fetch fresh and retry
let fresh_jwks = self.jwks_fetcher.fetch_fresh(&self.config.jwks_url).await?;
fresh_jwks.find_key(&kid)
.ok_or(VerificationError::KeyNotFound(kid))?
}
};
// 4. Build validation parameters
let mut validation = Validation::new(Algorithm::RS256);
validation.leeway = self.config.clock_skew_seconds;
validation.validate_exp = true;
validation.validate_nbf = self.config.require_nbf;
validation.set_required_spec_claims(&["exp", "sub", "iat"]);
if let Some(ref audience) = self.config.audience {
validation.set_audience(&[audience]);
}
// 5. Decode and verify
let decoding_key = DecodingKey::from_jwk(&jwk)?;
let token_data = decode::<ClerkClaims>(token, &decoding_key, &validation)?;
// 6. Validate authorized party (azp)
if let Some(ref authorized_parties) = self.config.authorized_parties {
if let Some(ref azp) = token_data.claims.azp {
if !authorized_parties.contains(azp) {
return Err(VerificationError::UnauthorizedParty(azp.clone()));
}
}
}
Ok(token_data.claims)
}
}
JWKS Caching
// crates/clerk-auth/src/jwks.rs
pub const DEFAULT_JWKS_CACHE_TTL: u64 = 300; // 5 minutes
pub struct JwksFetcher {
cache: KvCache, // Cloudflare KV
}
impl JwksFetcher {
pub async fn fetch(&self, url: &str) -> Result<JwkSet, JwksError> {
// Check cache first
if let Some(cached) = self.cache.get(url).await? {
return Ok(cached);
}
// Fetch from Clerk
let jwks = self.fetch_fresh(url).await?;
// Cache for 5 minutes
self.cache.put(url, &jwks, DEFAULT_JWKS_CACHE_TTL).await?;
Ok(jwks)
}
pub async fn fetch_fresh(&self, url: &str) -> Result<JwkSet, JwksError> {
let response = reqwest::get(url).await?;
let jwks: JwkSet = response.json().await?;
// Update cache
self.cache.put(url, &jwks, DEFAULT_JWKS_CACHE_TTL).await?;
Ok(jwks)
}
}
ClerkAuth Claims
// crates/clerk-auth/src/claims.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClerkClaims {
pub sub: String, // User ID
pub iat: u64, // Issued at
pub exp: u64, // Expiration
pub nbf: Option<u64>, // Not before
pub iss: Option<String>, // Issuer
pub azp: Option<String>, // Authorized party
// Organization membership
pub org_id: Option<String>,
pub org_role: Option<String>,
pub org_slug: Option<String>,
// Custom metadata
#[serde(default)]
pub public_metadata: serde_json::Value,
#[serde(default)]
pub private_metadata: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct ClerkAuth {
claims: ClerkClaims,
}
impl ClerkAuth {
pub fn user_id(&self) -> &str {
&self.claims.sub
}
pub fn org_id(&self) -> Option<&str> {
self.claims.org_id.as_deref()
}
pub fn org_role(&self) -> Option<&str> {
self.claims.org_role.as_deref()
}
pub fn has_role(&self, role: &str) -> bool {
self.claims.org_role.as_ref()
.map(|r| r == role)
.unwrap_or(false)
}
}
Configuration
From Environment
// crates/clerk-auth/src/config.rs
pub struct ClerkConfig {
pub jwks_url: String,
pub audience: Option<String>,
pub authorized_parties: Option<Vec<String>>,
pub clock_skew_seconds: u64,
pub require_nbf: bool,
}
impl ClerkConfig {
pub fn from_publishable_key(key: &str) -> Result<Self, ConfigError> {
// Extract instance from pk_test_XXXX or pk_live_XXXX
// pk_test_Y2xlcmsuZXhhbXBsZS5jb20k decodes to clerk.accounts.dev
let instance = extract_instance_from_key(key)?;
let jwks_url = format!("https://{}/.well-known/jwks.json", instance);
Ok(Self {
jwks_url,
audience: None,
authorized_parties: None,
clock_skew_seconds: 60,
require_nbf: false,
})
}
}
Building JWKS URL
The JWKS URL is derived from the Clerk publishable key:
Publishable Key: pk_test_Y2xlcmsuYWNjb3VudHMuZGV2JA==
└─────── Base64 ───────┘
Decoded: clerk.accounts.dev$
JWKS URL: https://clerk.accounts.dev/.well-known/jwks.json
Auth Middleware
ClerkAuthMiddleware
// crates/clerk-auth/src/middleware.rs
pub struct ClerkAuthMiddleware {
verifier: ClerkVerifier,
}
impl<S> Layer<S> for ClerkAuthMiddleware {
type Service = ClerkAuthService<S>;
fn layer(&self, inner: S) -> Self::Service {
ClerkAuthService {
inner,
verifier: self.verifier.clone(),
}
}
}
impl<S, B> Service<Request<B>> for ClerkAuthService<S>
where
S: Service<Request<B>, Response = Response>,
{
async fn call(&self, mut request: Request<B>) -> Self::Response {
// Extract token
let token = match extract_token(&request) {
Some(t) => t,
None => return unauthorized("Missing authentication token"),
};
// Verify token
let claims = match self.verifier.verify(&token).await {
Ok(c) => c,
Err(e) => return unauthorized(&format!("Invalid token: {}", e)),
};
// Add claims to request extensions
let auth = ClerkAuth::new(claims);
request.extensions_mut().insert(auth);
// Continue to handler
self.inner.call(request).await
}
}
Extracting Auth in Handlers
// In route handlers
pub async fn get_property(
Extension(auth): Extension<ClerkAuth>,
State(env): State<Env>,
Path(property_id): Path<String>,
) -> Result<Json<Property>> {
// auth.user_id() - the authenticated user
// auth.org_role() - their organization role
let user_email = auth.user_id();
// ...
}
Error Responses
Unauthorized (401)
{
"error": "unauthorized",
"message": "Missing authentication token"
}
Forbidden (403)
{
"error": "forbidden",
"message": "Insufficient permissions"
}
Key Rotation Handling
Clerk rotates signing keys periodically. The system handles this gracefully:
- JWT arrives with unknown
kid - Cached JWKS doesn't contain the key
- System fetches fresh JWKS from Clerk
- New key is found and used for verification
- Cache is updated with new JWKS
let jwk = match jwks.find_key(&kid) {
Some(key) => key,
None => {
// Key not in cache - rotation likely occurred
console_log!("Key {} not found, fetching fresh JWKS", kid);
let fresh_jwks = self.jwks_fetcher.fetch_fresh(url).await?;
fresh_jwks.find_key(&kid)
.ok_or(VerificationError::KeyNotFound(kid))?
}
};
See Also
- service-to-service-auth.md - Internal service authentication
- permissions.md - Role-based access control