Add OAuth2/OpenID Connect authorization server

Implement a full OAuth2 2.0 and OpenID Connect 1.0 authorization
server with support for authorization code flow (with PKCE),
refresh token rotation, device authorization grant, dynamic
client registration, token introspection, and token revocation.

Includes database schema, coredata layer, service logic, HTTP
handlers, OIDC discovery endpoint, and JWKS publishing.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-30 14:49:18 +02:00
parent e84094e62c
commit 11770b4058
155 changed files with 14483 additions and 223 deletions

View File

@@ -0,0 +1,107 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
-- OAuth2 Authorization Server tables
CREATE TYPE oauth2_client_visibility AS ENUM ('private', 'public');
CREATE TYPE oauth2_client_token_endpoint_auth_method AS ENUM ('client_secret_basic', 'client_secret_post', 'none');
CREATE TYPE oauth2_device_code_status AS ENUM ('pending', 'authorized', 'denied', 'expired');
CREATE TABLE iam_oauth2_clients (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
organization_id TEXT NOT NULL,
client_secret_hash BYTEA,
client_name TEXT NOT NULL,
visibility oauth2_client_visibility NOT NULL,
redirect_uris TEXT[] NOT NULL,
scopes TEXT[] NOT NULL,
grant_types TEXT[] NOT NULL,
response_types TEXT[] NOT NULL,
token_endpoint_auth_method oauth2_client_token_endpoint_auth_method NOT NULL,
logo_uri TEXT,
client_uri TEXT,
contacts TEXT[],
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE TABLE iam_oauth2_authorization_codes (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES iam_oauth2_clients(id),
identity_id TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
scopes TEXT[] NOT NULL,
code_challenge TEXT,
code_challenge_method TEXT,
nonce TEXT,
auth_time TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE TABLE iam_oauth2_access_tokens (
id TEXT PRIMARY KEY,
hashed_value BYTEA NOT NULL,
client_id TEXT NOT NULL REFERENCES iam_oauth2_clients(id),
identity_id TEXT NOT NULL,
scopes TEXT[] NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
CONSTRAINT iam_oauth2_access_tokens_hashed_value_unique UNIQUE (hashed_value)
);
CREATE TABLE iam_oauth2_refresh_tokens (
id TEXT PRIMARY KEY,
hashed_value BYTEA NOT NULL,
client_id TEXT NOT NULL REFERENCES iam_oauth2_clients(id),
identity_id TEXT NOT NULL,
scopes TEXT[] NOT NULL,
access_token_id TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
revoked_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT iam_oauth2_refresh_tokens_hashed_value_unique UNIQUE (hashed_value)
);
CREATE TABLE iam_oauth2_device_codes (
id TEXT PRIMARY KEY,
device_code_hash BYTEA NOT NULL,
user_code TEXT NOT NULL,
client_id TEXT NOT NULL REFERENCES iam_oauth2_clients(id),
scopes TEXT[] NOT NULL,
identity_id TEXT,
status oauth2_device_code_status NOT NULL,
last_polled_at TIMESTAMP WITH TIME ZONE,
poll_interval INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
CONSTRAINT iam_oauth2_device_codes_device_code_hash_unique UNIQUE (device_code_hash),
CONSTRAINT iam_oauth2_device_codes_user_code_unique UNIQUE (user_code)
);
CREATE TABLE iam_oauth2_consents (
id TEXT PRIMARY KEY,
identity_id TEXT NOT NULL,
client_id TEXT NOT NULL REFERENCES iam_oauth2_clients(id),
scopes TEXT[] NOT NULL,
redirect_uri TEXT NOT NULL,
code_challenge TEXT NOT NULL,
code_challenge_method TEXT NOT NULL,
nonce TEXT NOT NULL,
state TEXT NOT NULL,
approved BOOLEAN NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE iam_oauth2_consents
ADD COLUMN session_id TEXT NOT NULL REFERENCES iam_sessions(id);

View File

@@ -0,0 +1,49 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
-- Allow system-level OAuth2 clients that don't belong to any tenant or
-- organization (e.g. the Probo CLI).
ALTER TABLE iam_oauth2_clients ALTER COLUMN tenant_id DROP NOT NULL;
ALTER TABLE iam_oauth2_clients ALTER COLUMN organization_id DROP NOT NULL;
-- Well-known OAuth2 client for the Probo CLI (prb).
-- This client is hardcoded in the CLI binary and used for the device
-- authorization flow. Same pattern as GitHub CLI + GitHub Enterprise Server.
INSERT INTO iam_oauth2_clients (
id,
tenant_id,
organization_id,
client_name,
visibility,
redirect_uris,
scopes,
grant_types,
response_types,
token_endpoint_auth_method,
created_at,
updated_at
) VALUES (
'AAAAAAAAAAAASwAAAAAAAAAAcHJiY2xp',
NULL,
NULL,
'Probo CLI',
'public',
'{}',
'{openid,profile,email}',
'{urn:ietf:params:oauth:grant-type:device_code,refresh_token}',
'{code}',
'none',
NOW(),
NOW()
);

View File

@@ -0,0 +1,20 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
-- Add offline_access scope to the Probo CLI OAuth2 client so the device
-- authorization flow can request refresh tokens.
UPDATE iam_oauth2_clients
SET scopes = '{openid,profile,email,offline_access}',
updated_at = NOW()
WHERE id = 'AAAAAAAAAAAASwAAAAAAAAAAcHJiY2xp';

View File

@@ -0,0 +1,19 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
ALTER TABLE iam_oauth2_consents
ADD COLUMN IF NOT EXISTS device_code_id TEXT;
ALTER TABLE iam_oauth2_consents
ALTER COLUMN redirect_uri DROP NOT NULL;

View File

@@ -0,0 +1,17 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
ALTER TABLE iam_oauth2_authorization_codes
ADD COLUMN IF NOT EXISTS redeemed_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS access_token_id TEXT;

View File

@@ -0,0 +1,20 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
ALTER TABLE iam_oauth2_authorization_codes
ADD COLUMN IF NOT EXISTS hashed_value BYTEA;
CREATE UNIQUE INDEX IF NOT EXISTS iam_oauth2_authorization_codes_hashed_value_unique
ON iam_oauth2_authorization_codes (hashed_value)
WHERE hashed_value IS NOT NULL;