TessariDB OAuth
Get started

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

Built onOAuth 2.1OpenID ConnectRustTessariDBlinux/amd64linux/arm64

What it refuses

An authorization server is the things it says no to.

pkce

One browser flow

Authorization code with PKCE S256, with pushed authorization requests. Implicit, token-returning hybrid and password grants are not offered.

once

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.

exact

Redirects matched exactly

A redirect URI is compared as a string against the registered list, with only the native-app loopback port exception.

dpop

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.

keys

No shared secrets

Clients authenticate with private_key_jwt, EdDSA or ES256, or are public with PKCE. No client secret is ever made.

factor

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.

record

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.

store

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 sign-in page of the demo company Acme: an email field and a Continue button.
Sign-in, worded and branded per company
The consent page: the application acme-notes asks for groups and roles, sign-in confirmation and the user's name, at one service, with Allow access and Deny.
Consent names what is released and where
The account portal for a demo user: the sign-in address, the name, and how the user signs in.
The account portal, kept by the user
The admin console's Users page: addresses shown masked, each with its verification state and account id.
The admin console, addresses masked

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 →
authorization code + PKCEstep 1 of 6

Application → Browserredirect → /oauth2/authorize code_challenge (S256), state, nonce

The application keeps the code verifier. Only its SHA-256 hash travels.

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.

cnf.jkt = thumbprint(client key)sender-constrained
  • client, with its keyAuthorization: DPoP eyJ… + DPoP: proof(ath, htm, htu)answered
  • someone who copied the tokenAuthorization: DPoP eyJ… + no proof from the key401 invalid_token
  • the same token as a bearer tokenAuthorization: Bearer eyJ…refused
a proof is spent once; keys are named by their JWK thumbprint (RFC 7638)
acme → home region euredirected, never proxied
  • region usGET id.us.example.com/acme/oauth2/authorize?…307 → eu
  • region euGET id.eu.example.com/acme/oauth2/authorize?…answered where acme lives
each region is its own TessariDB store; peers read each other’s directory every five seconds

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.

shellsh
curl https://id.example.com/acme/.well-known/openid-configuration
# {"issuer":"https://id.example.com/acme",
#  "code_challenge_methods_supported":["S256"], …}