Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/coredata/entity_type_reg.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ const (
AuditLogEntryEntityType uint16 = 68
DocumentVersionApprovalQuorumEntityType uint16 = 69
DocumentVersionApprovalDecisionEntityType uint16 = 70
OAuth2ClientEntityType uint16 = 71
OAuth2ConsentEntityType uint16 = 72
)

func NewEntityFromID(id gid.GID) (any, bool) {
Expand Down Expand Up @@ -232,6 +234,10 @@ func NewEntityFromID(id gid.GID) (any, bool) {
return &DocumentVersionApprovalDecision{ID: id}, true
case DocumentVersionApprovalQuorumEntityType:
return &DocumentVersionApprovalQuorum{ID: id}, true
case OAuth2ClientEntityType:
return &OAuth2Client{ID: id}, true
case OAuth2ConsentEntityType:
return &OAuth2Consent{ID: id}, true
default:
return nil, false
}
Expand Down
102 changes: 102 additions & 0 deletions pkg/coredata/migrations/20260328T120000Z.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
-- 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,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
CONSTRAINT iam_oauth2_consents_identity_client_unique UNIQUE (identity_id, client_id)
);
168 changes: 168 additions & 0 deletions pkg/coredata/oauth2_access_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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.

package coredata

import (
"context"
"errors"
"fmt"
"time"

"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)

type (
OAuth2AccessToken struct {
ID string `db:"id"`
HashedValue []byte `db:"hashed_value"`
ClientID gid.GID `db:"client_id"`
IdentityID gid.GID `db:"identity_id"`
Scopes OAuth2Scopes `db:"scopes"`
CreatedAt time.Time `db:"created_at"`
ExpiresAt time.Time `db:"expires_at"`
}
)

func (t *OAuth2AccessToken) Insert(ctx context.Context, conn pg.Conn) error {
q := `
INSERT INTO iam_oauth2_access_tokens (
id,
hashed_value,
client_id,
identity_id,
scopes,
created_at,
expires_at
) VALUES (
@id,
@hashed_value,
@client_id,
@identity_id,
@scopes,
@created_at,
@expires_at
)
`

args := pgx.StrictNamedArgs{
"id": t.ID,
"hashed_value": t.HashedValue,
"client_id": t.ClientID,
"identity_id": t.IdentityID,
"scopes": t.Scopes,
"created_at": t.CreatedAt,
"expires_at": t.ExpiresAt,
}

_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot insert oauth2_access_token: %w", err)
}

return nil
}

func (t *OAuth2AccessToken) LoadByHashedValue(ctx context.Context, conn pg.Conn, hashedValue []byte) error {
q := `
SELECT
id,
hashed_value,
client_id,
identity_id,
scopes,
created_at,
expires_at
FROM
iam_oauth2_access_tokens
WHERE
hashed_value = @hashed_value
LIMIT 1;
`

rows, err := conn.Query(ctx, q, pgx.StrictNamedArgs{"hashed_value": hashedValue})
if err != nil {
return fmt.Errorf("cannot query oauth2_access_token: %w", err)
}

token, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[OAuth2AccessToken])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}

return fmt.Errorf("cannot collect oauth2_access_token: %w", err)
}

*t = token
return nil
}

func (t *OAuth2AccessToken) Delete(ctx context.Context, conn pg.Conn) error {
q := `
DELETE FROM iam_oauth2_access_tokens
WHERE
id = @id
`

_, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"id": t.ID})
if err != nil {
return fmt.Errorf("cannot delete oauth2_access_token: %w", err)
}

return nil
}

func (t *OAuth2AccessToken) DeleteExpired(ctx context.Context, conn pg.Conn, now time.Time) (int64, error) {
q := `
DELETE FROM iam_oauth2_access_tokens
WHERE
expires_at < @now
`

result, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"now": now})
if err != nil {
return 0, fmt.Errorf("cannot delete expired oauth2_access_tokens: %w", err)
}

return result.RowsAffected(), nil
}

func (t *OAuth2AccessToken) DeleteByClientAndIdentity(
ctx context.Context,
conn pg.Conn,
clientID gid.GID,
identityID gid.GID,
) (int64, error) {
q := `
DELETE FROM iam_oauth2_access_tokens
WHERE
client_id = @client_id
AND identity_id = @identity_id
`

args := pgx.StrictNamedArgs{
"client_id": clientID,
"identity_id": identityID,
}

result, err := conn.Exec(ctx, q, args)
if err != nil {
return 0, fmt.Errorf("cannot delete oauth2_access_tokens by client and identity: %w", err)
}

return result.RowsAffected(), nil
}
Loading
Loading