Files
ResolveSpec/pkg/security/lookup/ddl/mysql.sql
T
Hein 640faeeeaf 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.
2026-10-01 14:42:12 +02:00

290 lines
9.0 KiB
SQL

-- Reference schema for the lookup direct backend: MySQL 8.0.16+ / MariaDB 10.2+. Direct backend only. Run each statement separately (see ddl.Statements) unless multiStatements=true.
-- Table and column names are the lookup.DefaultSchema defaults, override them with lookup.Config.Schema.
-- Generated by the project, edit freely for your deployment (types, collations, extra columns).
-- password: bcrypt hash (nullable for OAuth2 users), legacy cleartext is accepted at login
-- roles: comma-separated roles
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255),
user_level INT DEFAULT 0,
roles VARCHAR(500),
is_active TINYINT(1) DEFAULT 1,
created_at DATETIME NULL,
updated_at DATETIME NULL,
last_login_at DATETIME,
program_user_id INT DEFAULT 0,
program_user_table VARCHAR(255) DEFAULT '',
remote_id VARCHAR(255),
auth_provider VARCHAR(50),
totp_secret VARCHAR(255),
totp_enabled TINYINT(1) DEFAULT 0,
totp_enabled_at DATETIME
);
CREATE TABLE IF NOT EXISTS user_sessions (
id INT AUTO_INCREMENT PRIMARY KEY,
session_token VARCHAR(500) NOT NULL UNIQUE,
user_id INT NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME NULL,
last_activity_at DATETIME NULL,
ip_address VARCHAR(45),
user_agent TEXT,
access_token TEXT,
refresh_token TEXT,
token_type VARCHAR(50) DEFAULT 'Bearer',
auth_provider VARCHAR(50),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_sessions_user_id (user_id),
INDEX idx_user_sessions_expires_at (expires_at)
);
CREATE TABLE IF NOT EXISTS token_blacklist (
id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(500) NOT NULL,
user_id INT,
expires_at DATETIME NOT NULL,
created_at DATETIME NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- code_hash: SHA-256 hex of the backup code
CREATE TABLE IF NOT EXISTS user_totp_backup_codes (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
code_hash VARCHAR(64) NOT NULL,
used TINYINT(1) DEFAULT 0,
used_at DATETIME,
created_at DATETIME NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_totp_user_id (user_id),
INDEX idx_totp_code_hash (code_hash)
);
-- credential_id: base64 text
-- public_key: base64 text
-- aaguid: base64 text
-- transports: JSON-encoded array
CREATE TABLE IF NOT EXISTS user_passkey_credentials (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
credential_id VARCHAR(1400) CHARACTER SET ascii NOT NULL UNIQUE,
public_key TEXT NOT NULL,
attestation_type VARCHAR(50) DEFAULT 'none',
aaguid TEXT,
sign_count INT DEFAULT 0,
clone_warning TINYINT(1) DEFAULT 0,
transports TEXT,
backup_eligible TINYINT(1) DEFAULT 0,
backup_state TINYINT(1) DEFAULT 0,
name VARCHAR(255),
created_at DATETIME NULL,
last_used_at DATETIME NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_passkey_user_id (user_id)
);
-- token_hash: SHA-256 hex of the raw token
CREATE TABLE IF NOT EXISTS user_password_resets (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token_hash VARCHAR(64) NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
created_at DATETIME NULL,
used TINYINT(1) DEFAULT 0,
used_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_pw_reset_user_id (user_id),
INDEX idx_pw_reset_expires_at (expires_at)
);
-- redirect_uris: JSON-encoded array
-- grant_types: JSON-encoded array
-- allowed_scopes: JSON-encoded array
-- client_secret_hash: SHA-256 hex of the confidential-client secret, NULL for public clients
CREATE TABLE IF NOT EXISTS oauth_clients (
id INT AUTO_INCREMENT PRIMARY KEY,
client_id VARCHAR(255) NOT NULL UNIQUE,
redirect_uris TEXT NOT NULL,
client_name VARCHAR(255),
grant_types TEXT,
allowed_scopes TEXT,
client_secret_hash TEXT,
token_endpoint_auth_method VARCHAR(30) DEFAULT 'none',
is_active TINYINT(1) DEFAULT 1,
metadata TEXT,
created_at DATETIME NULL
);
-- scopes: JSON-encoded array
CREATE TABLE IF NOT EXISTS oauth_codes (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(255) NOT NULL UNIQUE,
client_id VARCHAR(255) NOT NULL,
redirect_uri TEXT NOT NULL,
client_state TEXT,
code_challenge VARCHAR(255) NOT NULL,
code_challenge_method VARCHAR(10) DEFAULT 'S256',
session_token TEXT NOT NULL,
refresh_token TEXT,
scopes TEXT,
expires_at DATETIME NOT NULL,
extra TEXT,
created_at DATETIME NULL,
INDEX idx_oauth_codes_expires (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 INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
client_id VARCHAR(255) NOT NULL,
scopes TEXT,
created_at DATETIME NULL,
expires_at DATETIME NOT NULL,
INDEX idx_oauth_consents_user_client (user_id, client_id)
);
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
token_hash VARCHAR(64) NOT NULL UNIQUE,
family_id VARCHAR(64) NOT NULL,
client_id VARCHAR(255) NOT NULL,
user_id INT NOT NULL,
session_token VARCHAR(255),
scopes TEXT,
extra TEXT,
created_at DATETIME NULL,
expires_at DATETIME NOT NULL,
used_at DATETIME,
revoked_at DATETIME,
INDEX idx_oauth_refresh_family (family_id),
INDEX idx_oauth_refresh_session (session_token),
INDEX idx_oauth_refresh_expires (expires_at)
);
CREATE TABLE IF NOT EXISTS oauth_device_codes (
id INT AUTO_INCREMENT PRIMARY KEY,
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 INT,
session_token VARCHAR(255),
poll_interval INT NOT NULL DEFAULT 5,
created_at DATETIME NULL,
expires_at DATETIME NOT NULL,
last_polled_at DATETIME,
INDEX idx_oauth_device_expires (expires_at)
);
CREATE TABLE IF NOT EXISTS oauth_par_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
request_uri VARCHAR(255) NOT NULL UNIQUE,
client_id VARCHAR(255) NOT NULL,
params TEXT,
created_at DATETIME NULL,
expires_at DATETIME NOT NULL,
INDEX idx_oauth_par_expires (expires_at)
);
CREATE TABLE IF NOT EXISTS oauth_jti (
id INT AUTO_INCREMENT PRIMARY KEY,
jti_key VARCHAR(255) NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
INDEX idx_oauth_jti_expires (expires_at)
);
-- key_hash: SHA-256 hex
-- scopes: JSON-encoded array
-- meta: JSON-encoded object
CREATE TABLE IF NOT EXISTS user_keys (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
key_type VARCHAR(50) NOT NULL,
key_hash VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL DEFAULT '',
scopes TEXT,
meta TEXT,
expires_at DATETIME,
created_at DATETIME NULL,
last_used_at DATETIME,
is_active TINYINT(1) DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_keys_user_id (user_id),
INDEX idx_user_keys_key_type (key_type)
);
-- Optional: omit to use per-user rules only.
CREATE TABLE IF NOT EXISTS sec_group_members (
group_id INT NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY (group_id, user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- column_path: dot path under the table: col or col.sub.field
-- access_type: mask, hide, read, ...
-- extra_filters: JSON object
CREATE TABLE IF NOT EXISTS sec_column_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
group_id INT,
schema_name VARCHAR(255) NOT NULL,
table_name VARCHAR(255) NOT NULL,
column_path VARCHAR(255) NOT NULL,
access_type VARCHAR(50) NOT NULL,
mask_start INT DEFAULT 0,
mask_end INT DEFAULT 0,
mask_invert TINYINT(1) DEFAULT 0,
mask_char VARCHAR(10) DEFAULT '*',
extra_filters TEXT,
is_active TINYINT(1) NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL)),
INDEX idx_sec_column_rules_table (schema_name, table_name)
);
-- template: SQL fragment, e.g. user_id = {UserID}
CREATE TABLE IF NOT EXISTS sec_row_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
group_id INT,
schema_name VARCHAR(255) NOT NULL,
table_name VARCHAR(255) NOT NULL,
template TEXT,
has_block TINYINT(1) NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL)),
INDEX idx_sec_row_rules_table (schema_name, table_name)
);