mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 19:20:31 +00:00
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.
345 lines
13 KiB
SQL
345 lines
13 KiB
SQL
-- Reference schema for the lookup direct backend: Microsoft SQL Server 2016+. Direct backend only. Run each statement separately (see ddl.Statements).
|
|
-- 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
|
|
|
|
IF OBJECT_ID(N'users', N'U') IS NULL
|
|
CREATE TABLE users (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
username NVARCHAR(255) NOT NULL UNIQUE,
|
|
email NVARCHAR(255) NOT NULL UNIQUE,
|
|
password NVARCHAR(255),
|
|
user_level INT DEFAULT 0,
|
|
roles NVARCHAR(500),
|
|
is_active BIT DEFAULT 1,
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
updated_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
last_login_at DATETIME2,
|
|
program_user_id INT DEFAULT 0,
|
|
program_user_table NVARCHAR(255) DEFAULT '',
|
|
remote_id NVARCHAR(255),
|
|
auth_provider NVARCHAR(50),
|
|
totp_secret NVARCHAR(255),
|
|
totp_enabled BIT DEFAULT 0,
|
|
totp_enabled_at DATETIME2
|
|
);
|
|
|
|
|
|
IF OBJECT_ID(N'user_sessions', N'U') IS NULL
|
|
CREATE TABLE user_sessions (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
session_token NVARCHAR(450) NOT NULL UNIQUE,
|
|
user_id INT NOT NULL,
|
|
expires_at DATETIME2 NOT NULL,
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
last_activity_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
ip_address NVARCHAR(45),
|
|
user_agent NVARCHAR(MAX),
|
|
access_token NVARCHAR(MAX),
|
|
refresh_token NVARCHAR(MAX),
|
|
token_type NVARCHAR(50) DEFAULT 'Bearer',
|
|
auth_provider NVARCHAR(50),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_sessions_user_id' AND object_id = OBJECT_ID(N'user_sessions'))
|
|
CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_sessions_expires_at' AND object_id = OBJECT_ID(N'user_sessions'))
|
|
CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at);
|
|
|
|
|
|
IF OBJECT_ID(N'token_blacklist', N'U') IS NULL
|
|
CREATE TABLE token_blacklist (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
token NVARCHAR(500) NOT NULL,
|
|
user_id INT,
|
|
expires_at DATETIME2 NOT NULL,
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
|
|
-- code_hash: SHA-256 hex of the backup code
|
|
|
|
IF OBJECT_ID(N'user_totp_backup_codes', N'U') IS NULL
|
|
CREATE TABLE user_totp_backup_codes (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
user_id INT NOT NULL,
|
|
code_hash NVARCHAR(64) NOT NULL,
|
|
used BIT DEFAULT 0,
|
|
used_at DATETIME2,
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_totp_user_id' AND object_id = OBJECT_ID(N'user_totp_backup_codes'))
|
|
CREATE INDEX idx_totp_user_id ON user_totp_backup_codes(user_id);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_totp_code_hash' AND object_id = OBJECT_ID(N'user_totp_backup_codes'))
|
|
CREATE INDEX idx_totp_code_hash ON user_totp_backup_codes(code_hash);
|
|
|
|
|
|
-- credential_id: base64 text
|
|
-- public_key: base64 text
|
|
-- aaguid: base64 text
|
|
-- transports: JSON-encoded array
|
|
|
|
IF OBJECT_ID(N'user_passkey_credentials', N'U') IS NULL
|
|
CREATE TABLE user_passkey_credentials (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
user_id INT NOT NULL,
|
|
credential_id VARCHAR(900) NOT NULL UNIQUE,
|
|
public_key NVARCHAR(MAX) NOT NULL,
|
|
attestation_type NVARCHAR(50) DEFAULT 'none',
|
|
aaguid NVARCHAR(MAX),
|
|
sign_count INT DEFAULT 0,
|
|
clone_warning BIT DEFAULT 0,
|
|
transports NVARCHAR(MAX),
|
|
backup_eligible BIT DEFAULT 0,
|
|
backup_state BIT DEFAULT 0,
|
|
name NVARCHAR(255),
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
last_used_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_passkey_user_id' AND object_id = OBJECT_ID(N'user_passkey_credentials'))
|
|
CREATE INDEX idx_passkey_user_id ON user_passkey_credentials(user_id);
|
|
|
|
|
|
-- token_hash: SHA-256 hex of the raw token
|
|
|
|
IF OBJECT_ID(N'user_password_resets', N'U') IS NULL
|
|
CREATE TABLE user_password_resets (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
user_id INT NOT NULL,
|
|
token_hash NVARCHAR(64) NOT NULL UNIQUE,
|
|
expires_at DATETIME2 NOT NULL,
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
used BIT DEFAULT 0,
|
|
used_at DATETIME2,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_pw_reset_user_id' AND object_id = OBJECT_ID(N'user_password_resets'))
|
|
CREATE INDEX idx_pw_reset_user_id ON user_password_resets(user_id);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_pw_reset_expires_at' AND object_id = OBJECT_ID(N'user_password_resets'))
|
|
CREATE INDEX idx_pw_reset_expires_at ON user_password_resets(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
|
|
|
|
IF OBJECT_ID(N'oauth_clients', N'U') IS NULL
|
|
CREATE TABLE oauth_clients (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
client_id NVARCHAR(255) NOT NULL UNIQUE,
|
|
redirect_uris NVARCHAR(MAX) NOT NULL,
|
|
client_name NVARCHAR(255),
|
|
grant_types NVARCHAR(MAX),
|
|
allowed_scopes NVARCHAR(MAX),
|
|
client_secret_hash NVARCHAR(MAX),
|
|
token_endpoint_auth_method NVARCHAR(30) DEFAULT 'none',
|
|
is_active BIT DEFAULT 1,
|
|
metadata NVARCHAR(MAX),
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME()
|
|
);
|
|
|
|
|
|
-- scopes: JSON-encoded array
|
|
|
|
IF OBJECT_ID(N'oauth_codes', N'U') IS NULL
|
|
CREATE TABLE oauth_codes (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
code NVARCHAR(255) NOT NULL UNIQUE,
|
|
client_id NVARCHAR(255) NOT NULL,
|
|
redirect_uri NVARCHAR(MAX) NOT NULL,
|
|
client_state NVARCHAR(MAX),
|
|
code_challenge NVARCHAR(255) NOT NULL,
|
|
code_challenge_method NVARCHAR(10) DEFAULT 'S256',
|
|
session_token NVARCHAR(MAX) NOT NULL,
|
|
refresh_token NVARCHAR(MAX),
|
|
scopes NVARCHAR(MAX),
|
|
expires_at DATETIME2 NOT NULL,
|
|
extra NVARCHAR(MAX),
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME()
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_codes_expires' AND object_id = OBJECT_ID(N'oauth_codes'))
|
|
CREATE INDEX 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
|
|
|
|
IF OBJECT_ID(N'oauth_consents', N'U') IS NULL
|
|
CREATE TABLE oauth_consents (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
user_id INT NOT NULL,
|
|
client_id NVARCHAR(255) NOT NULL,
|
|
scopes NVARCHAR(MAX),
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
expires_at DATETIME2 NOT NULL
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_consents_user_client' AND object_id = OBJECT_ID(N'oauth_consents'))
|
|
CREATE INDEX idx_oauth_consents_user_client ON oauth_consents(user_id, client_id);
|
|
|
|
IF OBJECT_ID(N'oauth_refresh_tokens', N'U') IS NULL
|
|
CREATE TABLE oauth_refresh_tokens (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
token_hash NVARCHAR(64) NOT NULL UNIQUE,
|
|
family_id NVARCHAR(64) NOT NULL,
|
|
client_id NVARCHAR(255) NOT NULL,
|
|
user_id INT NOT NULL,
|
|
session_token NVARCHAR(255),
|
|
scopes NVARCHAR(MAX),
|
|
extra NVARCHAR(MAX),
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
expires_at DATETIME2 NOT NULL,
|
|
used_at DATETIME2,
|
|
revoked_at DATETIME2
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_refresh_family' AND object_id = OBJECT_ID(N'oauth_refresh_tokens'))
|
|
CREATE INDEX idx_oauth_refresh_family ON oauth_refresh_tokens(family_id);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_refresh_session' AND object_id = OBJECT_ID(N'oauth_refresh_tokens'))
|
|
CREATE INDEX idx_oauth_refresh_session ON oauth_refresh_tokens(session_token);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_refresh_expires' AND object_id = OBJECT_ID(N'oauth_refresh_tokens'))
|
|
CREATE INDEX idx_oauth_refresh_expires ON oauth_refresh_tokens(expires_at);
|
|
|
|
IF OBJECT_ID(N'oauth_device_codes', N'U') IS NULL
|
|
CREATE TABLE oauth_device_codes (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
device_hash NVARCHAR(64) NOT NULL UNIQUE,
|
|
user_code NVARCHAR(32) NOT NULL UNIQUE,
|
|
client_id NVARCHAR(255) NOT NULL,
|
|
scopes NVARCHAR(MAX),
|
|
status NVARCHAR(16) NOT NULL DEFAULT 'pending',
|
|
user_id INT,
|
|
session_token NVARCHAR(255),
|
|
poll_interval INT NOT NULL DEFAULT 5,
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
expires_at DATETIME2 NOT NULL,
|
|
last_polled_at DATETIME2
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_device_expires' AND object_id = OBJECT_ID(N'oauth_device_codes'))
|
|
CREATE INDEX idx_oauth_device_expires ON oauth_device_codes(expires_at);
|
|
|
|
IF OBJECT_ID(N'oauth_par_requests', N'U') IS NULL
|
|
CREATE TABLE oauth_par_requests (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
request_uri NVARCHAR(255) NOT NULL UNIQUE,
|
|
client_id NVARCHAR(255) NOT NULL,
|
|
params NVARCHAR(MAX),
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
expires_at DATETIME2 NOT NULL
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_par_expires' AND object_id = OBJECT_ID(N'oauth_par_requests'))
|
|
CREATE INDEX idx_oauth_par_expires ON oauth_par_requests(expires_at);
|
|
|
|
IF OBJECT_ID(N'oauth_jti', N'U') IS NULL
|
|
CREATE TABLE oauth_jti (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
jti_key NVARCHAR(255) NOT NULL UNIQUE,
|
|
expires_at DATETIME2 NOT NULL
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_jti_expires' AND object_id = OBJECT_ID(N'oauth_jti'))
|
|
CREATE INDEX idx_oauth_jti_expires ON oauth_jti(expires_at);
|
|
|
|
|
|
-- key_hash: SHA-256 hex
|
|
-- scopes: JSON-encoded array
|
|
-- meta: JSON-encoded object
|
|
|
|
IF OBJECT_ID(N'user_keys', N'U') IS NULL
|
|
CREATE TABLE user_keys (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
user_id INT NOT NULL,
|
|
key_type NVARCHAR(50) NOT NULL,
|
|
key_hash NVARCHAR(64) NOT NULL UNIQUE,
|
|
name NVARCHAR(255) NOT NULL DEFAULT '',
|
|
scopes NVARCHAR(MAX),
|
|
meta NVARCHAR(MAX),
|
|
expires_at DATETIME2,
|
|
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
|
last_used_at DATETIME2,
|
|
is_active BIT DEFAULT 1,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_keys_user_id' AND object_id = OBJECT_ID(N'user_keys'))
|
|
CREATE INDEX idx_user_keys_user_id ON user_keys(user_id);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_keys_key_type' AND object_id = OBJECT_ID(N'user_keys'))
|
|
CREATE INDEX idx_user_keys_key_type ON user_keys(key_type);
|
|
|
|
|
|
-- Optional: omit to use per-user rules only.
|
|
|
|
IF OBJECT_ID(N'sec_group_members', N'U') IS NULL
|
|
CREATE TABLE 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
|
|
|
|
IF OBJECT_ID(N'sec_column_rules', N'U') IS NULL
|
|
CREATE TABLE sec_column_rules (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
user_id INT,
|
|
group_id INT,
|
|
schema_name NVARCHAR(255) NOT NULL,
|
|
table_name NVARCHAR(255) NOT NULL,
|
|
column_path NVARCHAR(255) NOT NULL,
|
|
access_type NVARCHAR(50) NOT NULL,
|
|
mask_start INT DEFAULT 0,
|
|
mask_end INT DEFAULT 0,
|
|
mask_invert BIT DEFAULT 0,
|
|
mask_char NVARCHAR(10) DEFAULT '*',
|
|
extra_filters NVARCHAR(MAX),
|
|
is_active BIT 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))
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_sec_column_rules_table' AND object_id = OBJECT_ID(N'sec_column_rules'))
|
|
CREATE INDEX idx_sec_column_rules_table ON sec_column_rules(schema_name, table_name);
|
|
|
|
|
|
-- template: SQL fragment, e.g. user_id = {UserID}
|
|
|
|
IF OBJECT_ID(N'sec_row_rules', N'U') IS NULL
|
|
CREATE TABLE sec_row_rules (
|
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
user_id INT,
|
|
group_id INT,
|
|
schema_name NVARCHAR(255) NOT NULL,
|
|
table_name NVARCHAR(255) NOT NULL,
|
|
template NVARCHAR(MAX),
|
|
has_block BIT NOT NULL DEFAULT 0,
|
|
is_active BIT 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))
|
|
);
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_sec_row_rules_table' AND object_id = OBJECT_ID(N'sec_row_rules'))
|
|
CREATE INDEX idx_sec_row_rules_table ON sec_row_rules(schema_name, table_name);
|
|
|