hb-auth

Crates.iohb-auth
lib.rshb-auth
version0.2.0
created_at2025-11-19 00:43:48.260831+00
updated_at2025-12-07 19:47:39.138731+00
descriptionIdentity and permissions for Cloudflare Workers.
homepage
repositoryhttps://github.com/saavylab/hb
max_upload_size
id1939190
size54,181
(jayson-saavylab)

documentation

README

hb-auth

Identity and permissions for Cloudflare Workers.

hb-auth provides drop-in Cloudflare Access JWT validation with a strongly-typed permission DSL. It handles key rotation, signature verification, and identity extraction so you can focus on your business logic.

Part of the hb stack.


Features

  • Zero-Config Validation – Automatically fetches and caches JWKS from your Cloudflare Access team domain.
  • Type-Safe Identity – Extract a strongly-typed User struct from requests.
  • Role-Based Access – Map Access Groups (e.g., "super-admins") to internal Rust enums automatically.
  • Framework Agnostic – First-class support for axum (via extractors) and raw worker::Request.
  • Optional KV Caching – Opt-in cross-isolate JWKS caching via Cloudflare KV.

Installation

[dependencies]
hb-auth = { version = "0.1", features = ["axum"] }

Optional: KV-backed JWKS Caching

By default, hb-auth caches JWKS keys in memory. Since Workers isolates are ephemeral, this cache is lost on restart. Enable the kv feature for cross-isolate caching:

[dependencies]
hb-auth = { version = "0.1", features = ["axum", "kv"] }

This stores JWKS keys in Cloudflare KV with a 10-minute TTL, so all isolates share the same cached keys.


Quick Start (Axum)

1. Configure

In your router setup, initialize the config and add it to your state. Your state must implement HasAuthConfig.

use hb_auth::{AuthConfig, HasAuthConfig};

#[derive(Clone)]
struct AppState {
    auth_config: AuthConfig,
    // ... other state
}

impl HasAuthConfig for AppState {
    fn auth_config(&self) -> &AuthConfig {
        &self.auth_config
    }
}

fn router(env: Env) -> Router {
    let auth_config = AuthConfig::new(
        "https://my-team.cloudflareaccess.com",
        env.var("ACCESS_AUD").unwrap().to_string()
    );
    
    let state = AppState { auth_config };
    
    Router::new()
        .route("/secure", get(handler))
        .with_state(state)
}

2. Protect Routes

Add auth: User to your handler arguments. The handler will only run if a valid Cloudflare Access JWT is present.

use hb_auth::User;

async fn handler(auth: User) -> &'static str {
    format!("Hello, {}!", auth.email())
}

Setting Up KV Caching

If you enabled the kv feature, you need to:

1. Add a KV Namespace

In your wrangler.toml:

[[kv_namespaces]]
binding = "AUTH_CACHE"
id = "<your-kv-namespace-id>"

2. Implement HasJwksCache

Your state must implement HasJwksCache to provide the KV binding:

use hb_auth::{AuthConfig, HasAuthConfig, HasJwksCache};

#[derive(Clone)]
struct AppState {
    env: SendWrapper<Env>,
    auth_config: AuthConfig,
}

impl HasAuthConfig for AppState {
    fn auth_config(&self) -> &AuthConfig {
        &self.auth_config
    }
}

impl HasJwksCache for AppState {
    fn jwks_kv(&self) -> Option<worker::kv::KvStore> {
        self.env.kv("AUTH_CACHE").ok()
    }
}

The extractor automatically uses KV when available, falling back to in-memory caching if jwks_kv() returns None.


Advanced: Role-Based Access

You can map Cloudflare Access Groups (available in the JWT groups claim) to your own internal roles.

1. Define Roles

use hb_auth::{RoleMapper, Claims};

#[derive(Debug, PartialEq, Clone)]
pub enum Role {
    Admin,
    Editor,
    Viewer,
}

impl RoleMapper for Role {
    fn from_claims(claims: &Claims) -> Vec<Self> {
        let mut roles = vec![];
        
        // Map groups from Cloudflare Access
        for group in &claims.groups {
            match group.as_str() {
                "000-my-app-admins" => roles.push(Role::Admin),
                "000-my-app-editors" => roles.push(Role::Editor),
                _ => {}
            }
        }
        
        // Or map specific emails
        if claims.email.ends_with("@saavylab.com") {
            roles.push(Role::Viewer);
        }
        
        roles
    }
}

2. Enforce Permissions

Use User<Role> instead of the default User.

async fn delete_db(auth: User<Role>) -> Result<impl IntoResponse, StatusCode> {
    if !auth.has_role(Role::Admin) {
        return Err(StatusCode::FORBIDDEN);
    }
    
    // ... unsafe logic
    Ok("Deleted")
}

Usage with Raw Workers

If you aren't using Axum, you can still use hb-auth to validate requests.

use hb_auth::User;

async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    let config = AuthConfig::new(/* ... */);
    
    let user = match User::<()>::from_worker_request(&req, &config).await {
        Ok(u) => u,
        Err(e) => return Response::error(e, 401),
    };
    
    Response::ok(format!("Welcome {}", user.email()))
}

With KV caching enabled:

use hb_auth::User;

async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    let config = AuthConfig::new(/* ... */);
    let kv = env.kv("AUTH_CACHE")?;
    
    let user = match User::<()>::from_worker_request_cached(&req, &config, &kv).await {
        Ok(u) => u,
        Err(e) => return Response::error(e, 401),
    };
    
    Response::ok(format!("Welcome {}", user.email()))
}

Configuring Cloudflare Access

To get the groups claim in your JWT:

  1. Go to Zero Trust Dashboard > Access > Applications.
  2. Edit your application.
  3. Under Settings (or "Overview" -> "Edit"), find OIDC Claims or Additional Settings.
  4. Enable Groups (this might require adding "groups" to the scope depending on your configuration).
  5. Ensure the groups you want to map are assigned to the application policy.

The audience (AUD) is found in the Overview tab of your Access Application.


License

MIT

Commit count: 0

cargo fmt