Skip to main content

Service-to-Service Authentication

Internal worker-to-worker communication uses HMAC-SHA256 signed JWT tokens for secure authentication.

Key Files

  • crates/clerk-auth/src/service_auth.rs - Token creation and verification
  • crates/clerk-auth/src/middleware.rs - ServiceAuthMiddleware
  • crates/bf_types/src/mesh/client.rs - Typed service clients

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│ bf_notify Worker │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Received SmartThings webhook, need to lookup property_id │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ ServiceAuthenticator │ │
│ │ │ │
│ │ let token = authenticator.create_token("bf_notify")?; │ │
│ │ │ │
│ │ Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... │ │
│ │ Claims: { service: "bf_notify", iat, exp, jti } │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

│ POST /service/lookup
│ Authorization: Bearer {token}
│ X-Service-Name: bf_notify

┌─────────────────────────────────────────────────────────────────────────┐
│ bf_resident Worker │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ ServiceAuthMiddleware │ │
│ │ │ │
│ │ 1. Extract token from Authorization header │ │
│ │ 2. Verify HMAC-SHA256 signature (SERVICE_AUTH_SECRET) │ │
│ │ 3. Validate expiration (5 minute TTL) │ │
│ │ 4. Check service name is in allowed list │ │
│ │ 5. Add ServiceAuth to request extensions │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Token Format

JWT Claims

// crates/clerk-auth/src/service_auth.rs

#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceClaims {
pub service: String, // Calling service name
pub iat: u64, // Issued at (Unix timestamp)
pub exp: u64, // Expiration (iat + 300 seconds)
pub jti: String, // JWT ID (request ID for replay protection)
}

Example Token Payload

{
"service": "bf_notify",
"iat": 1705123456,
"exp": 1705123756,
"jti": "req_a1b2c3d4e5f6"
}

Security Properties

PropertyValuePurpose
AlgorithmHS256HMAC-SHA256 signing
TTL5 minutesLimits blast radius if compromised
JTIUUIDEnables replay detection
SecretSERVICE_AUTH_SECRETShared secret across workers

Valid Service Names

pub const SERVICE_NAMES: &[&str] = &[
"bf_resident",
"bf_notify",
"bf_smart",
"bf_storage",
"bf_user",
"bf_email_forwarder",
"bf_mobile",
"dev", // Development only
];

ServiceAuthenticator

Creating Tokens

// crates/clerk-auth/src/service_auth.rs

pub struct ServiceAuthenticator {
secret: String,
}

impl ServiceAuthenticator {
pub fn new(secret: String) -> Self {
Self { secret }
}

pub fn create_token(&self, service_name: &str) -> Result<String, ServiceAuthError> {
// Validate service name
if !SERVICE_NAMES.contains(&service_name) {
return Err(ServiceAuthError::InvalidServiceName(service_name.to_string()));
}

let now = current_timestamp_secs()?;
let claims = ServiceClaims {
service: service_name.to_string(),
iat: now,
exp: now + 300, // 5 minutes
jti: Self::generate_request_id(),
};

let token = encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret(self.secret.as_bytes()),
)?;

Ok(token)
}

fn generate_request_id() -> String {
format!("req_{}", uuid::Uuid::new_v4().to_string().replace("-", "")[..12])
}
}

Verifying Tokens

impl ServiceAuthenticator {
pub fn verify_token(&self, token: &str) -> Result<ServiceClaims, ServiceAuthError> {
let mut validation = Validation::new(Algorithm::HS256);
validation.validate_exp = true;
validation.set_required_spec_claims(&["service", "iat", "exp", "jti"]);

let token_data = decode::<ServiceClaims>(
token,
&DecodingKey::from_secret(self.secret.as_bytes()),
&validation,
)?;

let claims = token_data.claims;

// Verify service name is valid
if !SERVICE_NAMES.contains(&claims.service.as_str()) {
return Err(ServiceAuthError::InvalidServiceName(claims.service));
}

Ok(claims)
}
}

ServiceAuthMiddleware

// crates/clerk-auth/src/middleware.rs

pub struct ServiceAuthMiddleware {
authenticator: ServiceAuthenticator,
}

impl<S, B> Service<Request<B>> for ServiceAuthService<S> {
async fn call(&self, mut request: Request<B>) -> Self::Response {
// Extract token from header
let token = match get_header(&request, "Authorization") {
Some(h) if h.starts_with("Bearer ") => h.replace("Bearer ", ""),
Some(h) if h.starts_with("X-Service-Auth ") => h.replace("X-Service-Auth ", ""),
_ => return unauthorized("Missing service authentication"),
};

// Verify token
let claims = match self.authenticator.verify_token(&token) {
Ok(c) => c,
Err(e) => return unauthorized(&format!("Invalid service token: {}", e)),
};

// Add to request extensions
let auth = ServiceAuth(claims);
request.extensions_mut().insert(auth);

self.inner.call(request).await
}
}

Mesh Client Pattern

Type-safe service-to-service calls with automatic token management.

