This repository is the umbrella for the auth stack's three-layer separation of concerns (authentication & token issuance / authorization decision / authorization enforcement), and provides stack-level architecture docs and cross-component E2E tests. Perimeter protection against invalid / revoked tokens is handled by the optional auth.proxy, which sits outside the three layers.
Lightweight auth platform for early-stage projects.
A complete authentication + authorization stack that works out of the box. Each component runs as a standalone HTTP service and can be individually replaced with an enterprise alternative (Keycloak, OPA, Cedar, Envoy, etc.) as requirements grow — no application code changes required.
| Component | Repository | Description |
|---|---|---|
| auth.provider | o3co/auth.provider | OAuth 2.0 provider — login, token issuance, introspection |
| auth.proxy | o3co/auth.proxy | Token validation + caching reverse proxy |
| auth.policy-verifier | o3co/auth.policy-verifier | No-DSL ABAC policy verifier with Collector pattern |
| protobuf.interceptors | o3co/protobuf.interceptors | gRPC authorization middleware (Go) |
OAuth 2.0 / OIDC provider. Issues JWTs from session login (local username/password, WebAuthn passkeys, Google / GitHub federation) or the authorization code flow with PKCE; machine and delegated access via client credentials, the device authorization grant (RFC 8628) and token exchange (RFC 8693); sender-constrained tokens via DPoP (RFC 9449) and mTLS-bound tokens (RFC 8705); introspection, revocation, RP-initiated and back-channel logout. Modular composition — use only the modules you need. JWT signing supports EdDSA (default), ES256, RS256 and HS256, with a JWKS endpoint for the asymmetric algorithms.
Token validation reverse proxy with introspection result caching. Sits between client and downstream service.
This component is optional. auth.policy-verifier and protobuf.interceptors validate JWT directly, so the system works without auth.proxy. Benefits of adding it:
- Introspection-based validation — detects revoked tokens immediately, unlike JWT-only local validation which relies on token expiry
- Caching — introspection results are cached (default 30s TTL), reducing load on auth.provider
- Centralized validation — downstream services receive pre-validated requests without implementing auth logic
No-DSL ABAC policy engine. Runs as an HTTP service (POST /verify) or embeds as a library. Authorization logic is composed in TypeScript via the Collector pattern, not a policy DSL. Configurable JWT verification — HS256, RS256, ES256, EdDSA with JWKS URI or direct public key (symmetric design with auth.provider). Replaceable with OPA or Cedar — protobuf.interceptors supports all three as backends.
gRPC authorization middleware (Go). Declares access policy (resource + action) in .proto method options and enforces it via interceptors. Two independent modules: protobuf_policy_option (policy declaration/resolution) and policy_verification (enforcement against an authorization backend).
Each component is designed to be replaced independently. protobuf.interceptors is the exception — it persists across migrations as the bridge between your gRPC services and whichever authorization backend you use.
| Component | Replaceable by | What changes |
|---|---|---|
| auth.provider | Keycloak, Ory Hydra, Logto, Auth0 | Introspection endpoint URL in auth.proxy config |
| auth.proxy | Envoy ext_authz, Traefik ForwardAuth, Kong | Reverse proxy config; downstream services are unaffected |
| auth.policy-verifier | OPA, Cedar, Cerbos | protobuf.interceptors backend: NewOPAEndpoint() or NewCedarAgentEndpoint() |
| protobuf.interceptors | — | Not replaced. Backend-agnostic by design. Supports auth.policy-verifier, OPA, Cedar, and static rules. |
See docs/competitors.md for detailed competitor analysis per component.
Client
|
| (1) Login / Authorization code
v
auth.provider ──── Redis (sessions)
|
| (2) JWT access token
v
auth.proxy ──────── auth.provider (introspection)
|
| (3) Validated request
v
downstream service
|
| (4) POST /verify
v
auth.policy-verifier (ABAC)
For gRPC services, protobuf.interceptors provides interceptors that call the policy verifier (or OPA/Cedar as alternative backends).
See docs/architecture.md for detailed flow and component descriptions, and docs/claims-contract.md for the claim-level JWT contract between auth.provider and auth.policy-verifier.
make setup # Clone all component repos
make build # Install deps and build
make test-e2e # Start services, run E2E tests, tear downmake test-e2e tests each component at the revision pinned at the top of the Makefile (PROVIDER_REV, PROXY_REV, VERIFIER_REV). The pins are the tested baseline, and the pinned run of the e2e workflow — on every push to develop, on every pull request, and on a manual run left at its defaults — is the release gate. Moving the baseline means changing a pin in a pull request, where that gate runs.
When moving a pin (a component's release cut):
- Check that the nightly
e2e-developworkflow is still enabled and that its latest run is green:gh workflow list --all -R o3co/authshows its state. GitHub disables a public repository's scheduled workflows after 60 days without activity, and this repository has gone longer than that between commits. Turn it back on withgh workflow enable e2e-develop.yml -R o3co/auth. A red nightly is the breakage the new pin is about to take in. - Change the pin in a pull request, and record the green
e2erun in the commit message.
To test another revision, override its variable on the command line; a command-line variable wins over the Makefile's :=:
make test-e2e PROVIDER_REV=origin/develop # a branch, written origin/<branch>
make test-e2e PROXY_REV=v0.7.0 # a tag
make test-e2e VERIFIER_REV=1e29749 # a SHAmake setup runs git fetch origin and then git checkout --detach <rev>, so write a branch as origin/<branch>. A bare branch name resolves only to a local branch, and a clone has one only for the default branch, as it was when cloned: any other bare name fails, and the default one is stale in a clone that already existed. Only commits reachable from the component's branches and tags are fetched.
In CI:
- A manual run with overrides. The
e2eworkflow's Run workflow form takesprovider_rev,proxy_revandverifier_revin the same forms; an empty field keeps the pin. A run with any override is not the release gate: its run name and its check name (test-e2e (overrides, not the release gate)) say so. - Nightly against
develop.e2e-developruns the same suite daily with every component atorigin/develop. It gates nothing: a red run means a component'sdevelopno longer passes this suite, found before the pin is bumped at release time. - From auth.provider. auth.provider's
umbrella-e2eworkflow runs this suite, at this repository'sdevelop, on its pull requests todevelop, with the pull request's code asPROVIDER_REVand the proxy and verifier at their pins. A brokendevelophere turns those checks red.
Every run's job summary lists the commit each component was tested at, next to its pin.
A component change that narrows what it accepts (a required config key, a stricter claim, a new status) is met here first: update tests/ so that it passes against both the pinned component and the change, then the component's pull request can go green.
Apache License 2.0