mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-27 06:47:07 +00:00
e5262160e1
* 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)
105 lines
4.6 KiB
Go
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
|
|
}
|