| Crates.io | hb-auth |
| lib.rs | hb-auth |
| version | 0.2.0 |
| created_at | 2025-11-19 00:43:48.260831+00 |
| updated_at | 2025-12-07 19:47:39.138731+00 |
| description | Identity and permissions for Cloudflare Workers. |
| homepage | |
| repository | https://github.com/saavylab/hb |
| max_upload_size | |
| id | 1939190 |
| size | 54,181 |
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.
User struct from requests.axum (via extractors) and raw worker::Request.[dependencies]
hb-auth = { version = "0.1", features = ["axum"] }
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.
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)
}
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())
}
If you enabled the kv feature, you need to:
In your wrangler.toml:
[[kv_namespaces]]
binding = "AUTH_CACHE"
id = "<your-kv-namespace-id>"
HasJwksCacheYour 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.
You can map Cloudflare Access Groups (available in the JWT groups claim) to your own internal 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
}
}
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")
}
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()))
}
To get the groups claim in your JWT:
The audience (AUD) is found in the Overview tab of your Access Application.
MIT