authzoo is a Rust library for authorizing JWT-bearing callers through
configurable roles.
The central idea is similar to AWS IAM roles: a caller presents a JWT, and if that token satisfies the trust criteria for a role, the caller is considered to have assumed that role. Your application can then attach those roles to whatever it protects: upload permissions, internal API routes, deployment actions, token exchange mappings, or anything else with an authorization decision.
authzoo handles the reusable JWT parts:
- parsing role configuration from TOML-friendly Serde types
- validating issuer, audience, signature, and expiry
- resolving validation keys from a static key or OIDC/JWKS discovery
- matching token claims against role requirements
Your application still owns its own permission model. authzoo answers the
question: "Which roles can this token assume?"
A role describes which JWTs are trusted to assume it.
[[role]]
name = "release-pipeline"
issuer = "https://ci.example.com"
audience = "artifact-service"
algorithms = ["RS256"]
[role.claims]
organization = "example-org"
pipeline = "release"
sub = { any-of = ["builder", "release-bot"] }This role can be assumed by a token that:
- is issued by
https://ci.example.com - has audience
artifact-service - is signed with
RS256 - has
organization = "example-org" - has
pipeline = "release" - has
subequal to eitherbuilderorrelease-bot
Claim requirements may be written as exact string matches:
[role.claims]
environment = "production"Or as one-of matches:
[role.claims]
sub = { any-of = ["service-a", "service-b"] }Or, for a claim whose value is an array, as a membership match:
[role.claims]
groups = { contains = "release-approvers" }This matches a token whose groups claim is an array with an element exactly
equal to release-approvers. An element that merely contains that text does not
match, and a string-valued groups claim is not treated as a one-element array.
The other forms only ever match a string-valued claim.
If validation-key is omitted, authzoo discovers signing keys using OpenID
Connect discovery:
- Fetch
<issuer>/.well-known/openid-configuration. - Read the
jwks_urifield. - Fetch the JWKS document.
- Select a compatible key for the JWT header.
[[role]]
name = "cluster-workload"
issuer = "https://kubernetes.default.svc"
audience = "internal-api"
algorithms = ["RS256"]
[role.claims]
sub = "system:serviceaccount:default:worker"For Kubernetes service issuers, authzoo also uses the in-cluster service
account CA and bearer token when they are available.
For local testing or shared-secret issuers, configure a static validation key:
[[role]]
name = "local-ci"
issuer = "https://issuer.example"
audience = "artifact-service"
validation-key = "shared-secret"
algorithms = ["HS256"]Each protected resource declares the roles that are allowed to access it.
[[protected-action]]
name = "publish-release"
allowed-roles = ["release-pipeline", "release-admin"]At runtime, build a token validator from configuration, ask which roles the caller's JWT can assume, and intersect that list with the resource's allowed roles.
let validator = authzoo::TokenValidator::try_from(config.authzoo)?;
let assumed = validator.validate(bearer_token);
let Some(role) = action
.allowed_roles
.iter()
.find(|allowed| assumed.iter().any(|r| r == *allowed))
else {
return Err(Forbidden);
};
tracing::info!(role = %role, "request authorized");validate returns every configured role whose issuer, audience, signature,
and claim requirements are satisfied by the token. If no role matches, the
returned list is empty.
The main types are:
Config: a TOML-friendly wrapper containingrolesRoleConfig: issuer, audience, algorithms, validation key, and required claimsClaimRequirement: exact string or one-of string matchingTokenValidator: returns the roles a token can assume
authzoo intentionally does not decide what a role means inside your
application. It only validates that a caller can assume a role. Your application
decides what release-pipeline, cluster-workload, or admin is allowed to
do.
MIT