hb-auth 0.2.0

Identity and permissions for Cloudflare Workers.
Documentation
# 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

```toml
[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:

```toml
[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`.

```rust
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.

```rust
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`:

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

### 2. Implement `HasJwksCache`

Your state must implement `HasJwksCache` to provide the KV binding:

```rust
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

```rust
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`.

```rust
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.

```rust
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:

```rust
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