Skip to main content

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 verification
  • crates/clerk-auth/src/middleware.rs - Auth middleware
  • crates/clerk-auth/src/config.rs - Clerk configuration
  • crates/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:

  1. JWT arrives with unknown kid
  2. Cached JWKS doesn't contain the key
  3. System fetches fresh JWKS from Clerk
  4. New key is found and used for verification
  5. 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