Mesh Client

// crates/bf_types/src/mesh/client.rs

pub struct Mesh {
service_name: String,
authenticator: Arc<ServiceAuthenticator>,
urls: ServiceUrls,
client: reqwest::Client,
}

pub struct ServiceUrls {
pub resident: String,
pub smart: String,
pub notify: String,
}

impl Mesh {
pub fn new(service_name: &str, env: &Env) -> Result<Self, MeshError> {
let secret = env.var("SERVICE_AUTH_SECRET")?.to_string();
let authenticator = ServiceAuthenticator::new(secret);

let urls = ServiceUrls {
resident: env.var("BF_RESIDENT_URL")?.to_string(),
smart: env.var("BF_SMART_URL")?.to_string(),
notify: env.var("BF_NOTIFY_URL")?.to_string(),
};

Ok(Self {
service_name: service_name.to_string(),
authenticator: Arc::new(authenticator),
urls,
client: reqwest::Client::new(),
})
}

async fn request<R: DeserializeOwned>(
&self,
method: Method,
url: &str,
body: Option<&impl Serialize>,
) -> Result<R, MeshError> {
// Create fresh token for each request
let token = self.authenticator.create_token(&self.service_name)?;

let mut request = self.client
.request(method, url)
.header("Authorization", format!("Bearer {}", token))
.header("X-Service-Name", &self.service_name);

if let Some(body) = body {
request = request.json(body);
}

let response = request.send().await?;

if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(MeshError::RequestFailed { status, body: text });
}

response.json().await.map_err(MeshError::from)
}
}

Typed Service Clients

// Resident service operations
pub struct ResidentClient<'a> {
mesh: &'a Mesh,
}

impl<'a> ResidentClient<'a> {
pub async fn lookup_by_smartthings_ids(
&self,
request: &LookupBySmartthingsIdsRequest,
) -> Result<LookupByExternalIdResponse, MeshError> {
let url = format!("{}/service/lookup/smartthings", self.mesh.urls.resident);
self.mesh.post(&url, request).await
}

pub async fn lookup_by_yale_id(
&self,
request: &LookupByYaleIdRequest,
) -> Result<LookupByExternalIdResponse, MeshError> {
let url = format!("{}/service/lookup/yale", self.mesh.urls.resident);
self.mesh.post(&url, request).await
}

pub async fn sync_property_to_do(
&self,
property_id: &str,
) -> Result<(), MeshError> {
let url = format!("{}/service/sync/{}", self.mesh.urls.resident, property_id);
self.mesh.post_empty(&url, &()).await
}
}

// Smart service operations
pub struct SmartClient<'a> {
mesh: &'a Mesh,
}

impl<'a> SmartClient<'a> {
pub async fn execute_capability(
&self,
request: &ExecuteCapabilityRequest,
) -> Result<DeviceState, MeshError> {
let url = format!("{}/service/execute", self.mesh.urls.smart);
self.mesh.post(&url, request).await
}
}

Usage Example

// In bf_notify webhook handler

async fn handle_smartthings_webhook(req: Request, env: Env) -> Result<Response> {
let event: SmartThingsEvent = req.json().await?;

// Create mesh client
let mesh = Mesh::new("bf_notify", &env)?;

// Lookup property from SmartThings IDs
let lookup_result = mesh.resident()
.lookup_by_smartthings_ids(&LookupBySmartthingsIdsRequest {
smartthings_property_id: event.location_id.clone(),
smartthings_device_id: event.device_id.clone(),
})
.await?;

// Queue event for processing
let queued_event = QueuedSmartEvent {
property_id: lookup_result.property_id,
device_id: lookup_result.device_id,
event: event.into(),
};

env.queue("SMART_EVENTS")?.send(&queued_event).await?;

Response::ok("OK")
}

Error Handling

ServiceAuthError

#[derive(Debug, thiserror::Error)]
pub enum ServiceAuthError {
#[error("Invalid service name: {0}")]
InvalidServiceName(String),

#[error("Token expired")]
TokenExpired,

#[error("Invalid signature")]
InvalidSignature,

#[error("Missing required claim: {0}")]
MissingClaim(String),

#[error("JWT error: {0}")]
JwtError(#[from] jsonwebtoken::errors::Error),
}

HTTP Error Responses

401 Unauthorized:

{
"error": "unauthorized",
"message": "Missing service authentication"
}

403 Forbidden:

{
"error": "forbidden",
"message": "Invalid service name: unknown_service"
}

Security Considerations

Short Token Lifetime

5-minute expiration limits the impact of token compromise:

  • Intercepted tokens become useless quickly
  • No need for token revocation infrastructure
  • Fresh token created for each request

Request ID (JTI)

Each token has a unique JTI for:

  • Request tracing across services
  • Potential replay detection (if implemented)
  • Debugging and logging correlation

Shared Secret Management

SERVICE_AUTH_SECRET must be:

  • Same value across all workers
  • Stored in Cloudflare Secrets Manager
  • Rotated periodically (requires coordinated deployment)

See Also