feat(security): full OAuth 2.1 / OpenID Connect server and OIDC relying-party client

Authorization server: consent and scopes, OIDC (nonce, auth_time, acr, sid,
at_hash, signed userinfo, RP-initiated and back-channel logout), managed
refresh tokens with rotation and reuse detection, RFC 9068 JWT access tokens,
DPoP, PAR, device grant, token exchange, private_key_jwt, RFC 7591/7592
registration, RFC 9207 iss, signing keyring with rotation.

State is DB-backed through a new lookup.OAuthGrantStore (procedure and direct
backends, four dialect DDLs, conformance cases).

Client side: WithOIDC discovery, PKCE, nonce, id_token validation, OAuth2LogoutURL.

PeekRefresh now returns already rotated tokens so RotateRefresh can detect reuse.

Docs: OAUTH2_SERVER.md, oauth2_full_example.go, breaking_changes.md step 8.
This commit is contained in:
Hein
2026-10-01 14:42:12 +02:00
parent f54b707040
commit 640faeeeaf
51 changed files with 8682 additions and 1125 deletions
+73
View File
@@ -130,6 +130,7 @@ CREATE TABLE IF NOT EXISTS oauth_clients (
client_secret_hash TEXT,
token_endpoint_auth_method VARCHAR(30) DEFAULT 'none',
is_active BOOLEAN DEFAULT 1,
metadata TEXT,
created_at TIMESTAMP
);
@@ -148,12 +149,84 @@ CREATE TABLE IF NOT EXISTS oauth_codes (
refresh_token TEXT,
scopes TEXT,
expires_at TIMESTAMP NOT NULL,
extra TEXT,
created_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_oauth_codes_expires ON oauth_codes(expires_at);
-- oauth_consents.scopes / oauth_refresh_tokens.scopes+extra / oauth_device_codes.scopes / oauth_par_requests.params: JSON text
CREATE TABLE IF NOT EXISTS oauth_consents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
client_id VARCHAR(255) NOT NULL,
scopes TEXT,
created_at TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_oauth_consents_user_client ON oauth_consents(user_id, client_id);
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token_hash VARCHAR(64) NOT NULL UNIQUE,
family_id VARCHAR(64) NOT NULL,
client_id VARCHAR(255) NOT NULL,
user_id INTEGER NOT NULL,
session_token VARCHAR(255),
scopes TEXT,
extra TEXT,
created_at TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP,
revoked_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_oauth_refresh_family ON oauth_refresh_tokens(family_id);
CREATE INDEX IF NOT EXISTS idx_oauth_refresh_session ON oauth_refresh_tokens(session_token);
CREATE INDEX IF NOT EXISTS idx_oauth_refresh_expires ON oauth_refresh_tokens(expires_at);
CREATE TABLE IF NOT EXISTS oauth_device_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_hash VARCHAR(64) NOT NULL UNIQUE,
user_code VARCHAR(32) NOT NULL UNIQUE,
client_id VARCHAR(255) NOT NULL,
scopes TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
user_id INTEGER,
session_token VARCHAR(255),
poll_interval INTEGER NOT NULL DEFAULT 5,
created_at TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_polled_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_oauth_device_expires ON oauth_device_codes(expires_at);
CREATE TABLE IF NOT EXISTS oauth_par_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_uri VARCHAR(255) NOT NULL UNIQUE,
client_id VARCHAR(255) NOT NULL,
params TEXT,
created_at TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_oauth_par_expires ON oauth_par_requests(expires_at);
CREATE TABLE IF NOT EXISTS oauth_jti (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jti_key VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_oauth_jti_expires ON oauth_jti(expires_at);
-- key_hash: SHA-256 hex
-- scopes: JSON-encoded array
-- meta: JSON-encoded object