Files
pad/internal/models/oauth.go
T
xarmian e5262160e1 feat(oauth): install clients, service tokens and the issuance barrier for installed apps (TASK-3394, SPEC-6 U5a) (#1769)
* feat(store): install clients, the issuance barrier and app_token_bindings (TASK-3394)

* feat(oauth): install-client service tokens, in-process app-token introspection, public introspection hardening, A0 token gate (TASK-3394)

* test(store): the issuance barrier serializes with disable, rotate and re-enable on Postgres (TASK-3394)

* test(server): pin each install-token refusal by its own hint; bearer introspection of another token (TASK-3394)

* fix(oauth): a grant carries the epoch read before client auth; the install-caller check reads credentials as fosite does (TASK-3394, codex r1)

* fix(oauth): classify the token request from the body fosite parses, multipart included (TASK-3394, codex r2)

* test(server): account for the install-client token rules' form readers in the body-reader census (TASK-3394)

* chore(store): renumber the install-client migrations to 117 / pg 091 after #1768 (TASK-3394)
2026-10-04 05:58:33 -04:00

105 lines
4.6 KiB
Go

package models
import "time"
// Models for the OAuth 2.1 authorization server (PLAN-943 TASK-951).
// These are pad-internal types used by internal/store/oauth.go to
// persist OAuth state. Sub-PR B (TASK-1024) introduces fosite, and
// sub-PR C wires fosite.Requester ⇄ OAuthRequest adapters in the
// HTTP handlers. The store layer never imports fosite so the schema
// boundary stays clean.
// OAuthClient is a registered OAuth client (RFC 7591 Dynamic Client
// Registration). Only public clients (no secret) are supported in
// v1 — see migrations/048_oauth.sql for why. The Public flag is a
// forward-compat toggle; future confidential-client support adds a
// secret column without changing this struct's shape.
//
// Field names use the RFC 7591 vocabulary so the JSON form can be
// served back to clients verbatim from the registration endpoint
// (sub-PR C).
type OAuthClient struct {
ID string `json:"client_id"`
Name string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
Scopes []string `json:"scope"`
Public bool `json:"-"` // internal — never serialized
LogoURL string `json:"logo_uri,omitempty"`
CreatedAt time.Time `json:"client_id_issued_at"`
// Install clients (SPEC-6 U5a, TASK-3394). An installed app's one
// confidential client: a secret (only its hash is stored, from fosite's
// hasher), its own audiences (the app API resource only), and its
// install. All three are empty on an ordinary DCR client, whose
// audiences are the server's MCP canonicals. None is ever serialized.
SecretHash string `json:"-"`
AllowedAudiences []string `json:"-"`
AppInstallID string `json:"-"`
DisabledAt string `json:"-"`
}
// IsInstallClient reports whether the client belongs to an installed app.
func (c *OAuthClient) IsInstallClient() bool { return c != nil && c.AppInstallID != "" }
// OAuthRequest is the persisted form of a fosite.Requester (without
// the fosite import). Carries everything the auth-code, access-token,
// refresh-token, and PKCE storage rows need:
//
// - Signature: the HMAC-derived lookup key (the row's primary key).
// - RequestID: fosite's stable Requester.GetID() — preserved across
// refresh-token rotations, so it doubles as the chain identifier
// for theft-detection family revocation.
// - RequestedAt: original grant timestamp; used by the cleaner.
// - ClientID: FK to oauth_clients.
// - Scopes / GrantedScopes: space-separated.
// - RequestForm: URL-encoded form data from the original request
// (PKCE handler reads code_challenge from here).
// - SessionData: JSON-encoded session struct (subject + custom
// claims). Sub-PR B defines pad's session type; the store layer
// just round-trips it as bytes.
// - Audience / GrantedAudience: space-separated RFC 8707 resource
// indicators.
// - Active: token-revocation toggle. RevokeRefreshToken /
// RevokeAccessToken set this false; GetXxxSession returns
// ErrInactiveToken (a sentinel defined in oauth.go) when active=false.
// - Subject: denormalized from SessionData for fast subject-bound
// queries (admin "list active tokens for user X" surfaces). Empty
// for auth-code and PKCE rows where there is no subject yet.
// - AccessTokenSignature: refresh-only; links the refresh row to
// the access row issued in the same grant (or rotation step).
// Empty for non-refresh rows.
type OAuthRequest struct {
Signature string
RequestID string
RequestedAt time.Time
ClientID string
Scopes string
GrantedScopes string
RequestForm string
SessionData string
Audience string
GrantedAudience string
Active bool
Subject string
AccessTokenSignature string
}
// OAuthClientCreate is the input shape for the storage-level
// CreateClient method. The Postgres / SQLite migration already
// constrains required fields; this type lets callers set only the
// fields they have without populating the timestamp (the store sets
// CreatedAt from now()).
type OAuthClientCreate struct {
Name string
RedirectURIs []string
GrantTypes []string
ResponseTypes []string
TokenEndpointAuthMethod string
Scopes []string
Public bool
LogoURL string
}