OAuth 2.1 · OpenID Connect · self-hosted
Sign-in for your own applications. Every piece of its state in TessariDB.
TessariDB OAuth is an authorization server and OpenID provider written in Rust. One binary carries the protocol endpoints, the sign-in and account pages your users see, and an admin console on a listener of its own. Companies, users, clients, codes, grants, sealed keys and the audit trail are records in a TessariDB store.
docker pull tessaridb/oauth:0.1.00.1.0 · first release · alpha
What it refuses
An authorization server is the things it says no to.
One browser flow
Authorization code with PKCE S256, with pushed authorization requests. Implicit, token-returning hybrid and password grants are not offered.
A code redeems once
Of any number of simultaneous redemptions exactly one succeeds. A replayed code, or a reused refresh token, revokes the whole refresh family.
Redirects matched exactly
A redirect URI is compared as a string against the registered list, with only the native-app loopback port exception.
Tokens bound to a key
When a client asks, tokens and even codes are bound to its key with DPoP. A stolen token without the key is refused.
No shared secrets
Clients authenticate with private_key_jwt, EdDSA or ES256, or are public with PKCE. No client secret is ever made.
A second factor that is really there
Passkeys, authenticator apps and recovery codes, by the user's choice or the company's rule, never removed by an email alone.
Every issuance on the record
The audit trail is a TessariDB topic kept for 400 days, and an issuance that cannot be recorded is not answered.
State in TessariDB
Companies, users, codes, grants, consents, refresh families, sessions, sealed keys and counters are records in one TessariDB store, with no second store beside it.
- grants
- code + PKCE · refresh · client credentials
- sign-in
- password · TOTP · recovery codes · passkeys
- tokens
- 5-minute access · DPoP-bound on request
- image
- linux/amd64 · linux/arm64
The screens
What your users and your operators see.
Taken from a 0.1.0 server for the demo company Acme. The pages carry the company’s name, colour and links, follow the system theme, and need no script from anywhere else.




The browser flow
One flow, checked at every step.
Authorization code with PKCE is the only way a browser gets tokens. Step through it: each step says what the issuer or the application checks before the flow goes on.
Tokens, step by step →The application keeps the code verifier. Only its SHA-256 hash travels.
- redemption 1
- POST /oauth2/token
- spend code (one conditional write)
- tokens issued, family F1
- redemption 2
- POST /oauth2/token
- code already spent
- invalid_grant, family F1 revoked
Codes and refresh tokens
Spent once. A second use is a leak.
A code is stored by its digest and spent with one conditional write in TessariDB, so of any number of simultaneous redemptions exactly one succeeds. A code or refresh token presented again is refused and revokes the whole refresh family.
Sender-constrained tokens
A token that only its key can use.
When a client asks, its tokens are bound to its key with DPoP, and so is the code, by dpop_jkt or by the proof at the pushed request. A copied token without the key is refused.
- client, with its key
Authorization: DPoP eyJ… + DPoP: proof(ath, htm, htu)answered - someone who copied the token
Authorization: DPoP eyJ… + no proof from the key401 invalid_token - the same token as a bearer token
Authorization: Bearer eyJ…refused
- region us
GET id.us.example.com/acme/oauth2/authorize?…307 → eu - region eu
GET id.eu.example.com/acme/oauth2/authorize?…answered where acme lives
Regions
One home region per company.
Each region is its own TessariDB deployment, and a company’s identity data stays in its home region. A request that reaches another region is redirected there, never proxied.
How the server is built →Status
The first release, in alpha.
TessariDB OAuth is at 0.1.0, its first release, and runs on TessariDB 0.38.2 or later. Everything on this site is served by the binary and covered by its tests. The configuration and the HTTP surface may still change in a minor release before 1.0, so pin the image tag.
curl https://id.example.com/acme/.well-known/openid-configuration
# {"issuer":"https://id.example.com/acme",
# "code_challenge_methods_supported":["S256"], …}