Documentation
¶
Overview ¶
admin_clients.go: the admin surface for editing an OAuth2 client that already exists. Creation, listing and deletion live in plugin.go; this file holds the update path so plugin.go does not grow another handler.
This is deliberately separate from the RFC 7592 management routes in register.go. Those authenticate the *client* with its registration access token and refuse anything that is not DynamicallyRegistered, which is every admin-created client. The two surfaces have disjoint callers and neither can stand in for the other.
clients_service.go: the client and signing key admin logic as plain methods.
The HTTP handlers take a forge.Context, which the dashboard's contract handlers do not have. The logic lives here instead, on a plain context.Context, and the handlers are thin wrappers that resolve the caller's app and map the errors. Every method takes the app it acts for and treats a client of any other app as not found, so the tenancy rule is enforced here rather than left to each caller.
contract.go: Wires the oauth2provider plugin into the forge-dashboard contract surface via plugin.ContractContributor.
Package oauth2provider implements an OAuth2 authorization server plugin for AuthSome. It supports Authorization Code + PKCE (RFC 7636), Client Credentials grants, token revocation, OIDC userinfo, and OpenID Connect discovery.
Index ¶
- Constants
- Variables
- type AppSlugRequest
- type AuthServerMetadata
- type AuthorizationCode
- type AuthorizeRequest
- type Config
- type ConsentDecision
- type ConsentGate
- type ConsentResult
- type ConsentView
- type CreateClientRequest
- type CreateClientResponse
- type DeleteClientRequest
- type DeleteClientResponse
- type DeviceAuthRequest
- type DeviceAuthResponse
- type DeviceCode
- type DeviceCompleteRequest
- type DeviceCompleteResponse
- type DiscoveryRequest
- type DiscoveryResponse
- type Grant
- type GrantPathRequest
- type GrantView
- type IssuerInfo
- type JWKSResponse
- type KeyInfo
- type ListClientsRequest
- type ListClientsResponse
- type ListGrantsRequest
- type ListGrantsResponse
- type MemoryStore
- func (s *MemoryStore) ConsumeAuthCode(_ context.Context, code string) (bool, error)
- func (s *MemoryStore) CreateAuthCode(_ context.Context, code *AuthorizationCode) error
- func (s *MemoryStore) CreateClient(_ context.Context, c *OAuth2Client) error
- func (s *MemoryStore) CreateDeviceCode(_ context.Context, dc *DeviceCode) error
- func (s *MemoryStore) CreateSigningKey(_ context.Context, k *SigningKey) error
- func (s *MemoryStore) DeleteClient(_ context.Context, clientID id.OAuth2ClientID) error
- func (s *MemoryStore) DeleteExpiredAuthCodes(_ context.Context, before time.Time, batch int) (int64, error)
- func (s *MemoryStore) DeleteExpiredDeviceCodes(_ context.Context) error
- func (s *MemoryStore) DeleteExpiredDeviceCodesBefore(_ context.Context, before time.Time, batch int) (int64, error)
- func (s *MemoryStore) DeleteGrant(_ context.Context, appID id.AppID, userID id.UserID, clientID string) error
- func (s *MemoryStore) GetAuthCode(_ context.Context, code string) (*AuthorizationCode, error)
- func (s *MemoryStore) GetClient(_ context.Context, clientID string) (*OAuth2Client, error)
- func (s *MemoryStore) GetClientByID(_ context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
- func (s *MemoryStore) GetDeviceCodeByDeviceCode(_ context.Context, deviceCode string) (*DeviceCode, error)
- func (s *MemoryStore) GetDeviceCodeByUserCode(_ context.Context, userCode string) (*DeviceCode, error)
- func (s *MemoryStore) GetGrant(_ context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)
- func (s *MemoryStore) ListClients(_ context.Context, appID id.AppID) ([]*OAuth2Client, error)
- func (s *MemoryStore) ListGrantsByUser(_ context.Context, appID id.AppID, userID id.UserID) ([]*Grant, error)
- func (s *MemoryStore) ListSigningKeys(_ context.Context, scope string) ([]*SigningKey, error)
- func (s *MemoryStore) RetireSigningKey(_ context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
- func (s *MemoryStore) UpdateClient(_ context.Context, c *OAuth2Client) error
- func (s *MemoryStore) UpdateDeviceCode(_ context.Context, dc *DeviceCode) error
- func (s *MemoryStore) UpsertGrant(_ context.Context, g *Grant) error
- type MongoStore
- func (s *MongoStore) ConsumeAuthCode(ctx context.Context, code string) (bool, error)
- func (s *MongoStore) CreateAuthCode(ctx context.Context, code *AuthorizationCode) error
- func (s *MongoStore) CreateClient(ctx context.Context, c *OAuth2Client) error
- func (s *MongoStore) CreateDeviceCode(ctx context.Context, dc *DeviceCode) error
- func (s *MongoStore) CreateSigningKey(ctx context.Context, k *SigningKey) error
- func (s *MongoStore) DeleteClient(ctx context.Context, clientID id.OAuth2ClientID) error
- func (s *MongoStore) DeleteExpiredAuthCodes(ctx context.Context, before time.Time, batch int) (int64, error)
- func (s *MongoStore) DeleteExpiredDeviceCodes(ctx context.Context) error
- func (s *MongoStore) DeleteExpiredDeviceCodesBefore(ctx context.Context, before time.Time, batch int) (int64, error)
- func (s *MongoStore) DeleteGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) error
- func (s *MongoStore) GetAuthCode(ctx context.Context, code string) (*AuthorizationCode, error)
- func (s *MongoStore) GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
- func (s *MongoStore) GetClientByID(ctx context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
- func (s *MongoStore) GetDeviceCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
- func (s *MongoStore) GetDeviceCodeByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
- func (s *MongoStore) GetGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)
- func (s *MongoStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
- func (s *MongoStore) ListGrantsByUser(ctx context.Context, appID id.AppID, userID id.UserID) ([]*Grant, error)
- func (s *MongoStore) ListSigningKeys(ctx context.Context, scope string) ([]*SigningKey, error)
- func (s *MongoStore) RetireSigningKey(ctx context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
- func (s *MongoStore) UpdateClient(ctx context.Context, c *OAuth2Client) error
- func (s *MongoStore) UpdateDeviceCode(ctx context.Context, dc *DeviceCode) error
- func (s *MongoStore) UpsertGrant(ctx context.Context, g *Grant) error
- type OAuth2Client
- type OAuth2Error
- type OAuth2HTTPError
- type Plugin
- func (p *Plugin) CreateClientFor(ctx context.Context, appID id.AppID, req CreateClientRequest) (*CreateClientResponse, error)
- func (p *Plugin) DeclareSettings(m *settings.Manager) error
- func (p *Plugin) DeleteClientFor(ctx context.Context, appID id.AppID, clientID string) error
- func (p *Plugin) EvaluateConsent(ctx context.Context, clientID string, userID id.UserID, orgID id.OrgID, ...) error
- func (p *Plugin) GetClientFor(ctx context.Context, appID id.AppID, clientID string) (*OAuth2Client, error)
- func (p *Plugin) IssuerInfo(ctx context.Context, appID id.AppID) (IssuerInfo, error)
- func (p *Plugin) ListClientsFor(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
- func (p *Plugin) MigrationGroups(driverName string) []*migrate.Group
- func (p *Plugin) Name() string
- func (p *Plugin) OnInit(ctx context.Context, engine plugin.Engine) error
- func (p *Plugin) RegisterContract(d *dispatcher.Dispatcher, reg contract.Registry, wreg contract.WardenRegistry, ...) error
- func (p *Plugin) RegisterRootRoutes(router forge.Router) error
- func (p *Plugin) RegisterRoutes(router forge.Router) error
- func (p *Plugin) RotateClientSecretFor(ctx context.Context, appID id.AppID, clientID string) (secret string, err error)
- func (p *Plugin) RotateSigningKey(ctx context.Context, appID id.AppID) (KeyInfo, error)
- func (p *Plugin) SetConsentGate(g ConsentGate)
- func (p *Plugin) SetDynamicRegistrationForTest(enabled bool)
- func (p *Plugin) SetOAuth2Store(s Store)
- func (p *Plugin) SetStore(s store.Store)
- func (p *Plugin) SigningKeys(ctx context.Context, appID id.AppID) (issuer string, keys []KeyInfo, err error)
- func (p *Plugin) SweepRetention(ctx context.Context, cutoff func(kind string) time.Time, batch int) (int64, error)
- func (p *Plugin) UpdateClientFor(ctx context.Context, appID id.AppID, clientID string, req UpdateClientRequest) (*OAuth2Client, error)
- type PostgresStore
- func (s *PostgresStore) ConsumeAuthCode(ctx context.Context, code string) (bool, error)
- func (s *PostgresStore) CreateAuthCode(ctx context.Context, code *AuthorizationCode) error
- func (s *PostgresStore) CreateClient(ctx context.Context, c *OAuth2Client) error
- func (s *PostgresStore) CreateDeviceCode(ctx context.Context, dc *DeviceCode) error
- func (s *PostgresStore) CreateSigningKey(ctx context.Context, k *SigningKey) error
- func (s *PostgresStore) DeleteClient(ctx context.Context, clientID id.OAuth2ClientID) error
- func (s *PostgresStore) DeleteExpiredAuthCodes(ctx context.Context, before time.Time, batch int) (int64, error)
- func (s *PostgresStore) DeleteExpiredDeviceCodes(ctx context.Context) error
- func (s *PostgresStore) DeleteExpiredDeviceCodesBefore(ctx context.Context, before time.Time, batch int) (int64, error)
- func (s *PostgresStore) DeleteGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) error
- func (s *PostgresStore) GetAuthCode(ctx context.Context, code string) (*AuthorizationCode, error)
- func (s *PostgresStore) GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
- func (s *PostgresStore) GetClientByID(ctx context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
- func (s *PostgresStore) GetDeviceCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
- func (s *PostgresStore) GetDeviceCodeByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
- func (s *PostgresStore) GetGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)
- func (s *PostgresStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
- func (s *PostgresStore) ListGrantsByUser(ctx context.Context, appID id.AppID, userID id.UserID) ([]*Grant, error)
- func (s *PostgresStore) ListSigningKeys(ctx context.Context, scope string) ([]*SigningKey, error)
- func (s *PostgresStore) RetireSigningKey(ctx context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
- func (s *PostgresStore) UpdateClient(ctx context.Context, c *OAuth2Client) error
- func (s *PostgresStore) UpdateDeviceCode(ctx context.Context, dc *DeviceCode) error
- func (s *PostgresStore) UpsertGrant(ctx context.Context, g *Grant) error
- type ProtectedResource
- type ProtectedResourceMetadata
- type ProtectedResourceRequest
- type RateLimit
- type RegisterClientRequest
- type RegisterClientResponse
- type RegistrationRequest
- type RevokeRequest
- type RotateClientSecretRequest
- type RotateClientSecretResponse
- type SigningKey
- type SqliteStore
- func (s *SqliteStore) ConsumeAuthCode(ctx context.Context, code string) (bool, error)
- func (s *SqliteStore) CreateAuthCode(ctx context.Context, code *AuthorizationCode) error
- func (s *SqliteStore) CreateClient(ctx context.Context, c *OAuth2Client) error
- func (s *SqliteStore) CreateDeviceCode(ctx context.Context, dc *DeviceCode) error
- func (s *SqliteStore) CreateSigningKey(ctx context.Context, k *SigningKey) error
- func (s *SqliteStore) DeleteClient(ctx context.Context, clientID id.OAuth2ClientID) error
- func (s *SqliteStore) DeleteExpiredAuthCodes(ctx context.Context, before time.Time, batch int) (int64, error)
- func (s *SqliteStore) DeleteExpiredDeviceCodes(ctx context.Context) error
- func (s *SqliteStore) DeleteExpiredDeviceCodesBefore(ctx context.Context, before time.Time, batch int) (int64, error)
- func (s *SqliteStore) DeleteGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) error
- func (s *SqliteStore) GetAuthCode(ctx context.Context, code string) (*AuthorizationCode, error)
- func (s *SqliteStore) GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
- func (s *SqliteStore) GetClientByID(ctx context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
- func (s *SqliteStore) GetDeviceCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
- func (s *SqliteStore) GetDeviceCodeByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
- func (s *SqliteStore) GetGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)
- func (s *SqliteStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
- func (s *SqliteStore) ListGrantsByUser(ctx context.Context, appID id.AppID, userID id.UserID) ([]*Grant, error)
- func (s *SqliteStore) ListSigningKeys(ctx context.Context, scope string) ([]*SigningKey, error)
- func (s *SqliteStore) RetireSigningKey(ctx context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
- func (s *SqliteStore) UpdateClient(ctx context.Context, c *OAuth2Client) error
- func (s *SqliteStore) UpdateDeviceCode(ctx context.Context, dc *DeviceCode) error
- func (s *SqliteStore) UpsertGrant(ctx context.Context, g *Grant) error
- type StandardClaims
- type Store
- type TokenRequest
- type TokenResponse
- type UpdateClientRequest
- type UpdateClientResponse
- type UpdateRegistrationRequest
- type UserInfo
- type UserInfoRequest
Constants ¶
const ( DeviceCodeStatusPending = "pending" DeviceCodeStatusAuthorized = "authorized" DeviceCodeStatusDenied = "denied" DeviceCodeStatusConsumed = "consumed" )
Device code status constants.
Variables ¶
var ( ErrClientNotFound = errors.New("oauth2: client not found") ErrCodeNotFound = errors.New("oauth2: authorization code not found") ErrDeviceCodeNotFound = errors.New("oauth2: device code not found") ErrGrantNotFound = errors.New("oauth2: grant not found") )
Store errors.
var ErrKeyTooNew = errors.New("oauth2: the current signing key is too new to rotate")
ErrKeyTooNew is returned when a manual rotation is asked for while the active key is younger than manualRotationMinAge.
var ErrOperatorKey = errors.New("oauth2: the signing key for this issuer is managed by configuration")
ErrOperatorKey is returned when a rotation is asked of an issuer whose key comes from Config. Rotating it means changing the configuration.
ErrSharedKey is returned when the dashboard asks to rotate the deployment issuer's managed key. Every app that has not opted into its own issuer signs with it, so one app's operator must not rotate it; it rotates on IDTokenKeyRotation.
var ErrSigningKeyConflict = errors.New("oauth2: signing key generation already exists")
ErrSigningKeyConflict is returned by CreateSigningKey when a key with the same (Scope, Generation) already exists.
var MongoMigrations = migrate.NewGroup("authsome-oauth2", migrate.DependsOn("authsome"))
MongoMigrations is the MongoDB migration group for the OAuth2 plugin. The only thing it creates is the signing key uniqueness index: mongo makes collections on first write, and the unique index is what makes concurrent CreateSigningKey calls for one (scope, generation) pick a single winner.
var PostgresMigrations = migrate.NewGroup("authsome-oauth2", migrate.DependsOn("authsome"))
PostgresMigrations is the postgres migration group for the OAuth2 provider plugin.
var SettingAppIssuer = settings.Define("oauth2.app_issuer", false, settings.WithScopes(settings.ScopeApp), settings.WithDisplayName("Per-app OIDC issuer"), settings.WithDescription("Give this app its own issuer at {issuer}/apps/{slug}. Turning this on or off, or renaming the app's slug, changes the issuer for every OAuth2 client of this app, and they will reject tokens until they are reconfigured."), settings.WithCategory("OpenID Connect"), )
SettingAppIssuer lets an app have its own OpenID Connect issuer.
var SqliteMigrations = migrate.NewGroup("authsome-oauth2", migrate.DependsOn("authsome"))
SqliteMigrations is the SQLite migration group for the OAuth2 provider plugin.
Functions ¶
This section is empty.
Types ¶
type AppSlugRequest ¶ added in v1.9.1
type AppSlugRequest struct {
Slug string `path:"slug"`
}
AppSlugRequest addresses one app's issuer by its slug.
type AuthServerMetadata ¶
type AuthServerMetadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
JWKSURI string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ScopesSupported []string `json:"scopes_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
// ResourceIndicatorsSupported advertises RFC 8707.
//
// This name is not registered. RFC 8707 registers the `resource`
// parameter and the `invalid_target` error and defines no discovery
// metadata at all, and the RFC 8414 IANA registry has no entry for it.
// It is the convention that came out of the MCP ecosystem and it is what
// clients look for, so do not read it as standardised.
ResourceIndicatorsSupported bool `json:"resource_indicators_supported"`
// DPoPSigningAlgValuesSupported lists the JWS algorithms accepted in DPoP
// proofs (RFC 9449 section 5.1). Advertised unconditionally: this document
// is not app scoped, and a server that can validate ES256 proofs can do so
// whoever is asking. Per-client mode is discovered at registration.
DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported"`
ResponseModesSupported []string `json:"response_modes_supported"`
}
AuthServerMetadata is the RFC 8414 authorization server metadata document. It carries every field the OIDC discovery document does, plus the RFC 7591 registration endpoint. handleDiscovery hand-maps buildAuthServerMetadata's output onto the OIDC DiscoveryResponse struct field by field; the two are not one shared type, so TestMetadata_OIDCAndAuthServerAgree compares the full set of JSON keys both documents actually advertise, to catch a field added to one and not the other.
type AuthorizationCode ¶
type AuthorizationCode struct {
ID id.AuthCodeID `json:"id"`
Code string `json:"-"`
ClientID string `json:"client_id"`
UserID id.UserID `json:"user_id"`
AppID id.AppID `json:"app_id"`
RedirectURI string `json:"redirect_uri"`
Scopes []string `json:"scopes"`
// Resources is the audience the user authorized this code for. The token
// endpoint may narrow it further but never widen it.
Resources []string `json:"resources"`
CodeChallenge string `json:"-"` // PKCE
CodeChallengeMethod string `json:"-"` // "S256" or "plain"
ExpiresAt time.Time `json:"expires_at"`
Consumed bool `json:"consumed"`
CreatedAt time.Time `json:"created_at"`
// Nonce is the client's OpenID Connect nonce from the authorization
// request, empty when it sent none. It is echoed into the id_token.
Nonce string `json:"-"`
// AuthTime is when the user authenticated: the creation time of the
// session that approved the request. Zero on codes minted before this
// field existed.
AuthTime time.Time `json:"-"`
}
AuthorizationCode represents a short-lived authorization code.
type AuthorizeRequest ¶
type AuthorizeRequest struct {
ResponseType string `query:"response_type"`
ClientID string `query:"client_id"`
// Optional per RFC 6749 §4.1.1 — when omitted the client must have exactly
// one registered URI, which resolveRedirectURI selects. Marking it
// required here rejected such requests at the binder, before the handler
// could apply that rule.
RedirectURI string `query:"redirect_uri,omitempty"`
Scope string `query:"scope,omitempty"`
State string `query:"state,omitempty"`
CodeChallenge string `query:"code_challenge,omitempty"`
CodeChallengeMethod string `query:"code_challenge_method,omitempty"`
// RFC 8707, repeatable. A field rather than a raw-request read, because
// only a declared field reaches the OpenAPI document, and only a described
// parameter reaches the generated clients.
Resource []string `query:"resource,omitempty"`
// Prompt=consent forces the consent page even when a grant covers the
// request (OpenID Connect Core 3.1.2.1).
//
// It stays last. The generated clients take query parameters positionally
// in field order, so a field added above Resource moves it, and a caller
// passing resources by position would hand them to this one instead.
Prompt string `query:"prompt,omitempty"`
// Nonce is the OpenID Connect replay guard (Core 3.1.2.1), echoed into the
// id_token. At most maxNonceBytes. It stays last for the same reason
// Prompt did: generated clients pass query parameters positionally.
Nonce string `query:"nonce,omitempty"`
}
AuthorizeRequest is the OAuth2 authorization request.
type Config ¶
type Config struct {
// Issuer is the OAuth2 issuer URL (e.g. "https://auth.example.com").
Issuer string
// AuthCodeTTL is the lifetime of authorization codes (default: 10 minutes).
AuthCodeTTL time.Duration
// AccessTokenTTL is the lifetime of access tokens (default: 1 hour).
AccessTokenTTL time.Duration
// DeviceCodeTTL is the lifetime of device authorization codes (default: 10 minutes).
DeviceCodeTTL time.Duration
// DeviceCodeInterval is the minimum polling interval in seconds (default: 5).
DeviceCodeInterval int
// VerificationURI is the customizable user verification URL for the device flow.
// If empty, defaults to "{issuer}/v1/oauth/device".
// Set this to a custom URL (e.g. "https://myapp.com/device") when using
// an external UI like authsome-ui to host the verification page.
VerificationURI string
// DynamicRegistration enables RFC 7591 registration. Off by default:
// an upgrade must never open a public registration endpoint on its own.
DynamicRegistration bool
// RegistrationAppID is the app dynamic clients belong to when the
// request carries no resolvable publishable key. Leave empty on a
// multi-tenant deployment so an unkeyed request is refused rather
// than pooled into somebody's app.
RegistrationAppID string
// DynamicRegistrationScopes is the allowlist a dynamic client's
// requested scopes are intersected against. Defaults to openid,
// profile, email and offline_access.
DynamicRegistrationScopes []string
// RegistrationRateLimit caps POST /register per client IP.
// Defaults to 10 per hour.
RegistrationRateLimit RateLimit
// ProtectedResources declares additional RFC 9728 resource identifiers,
// keyed by the path suffix they are served under. AuthSome always
// describes itself at the unsuffixed path regardless of this map.
ProtectedResources map[string]ProtectedResource
// ConsentGate, if set, is consulted before every authorization code is
// issued. See SetConsentGate for post-construction wiring.
ConsentGate ConsentGate
// IDTokenSigningKey is a PEM private key (RSA of at least 2048 bits, or
// EC P-256) that signs id_tokens for the deployment issuer. When it or
// IDTokenSigningKeyFile is set, no key is generated for that issuer.
IDTokenSigningKey []byte
IDTokenSigningKeyFile string
// IDTokenSigningKeyID is the kid. Defaults to the key's RFC 7638
// thumbprint.
IDTokenSigningKeyID string
// IDTokenPreviousKeys are PEM keys, public or private, that are published
// in JWKS but never sign. Keep a rotated-out key here until every token it
// signed has expired.
//
// A previous key's kid is always its RFC 7638 thumbprint. If the old key
// signed under IDTokenSigningKeyID, its tokens carry that kid, so either
// keep it as the signing key until they expire or accept that they stop
// verifying when the kid changes.
IDTokenPreviousKeys [][]byte
// IDTokenTTL is the lifetime of id_tokens (default: 1 hour).
IDTokenTTL time.Duration
// IDTokenKeyRotation is how old a managed signing key gets before the
// next one replaces it (default: 90 days).
IDTokenKeyRotation time.Duration
// IDTokenKeyRetireGrace is how long a replaced managed key stays in
// JWKS so tokens it signed still verify (default: 7 days).
IDTokenKeyRetireGrace time.Duration
}
Config configures the OAuth2 provider plugin.
type ConsentDecision ¶
type ConsentDecision struct {
CID string `json:"cid"`
CSRF string `json:"csrf"`
Decision string `json:"decision"`
}
ConsentDecision is the JSON form of the decision body.
type ConsentGate ¶
type ConsentGate interface {
// Evaluate returns a non-nil error to refuse the authorization. The error
// is surfaced to the caller, so it should be a forge HTTP error.
//
// orgID is the caller's active organization, taken from the session that
// is granting consent. It is the zero value (id.OrgID{}) whenever that
// session is not org-scoped — that is not an assertion that no policy
// applies. An implementation must not treat a zero orgID as permission to
// skip its policy check; do the same evaluation you would for any org.
//
// appID is the app the OAuth2 client (clientID) belongs to, resolved by
// the caller from its own client/device-code record rather than trusted
// from the request. A gate that resolves clientID to some other record
// globally (not scoped to an app) must bind its decision to appID, or a
// client_id collision across two apps lets one app's registration govern
// the other's client.
Evaluate(ctx context.Context, clientID string, userID id.UserID, orgID id.OrgID, appID id.AppID, scopes []string) error
}
ConsentGate is an optional hook another plugin implements to veto an authorization before a code is issued. agentauth registers itself as the gate so an org's policy on delegated agents is enforced at the moment a user consents, which is the only point where "may this agent touch our data" is a well-formed question.
A nil gate allows everything, so the provider's behavior is unchanged when no plugin registers one.
type ConsentResult ¶
type ConsentResult struct {
RedirectURL string `json:"redirect_url"`
}
ConsentResult tells a JSON caller where to send the browser next.
type ConsentView ¶
type ConsentView struct {
CID string `json:"cid"`
CSRF string `json:"csrf"`
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
Scopes []string `json:"scopes"`
// Action is the path the decision posts to.
Action string `json:"action"`
}
ConsentView is what the page and the JSON form of it show.
type CreateClientRequest ¶
type CreateClientRequest struct {
AppID string `json:"app_id"`
Name string `json:"name"`
RedirectURIs []string `json:"redirect_uris"`
Scopes []string `json:"scopes,omitempty"`
Resources []string `json:"resources,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
Public bool `json:"public,omitempty"`
DPoPMode string `json:"dpop_mode,omitempty"`
// FirstParty marks a client the operator owns; authorization skips the
// consent page for it.
FirstParty bool `json:"first_party,omitempty"`
}
CreateClientRequest is the admin request to create an OAuth2 client.
type CreateClientResponse ¶
type CreateClientResponse struct {
ID string `json:"id"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"` // Only returned once at creation
Name string `json:"name"`
RedirectURIs []string `json:"redirect_uris"`
Scopes []string `json:"scopes"`
Resources []string `json:"resources"`
GrantTypes []string `json:"grant_types"`
Public bool `json:"public"`
}
CreateClientResponse is returned when an OAuth2 client is created.
type DeleteClientRequest ¶
type DeleteClientRequest struct {
ClientID string `path:"clientId"`
}
DeleteClientRequest deletes a client by internal ID.
The tag is `path`, not `param`: forge binds path parameters from the former (internal/router/openapi_request_schema.go). Under `param` the field never bound, so request validation rejected every call with "ClientID: field is required" and the handler never ran.
type DeleteClientResponse ¶
type DeleteClientResponse struct {
Status string `json:"status"`
}
DeleteClientResponse is the response after deleting a client.
type DeviceAuthRequest ¶
type DeviceAuthRequest struct {
ClientID string `json:"client_id" form:"client_id"`
Scope string `json:"scope,omitempty" form:"scope,omitempty"`
// RFC 8707, repeatable. RFC 8628 section 3.1 makes this endpoint
// form-encoded, the same as the token endpoint, and bindFormParam fills a
// []string from every occurrence. The document describes the field from
// these tags, so this endpoint needs no separate declaration the way
// /authorize does.
Resource []string `json:"resource,omitempty" form:"resource,omitempty"`
}
DeviceAuthRequest is the device authorization request (RFC 8628 Section 3.1).
type DeviceAuthResponse ¶
type DeviceAuthResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
DeviceAuthResponse is the device authorization response (RFC 8628 Section 3.2).
type DeviceCode ¶
type DeviceCode struct {
ID id.DeviceCodeID `json:"id"`
DeviceCode string `json:"-"` // opaque polling token (256-bit hex)
UserCode string `json:"user_code"` // short human-readable code, e.g. "BCDF-GHJK"
ClientID string `json:"client_id"` // OAuth2 client_id
AppID id.AppID `json:"app_id"`
Scopes []string `json:"scopes"`
// Resources is the audience this device authorization was granted for.
Resources []string `json:"resources"`
VerificationURI string `json:"verification_uri"` // where the user goes
ExpiresAt time.Time `json:"expires_at"`
Interval int `json:"interval"` // polling interval in seconds
Status string `json:"status"` // "pending", "authorized", "denied", "consumed"
UserID id.UserID `json:"user_id,omitempty"` // set when user authorizes
LastPolledAt time.Time `json:"last_polled_at,omitempty"` // last time the CLI polled for this code
CreatedAt time.Time `json:"created_at"`
// AuthTime is when the approving user authenticated, set at approval from
// that user's session. Zero until approved.
AuthTime time.Time `json:"-"`
}
DeviceCode represents an OAuth2 device authorization code (RFC 8628).
type DeviceCompleteRequest ¶
type DeviceCompleteRequest struct {
UserCode string `json:"user_code" form:"user_code"`
Action string `json:"action" form:"action"` // "approve" or "deny"
}
DeviceCompleteRequest allows an authenticated user to approve or deny a device code.
type DeviceCompleteResponse ¶
type DeviceCompleteResponse struct {
Status string `json:"status"` // "authorized" or "denied"
}
DeviceCompleteResponse is the response after completing device authorization.
type DiscoveryResponse ¶
type DiscoveryResponse struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
JWKSURI string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ScopesSupported []string `json:"scopes_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
// ResourceIndicatorsSupported advertises RFC 8707.
//
// This name is not registered. RFC 8707 registers the `resource`
// parameter and the `invalid_target` error and defines no discovery
// metadata at all, and the RFC 8414 IANA registry has no entry for it.
// It is the convention that came out of the MCP ecosystem and it is what
// clients look for, so do not read it as standardised.
ResourceIndicatorsSupported bool `json:"resource_indicators_supported"`
// DPoPSigningAlgValuesSupported lists the JWS algorithms accepted in DPoP
// proofs (RFC 9449 section 5.1).
DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported"`
ResponseModesSupported []string `json:"response_modes_supported"`
}
DiscoveryResponse is the OpenID Connect discovery document.
type Grant ¶
type Grant struct {
ID id.OAuth2GrantID `json:"id"`
AppID id.AppID `json:"app_id"`
UserID id.UserID `json:"user_id"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Grant records that a user approved a client for a set of scopes in an app. Authorization consults it to decide whether to show the consent page, and the user can list and revoke grants at /v1/me/oauth/grants.
type GrantPathRequest ¶
type GrantPathRequest struct {
ClientID string `path:"clientId"`
}
GrantPathRequest names the client whose grant is revoked.
type GrantView ¶
type GrantView struct {
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
Scopes []string `json:"scopes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
GrantView is one entry of the signed-in user's grant list.
type IssuerInfo ¶ added in v1.9.1
type IssuerInfo struct {
Issuer string
DiscoveryURL string
JWKSURI string
// AppIssuer is true when the app has its own issuer rather than the
// deployment's.
AppIssuer bool
Algs []string
}
IssuerInfo is what an operator needs to point a relying party at the issuer an app's id_tokens name.
type JWKSResponse ¶ added in v1.9.1
JWKSResponse is the JSON Web Key Set the provider publishes.
type KeyInfo ¶ added in v1.9.1
type KeyInfo struct {
KID string
Alg string
// Source is "operator" or "managed".
Source string
// Status is "active", "previous" or "retiring".
Status string
CreatedAt time.Time
RetiresAt *time.Time
}
KeyInfo describes one signing key for the admin dashboard. It never carries key material.
type ListClientsRequest ¶
type ListClientsRequest struct {
AppID string `query:"app_id"`
}
ListClientsRequest is the request to list OAuth2 clients.
type ListClientsResponse ¶
type ListClientsResponse struct {
Clients []*OAuth2Client `json:"clients"`
}
ListClientsResponse is the response listing OAuth2 clients.
type ListGrantsRequest ¶
type ListGrantsRequest struct{}
ListGrantsRequest carries nothing; the user comes from the session.
type ListGrantsResponse ¶
type ListGrantsResponse struct {
Grants []GrantView `json:"grants"`
}
ListGrantsResponse is the body of GET /v1/me/oauth/grants.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory implementation of the OAuth2 Store for testing.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore creates a new in-memory OAuth2 store.
func (*MemoryStore) ConsumeAuthCode ¶
func (*MemoryStore) CreateAuthCode ¶
func (s *MemoryStore) CreateAuthCode(_ context.Context, code *AuthorizationCode) error
func (*MemoryStore) CreateClient ¶
func (s *MemoryStore) CreateClient(_ context.Context, c *OAuth2Client) error
func (*MemoryStore) CreateDeviceCode ¶
func (s *MemoryStore) CreateDeviceCode(_ context.Context, dc *DeviceCode) error
func (*MemoryStore) CreateSigningKey ¶ added in v1.9.1
func (s *MemoryStore) CreateSigningKey(_ context.Context, k *SigningKey) error
func (*MemoryStore) DeleteClient ¶
func (s *MemoryStore) DeleteClient(_ context.Context, clientID id.OAuth2ClientID) error
func (*MemoryStore) DeleteExpiredAuthCodes ¶
func (*MemoryStore) DeleteExpiredDeviceCodes ¶
func (s *MemoryStore) DeleteExpiredDeviceCodes(_ context.Context) error
func (*MemoryStore) DeleteExpiredDeviceCodesBefore ¶
func (*MemoryStore) DeleteGrant ¶
func (*MemoryStore) GetAuthCode ¶
func (s *MemoryStore) GetAuthCode(_ context.Context, code string) (*AuthorizationCode, error)
func (*MemoryStore) GetClient ¶
func (s *MemoryStore) GetClient(_ context.Context, clientID string) (*OAuth2Client, error)
func (*MemoryStore) GetClientByID ¶
func (s *MemoryStore) GetClientByID(_ context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
func (*MemoryStore) GetDeviceCodeByDeviceCode ¶
func (s *MemoryStore) GetDeviceCodeByDeviceCode(_ context.Context, deviceCode string) (*DeviceCode, error)
func (*MemoryStore) GetDeviceCodeByUserCode ¶
func (s *MemoryStore) GetDeviceCodeByUserCode(_ context.Context, userCode string) (*DeviceCode, error)
func (*MemoryStore) ListClients ¶
func (s *MemoryStore) ListClients(_ context.Context, appID id.AppID) ([]*OAuth2Client, error)
func (*MemoryStore) ListGrantsByUser ¶
func (*MemoryStore) ListSigningKeys ¶ added in v1.9.1
func (s *MemoryStore) ListSigningKeys(_ context.Context, scope string) ([]*SigningKey, error)
func (*MemoryStore) RetireSigningKey ¶ added in v1.9.1
func (s *MemoryStore) RetireSigningKey(_ context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
func (*MemoryStore) UpdateClient ¶
func (s *MemoryStore) UpdateClient(_ context.Context, c *OAuth2Client) error
func (*MemoryStore) UpdateDeviceCode ¶
func (s *MemoryStore) UpdateDeviceCode(_ context.Context, dc *DeviceCode) error
func (*MemoryStore) UpsertGrant ¶
func (s *MemoryStore) UpsertGrant(_ context.Context, g *Grant) error
type MongoStore ¶
type MongoStore struct {
// contains filtered or unexported fields
}
MongoStore implements oauth2provider.Store using the Grove MongoDB driver.
func NewMongoStore ¶
func NewMongoStore(db *grove.DB) *MongoStore
NewMongoStore creates a new MongoDB-backed OAuth2 store.
func (*MongoStore) ConsumeAuthCode ¶
func (*MongoStore) CreateAuthCode ¶
func (s *MongoStore) CreateAuthCode(ctx context.Context, code *AuthorizationCode) error
func (*MongoStore) CreateClient ¶
func (s *MongoStore) CreateClient(ctx context.Context, c *OAuth2Client) error
func (*MongoStore) CreateDeviceCode ¶
func (s *MongoStore) CreateDeviceCode(ctx context.Context, dc *DeviceCode) error
func (*MongoStore) CreateSigningKey ¶ added in v1.9.1
func (s *MongoStore) CreateSigningKey(ctx context.Context, k *SigningKey) error
func (*MongoStore) DeleteClient ¶
func (s *MongoStore) DeleteClient(ctx context.Context, clientID id.OAuth2ClientID) error
func (*MongoStore) DeleteExpiredAuthCodes ¶
func (*MongoStore) DeleteExpiredDeviceCodes ¶
func (s *MongoStore) DeleteExpiredDeviceCodes(ctx context.Context) error
func (*MongoStore) DeleteExpiredDeviceCodesBefore ¶
func (*MongoStore) DeleteGrant ¶
func (*MongoStore) GetAuthCode ¶
func (s *MongoStore) GetAuthCode(ctx context.Context, code string) (*AuthorizationCode, error)
func (*MongoStore) GetClient ¶
func (s *MongoStore) GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
func (*MongoStore) GetClientByID ¶
func (s *MongoStore) GetClientByID(ctx context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
func (*MongoStore) GetDeviceCodeByDeviceCode ¶
func (s *MongoStore) GetDeviceCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
func (*MongoStore) GetDeviceCodeByUserCode ¶
func (s *MongoStore) GetDeviceCodeByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
func (*MongoStore) ListClients ¶
func (s *MongoStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
func (*MongoStore) ListGrantsByUser ¶
func (*MongoStore) ListSigningKeys ¶ added in v1.9.1
func (s *MongoStore) ListSigningKeys(ctx context.Context, scope string) ([]*SigningKey, error)
func (*MongoStore) RetireSigningKey ¶ added in v1.9.1
func (s *MongoStore) RetireSigningKey(ctx context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
RetireSigningKey sets retires_at, overwriting an earlier value. An unknown id matches nothing and is not an error.
func (*MongoStore) UpdateClient ¶
func (s *MongoStore) UpdateClient(ctx context.Context, c *OAuth2Client) error
func (*MongoStore) UpdateDeviceCode ¶
func (s *MongoStore) UpdateDeviceCode(ctx context.Context, dc *DeviceCode) error
func (*MongoStore) UpsertGrant ¶
func (s *MongoStore) UpsertGrant(ctx context.Context, g *Grant) error
type OAuth2Client ¶
type OAuth2Client struct {
ID id.OAuth2ClientID `json:"id"`
AppID id.AppID `json:"app_id"`
Name string `json:"name"`
ClientID string `json:"client_id"`
ClientSecret string `json:"-"` // Hashed; never exposed
RedirectURIs []string `json:"redirect_uris"`
Scopes []string `json:"scopes"`
// Resources is the allowlist of resource identifiers this client may
// request a token for (RFC 8707). Empty means the client may target
// nothing, so a request naming any resource is rejected with
// invalid_target. Deny by default: an empty list is the state every
// existing client is in, and a client that never sends `resource` is
// unaffected either way.
Resources []string `json:"resources"`
GrantTypes []string `json:"grant_types"` // "authorization_code", "client_credentials"
// PrincipalID links this client to the non-human principal it acts as.
// Required for the RFC 8693 token exchange grant and unused by the other
// grants: Engine.ExchangeToken takes a principal.Ref actor and matches it
// against a delegation grant, so a client with no principal has no
// identity to act with and nothing a grant could have been written
// against. Zero for every client that only uses the other grants.
PrincipalID id.ServiceAccountID `json:"principal_id,omitempty"`
Public bool `json:"public"` // Public clients (SPAs, mobile) don't have a secret
// FirstParty marks a client the operator owns. Authorization skips the
// consent page for it, since the user already trusts the application
// they are signing in to. Only the admin API can set it.
FirstParty bool `json:"first_party"`
// TokenEndpointAuthMethod is RFC 7591 token_endpoint_auth_method:
// "none", "client_secret_basic" or "client_secret_post". Every
// enforcement site (plugin.go's PKCE, client-secret and
// client_credentials checks) reads Public, not this field; nothing
// reads TokenEndpointAuthMethod to decide behaviour. The two are kept
// in sync at every write site instead: the admin and dashboard paths
// derive the method from Public, and the dynamic registration path
// derives Public from the method ("none" means public). Two flags that
// can disagree is a bug waiting to happen.
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
// RegistrationTokenHash is the bcrypt hash of the RFC 7592 registration
// access token. Empty for admin-created clients, which is what makes
// them unreachable over the 7592 routes.
RegistrationTokenHash string `json:"-"`
// DynamicallyRegistered records that this client came in over RFC 7591
// rather than the admin surface. It gates the 7592 routes and lets the
// dashboard tell the two populations apart.
DynamicallyRegistered bool `json:"dynamically_registered"`
// ClientSecretExpiresAt is RFC 7591 client_secret_expires_at. Nil means
// the secret never expires.
//
// A pointer, not a bare time.Time: encoding/json never treats a struct as
// empty, so omitempty would not fire and every client serialised through
// ListClientsResponse would carry "0001-01-01T00:00:00Z".
ClientSecretExpiresAt *time.Time `json:"client_secret_expires_at,omitempty"`
// Metadata holds the RFC 7591 fields that carry no behaviour:
// client_uri, logo_uri, contacts, tos_uri, policy_uri, software_id,
// software_version. They only need to round-trip on a 7592 read, so
// they do not each earn a column across four backends.
Metadata map[string]any `json:"metadata,omitempty"`
// DPoPMode is this client's RFC 9449 mode: "off", "optional", "required",
// or empty to inherit the app's setting.
//
// Resolution is monotonic. The effective mode is the stricter of the app's
// and this value, so a client can raise the bar but never lower one the
// app has set.
DPoPMode string `json:"dpop_mode,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
OAuth2Client represents a registered OAuth2 client application.
type OAuth2Error ¶
type OAuth2HTTPError ¶
type OAuth2HTTPError struct {
// contains filtered or unexported fields
}
OAuth2HTTPError wraps OAuth2 error codes as a framework-compatible HTTP error. This allows RFC 8628 device flow errors to flow through the Forge framework's error handler while maintaining the OAuth2 JSON error response format.
func (*OAuth2HTTPError) Error ¶
func (e *OAuth2HTTPError) Error() string
func (*OAuth2HTTPError) ResponseBody ¶
func (e *OAuth2HTTPError) ResponseBody() any
func (*OAuth2HTTPError) StatusCode ¶
func (e *OAuth2HTTPError) StatusCode() int
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the OAuth2 provider plugin.
func (*Plugin) CreateClientFor ¶ added in v1.9.1
func (p *Plugin) CreateClientFor(ctx context.Context, appID id.AppID, req CreateClientRequest) (*CreateClientResponse, error)
CreateClientFor registers a client under appID. req.AppID is ignored: the caller has already decided which app this is for.
func (*Plugin) DeclareSettings ¶ added in v1.9.1
DeclareSettings implements plugin.SettingsProvider.
func (*Plugin) DeleteClientFor ¶ added in v1.9.1
DeleteClientFor deletes one of appID's clients. The row is loaded first: the tenancy check needs its AppID and the store's delete takes only an id.
func (*Plugin) EvaluateConsent ¶
func (p *Plugin) EvaluateConsent(ctx context.Context, clientID string, userID id.UserID, orgID id.OrgID, appID id.AppID, scopes []string) error
EvaluateConsent runs the registered gate, if any.
func (*Plugin) GetClientFor ¶ added in v1.9.1
func (p *Plugin) GetClientFor(ctx context.Context, appID id.AppID, clientID string) (*OAuth2Client, error)
GetClientFor loads one of appID's clients by its internal id.
func (*Plugin) IssuerInfo ¶ added in v1.9.1
IssuerInfo describes the issuer appID's id_tokens are minted under.
func (*Plugin) ListClientsFor ¶ added in v1.9.1
ListClientsFor lists appID's clients. The result is never nil.
func (*Plugin) MigrationGroups ¶
MigrationGroups returns the OAuth2 migration groups for the given driver.
func (*Plugin) RegisterContract ¶
func (p *Plugin) RegisterContract( d *dispatcher.Dispatcher, reg contract.Registry, wreg contract.WardenRegistry, engine plugin.Engine, ) error
func (*Plugin) RegisterRootRoutes ¶
RegisterRootRoutes registers discovery documents at the origin root. These cannot live on the grouped router: a client that only knows the host fetches https://host/.well-known/... with no prefix.
func (*Plugin) RegisterRoutes ¶
RegisterRoutes registers OAuth2 provider HTTP endpoints.
func (*Plugin) RotateClientSecretFor ¶ added in v1.9.1
func (p *Plugin) RotateClientSecretFor(ctx context.Context, appID id.AppID, clientID string) (secret string, err error)
RotateClientSecretFor replaces a confidential client's secret and returns the new one in the clear. It is the only time the secret is readable.
func (*Plugin) RotateSigningKey ¶ added in v1.9.1
RotateSigningKey generates the next managed signing key for appID's own issuer. It only rotates an app's own key: an app on the deployment issuer gets ErrOperatorKey when a configured key serves it and ErrSharedKey otherwise, since that key signs for every app and rotates on its own schedule. An app key younger than an hour gets ErrKeyTooNew.
func (*Plugin) SetConsentGate ¶
func (p *Plugin) SetConsentGate(g ConsentGate)
SetConsentGate registers a gate consulted before every authorization code is issued. Use it when the gate is only available after the provider has been constructed; otherwise set Config.ConsentGate.
func (*Plugin) SetDynamicRegistrationForTest ¶
SetDynamicRegistrationForTest toggles RFC 7591 registration after construction. Tests use it to prove the 7592 routes keep working once registration itself is closed.
func (*Plugin) SetOAuth2Store ¶
SetOAuth2Store allows direct OAuth2 store injection for testing.
func (*Plugin) SigningKeys ¶ added in v1.9.1
func (p *Plugin) SigningKeys(ctx context.Context, appID id.AppID) (issuer string, keys []KeyInfo, err error)
SigningKeys lists the id_token signing keys for appID's issuer, active key first, along with that issuer.
func (*Plugin) SweepRetention ¶
func (p *Plugin) SweepRetention(ctx context.Context, cutoff func(kind string) time.Time, batch int) (int64, error)
SweepRetention implements plugin.RetentionSweeper: authorization codes and device codes that expired before their cutoff are removed in batches.
func (*Plugin) UpdateClientFor ¶ added in v1.9.1
func (p *Plugin) UpdateClientFor(ctx context.Context, appID id.AppID, clientID string, req UpdateClientRequest) (*OAuth2Client, error)
UpdateClientFor applies the fields req carries to one of appID's clients. req.ClientID is not read: clientID is the only identifier that counts, so a body cannot redirect the write.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore implements oauth2provider.Store using the Grove ORM with PostgreSQL.
func NewPostgresStore ¶
func NewPostgresStore(db *grove.DB) *PostgresStore
NewPostgresStore creates a new PostgreSQL-backed OAuth2 store.
func (*PostgresStore) ConsumeAuthCode ¶
func (*PostgresStore) CreateAuthCode ¶
func (s *PostgresStore) CreateAuthCode(ctx context.Context, code *AuthorizationCode) error
func (*PostgresStore) CreateClient ¶
func (s *PostgresStore) CreateClient(ctx context.Context, c *OAuth2Client) error
func (*PostgresStore) CreateDeviceCode ¶
func (s *PostgresStore) CreateDeviceCode(ctx context.Context, dc *DeviceCode) error
func (*PostgresStore) CreateSigningKey ¶ added in v1.9.1
func (s *PostgresStore) CreateSigningKey(ctx context.Context, k *SigningKey) error
func (*PostgresStore) DeleteClient ¶
func (s *PostgresStore) DeleteClient(ctx context.Context, clientID id.OAuth2ClientID) error
func (*PostgresStore) DeleteExpiredAuthCodes ¶
func (*PostgresStore) DeleteExpiredDeviceCodes ¶
func (s *PostgresStore) DeleteExpiredDeviceCodes(ctx context.Context) error
func (*PostgresStore) DeleteExpiredDeviceCodesBefore ¶
func (*PostgresStore) DeleteGrant ¶
func (*PostgresStore) GetAuthCode ¶
func (s *PostgresStore) GetAuthCode(ctx context.Context, code string) (*AuthorizationCode, error)
func (*PostgresStore) GetClient ¶
func (s *PostgresStore) GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
func (*PostgresStore) GetClientByID ¶
func (s *PostgresStore) GetClientByID(ctx context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
func (*PostgresStore) GetDeviceCodeByDeviceCode ¶
func (s *PostgresStore) GetDeviceCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
func (*PostgresStore) GetDeviceCodeByUserCode ¶
func (s *PostgresStore) GetDeviceCodeByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
func (*PostgresStore) ListClients ¶
func (s *PostgresStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
func (*PostgresStore) ListGrantsByUser ¶
func (*PostgresStore) ListSigningKeys ¶ added in v1.9.1
func (s *PostgresStore) ListSigningKeys(ctx context.Context, scope string) ([]*SigningKey, error)
func (*PostgresStore) RetireSigningKey ¶ added in v1.9.1
func (s *PostgresStore) RetireSigningKey(ctx context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
RetireSigningKey sets RetiresAt, overwriting an earlier value. An unknown id updates no rows and is not an error.
func (*PostgresStore) UpdateClient ¶
func (s *PostgresStore) UpdateClient(ctx context.Context, c *OAuth2Client) error
func (*PostgresStore) UpdateDeviceCode ¶
func (s *PostgresStore) UpdateDeviceCode(ctx context.Context, dc *DeviceCode) error
func (*PostgresStore) UpsertGrant ¶
func (s *PostgresStore) UpsertGrant(ctx context.Context, g *Grant) error
type ProtectedResource ¶
ProtectedResource describes one RFC 9728 protected resource.
type ProtectedResourceMetadata ¶
type ProtectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
BearerMethodsSupported []string `json:"bearer_methods_supported"`
}
ProtectedResourceMetadata is the RFC 9728 protected resource metadata document. An MCP client fetches this first and follows authorization_servers to the RFC 8414 document.
type ProtectedResourceRequest ¶
type ProtectedResourceRequest struct {
Path string `path:"resourcePath"`
}
ProtectedResourceRequest addresses one configured resource by the path suffix it is served under (RFC 9728 section 3.1).
type RegisterClientRequest ¶
type RegisterClientRequest struct {
RedirectURIs []string `json:"redirect_uris"`
ClientName string `json:"client_name,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
TOSURI string `json:"tos_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
SoftwareID string `json:"software_id,omitempty"`
SoftwareVersion string `json:"software_version,omitempty"`
Contacts []string `json:"contacts,omitempty"`
}
RegisterClientRequest is an RFC 7591 client registration request.
type RegisterClientResponse ¶
type RegisterClientResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"`
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
RegistrationClientURI string `json:"registration_client_uri,omitempty"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
Scope string `json:"scope"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
ClientName string `json:"client_name,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
TOSURI string `json:"tos_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
SoftwareID string `json:"software_id,omitempty"`
SoftwareVersion string `json:"software_version,omitempty"`
Contacts []string `json:"contacts,omitempty"`
}
RegisterClientResponse is an RFC 7591 client information response. It is also what the RFC 7592 read and update endpoints return.
type RegistrationRequest ¶
type RegistrationRequest struct {
ClientID string `path:"clientId" json:"-"`
}
RegistrationRequest addresses one registration by its client_id.
ClientID is tagged json:"-" on purpose, not left untagged. forge's BindRequest binds path params first and then JSON-decodes the body over the same struct, and encoding/json matches an untagged field case-insensitively — so a body key "clientId" would otherwise be able to overwrite the path value after the fact. This struct has no other field for a body to collide with today, which is the only reason leaving it untagged would happen to be safe right now; json:"-" keeps it safe if a body field is ever added here later. See UpdateRegistrationRequest below for the case where that actually bit us.
type RevokeRequest ¶
type RevokeRequest struct {
Token string `json:"token" form:"token"`
TokenTypeHint string `json:"token_type_hint,omitempty" form:"token_type_hint"`
ClientID string `json:"client_id,omitempty" form:"client_id"`
ClientSecret string `json:"client_secret,omitempty" form:"client_secret"`
}
RevokeRequest is the OAuth2 revocation request.
type RotateClientSecretRequest ¶
type RotateClientSecretRequest struct {
ClientID string `path:"clientId"`
}
RotateClientSecretRequest identifies the client whose secret to replace. There is no body: the new secret is server-generated, the way it is at creation. Letting a caller supply one would invite weak or reused values.
type RotateClientSecretResponse ¶
type RotateClientSecretResponse struct {
ID string `json:"id"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
}
RotateClientSecretResponse carries the new secret in the clear. This is the only time it is ever readable, matching CreateClientResponse.
type SigningKey ¶ added in v1.9.1
type SigningKey struct {
ID id.OAuth2SigningKeyID
// Scope is "" for the deployment issuer, else the app id.
Scope string
Alg string // "RS256"
// PrivateKey is sealed with engine.TokenEncryptor().
PrivateKey string
// PublicJWK is the JSON of one JWK, exactly as served.
PublicJWK []byte
Generation int64
CreatedAt time.Time
// RetiresAt is nil while the key is active or not yet retired.
RetiresAt *time.Time
}
SigningKey is one id_token signing key. Keys are never deleted: a retired key keeps its public half in the JWKS until RetiresAt passes, so tokens signed just before a rotation still verify.
type SqliteStore ¶
type SqliteStore struct {
// contains filtered or unexported fields
}
SqliteStore implements oauth2provider.Store using the Grove ORM with SQLite.
func NewSqliteStore ¶
func NewSqliteStore(db *grove.DB) *SqliteStore
NewSqliteStore creates a new SQLite-backed OAuth2 store.
func (*SqliteStore) ConsumeAuthCode ¶
func (*SqliteStore) CreateAuthCode ¶
func (s *SqliteStore) CreateAuthCode(ctx context.Context, code *AuthorizationCode) error
func (*SqliteStore) CreateClient ¶
func (s *SqliteStore) CreateClient(ctx context.Context, c *OAuth2Client) error
func (*SqliteStore) CreateDeviceCode ¶
func (s *SqliteStore) CreateDeviceCode(ctx context.Context, dc *DeviceCode) error
func (*SqliteStore) CreateSigningKey ¶ added in v1.9.1
func (s *SqliteStore) CreateSigningKey(ctx context.Context, k *SigningKey) error
func (*SqliteStore) DeleteClient ¶
func (s *SqliteStore) DeleteClient(ctx context.Context, clientID id.OAuth2ClientID) error
func (*SqliteStore) DeleteExpiredAuthCodes ¶
func (*SqliteStore) DeleteExpiredDeviceCodes ¶
func (s *SqliteStore) DeleteExpiredDeviceCodes(ctx context.Context) error
func (*SqliteStore) DeleteExpiredDeviceCodesBefore ¶
func (*SqliteStore) DeleteGrant ¶
func (*SqliteStore) GetAuthCode ¶
func (s *SqliteStore) GetAuthCode(ctx context.Context, code string) (*AuthorizationCode, error)
func (*SqliteStore) GetClient ¶
func (s *SqliteStore) GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
func (*SqliteStore) GetClientByID ¶
func (s *SqliteStore) GetClientByID(ctx context.Context, clientID id.OAuth2ClientID) (*OAuth2Client, error)
func (*SqliteStore) GetDeviceCodeByDeviceCode ¶
func (s *SqliteStore) GetDeviceCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
func (*SqliteStore) GetDeviceCodeByUserCode ¶
func (s *SqliteStore) GetDeviceCodeByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
func (*SqliteStore) ListClients ¶
func (s *SqliteStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
func (*SqliteStore) ListGrantsByUser ¶
func (*SqliteStore) ListSigningKeys ¶ added in v1.9.1
func (s *SqliteStore) ListSigningKeys(ctx context.Context, scope string) ([]*SigningKey, error)
func (*SqliteStore) RetireSigningKey ¶ added in v1.9.1
func (s *SqliteStore) RetireSigningKey(ctx context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
RetireSigningKey sets RetiresAt, overwriting an earlier value. An unknown id updates no rows and is not an error.
func (*SqliteStore) UpdateClient ¶
func (s *SqliteStore) UpdateClient(ctx context.Context, c *OAuth2Client) error
func (*SqliteStore) UpdateDeviceCode ¶
func (s *SqliteStore) UpdateDeviceCode(ctx context.Context, dc *DeviceCode) error
func (*SqliteStore) UpsertGrant ¶
func (s *SqliteStore) UpsertGrant(ctx context.Context, g *Grant) error
type StandardClaims ¶ added in v1.9.1
type StandardClaims struct {
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
Picture string `json:"picture,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified *bool `json:"email_verified,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified *bool `json:"phone_number_verified,omitempty"`
}
StandardClaims are the OpenID Connect Core 5.1 claims authsome can fill from a user. Userinfo and id_tokens share this one shape, so a claim cannot be present in one and missing from the other. The verified flags are pointers so a false value is still written when its scope is granted. They point at copies, never into the user.
type Store ¶
type Store interface {
// Clients
CreateClient(ctx context.Context, c *OAuth2Client) error
GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
GetClientByID(ctx context.Context, id id.OAuth2ClientID) (*OAuth2Client, error)
// UpdateClient persists changes to an existing client, matched on ID.
// Returns ErrClientNotFound when no row matches.
UpdateClient(ctx context.Context, c *OAuth2Client) error
ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)
DeleteClient(ctx context.Context, id id.OAuth2ClientID) error
// Authorization codes
CreateAuthCode(ctx context.Context, code *AuthorizationCode) error
GetAuthCode(ctx context.Context, code string) (*AuthorizationCode, error)
// ConsumeAuthCode marks a code used and reports whether this call was the
// one that did so. A false return means the code was already consumed —
// callers must treat that as a replay, not a success. Implementations must
// make the test-and-set atomic.
ConsumeAuthCode(ctx context.Context, code string) (bool, error)
// Device codes (RFC 8628)
CreateDeviceCode(ctx context.Context, dc *DeviceCode) error
GetDeviceCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceCode, error)
GetDeviceCodeByUserCode(ctx context.Context, userCode string) (*DeviceCode, error)
UpdateDeviceCode(ctx context.Context, dc *DeviceCode) error
DeleteExpiredDeviceCodes(ctx context.Context) error
// Retention. Each call removes at most batch codes that expired before
// the given instant and reports how many went; a batch of zero or less
// removes every match. See store.Retention for why deletes are batched.
DeleteExpiredAuthCodes(ctx context.Context, before time.Time, batch int) (int64, error)
DeleteExpiredDeviceCodesBefore(ctx context.Context, before time.Time, batch int) (int64, error)
// Grants are keyed by (app, user, client). UpsertGrant creates the grant
// or replaces its scopes; GetGrant returns ErrGrantNotFound when the user
// never approved the client; DeleteGrant revokes it.
UpsertGrant(ctx context.Context, g *Grant) error
GetGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)
ListGrantsByUser(ctx context.Context, appID id.AppID, userID id.UserID) ([]*Grant, error)
DeleteGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) error
// Signing keys for id_tokens. Generation is unique within a scope:
// CreateSigningKey returns ErrSigningKeyConflict when (Scope, Generation)
// already exists, which is how concurrent rotations pick one winner.
// ListSigningKeys returns a scope's keys, newest generation first.
// RetireSigningKey sets RetiresAt on the key; an unknown id is a no-op.
CreateSigningKey(ctx context.Context, k *SigningKey) error
ListSigningKeys(ctx context.Context, scope string) ([]*SigningKey, error)
RetireSigningKey(ctx context.Context, keyID id.OAuth2SigningKeyID, at time.Time) error
}
Store persists OAuth2 clients, authorization codes, and device codes.
type TokenRequest ¶
type TokenRequest struct {
GrantType string `json:"grant_type" form:"grant_type"`
Code string `json:"code,omitempty" form:"code"`
RedirectURI string `json:"redirect_uri,omitempty" form:"redirect_uri"`
ClientID string `json:"client_id,omitempty" form:"client_id"`
ClientSecret string `json:"client_secret,omitempty" form:"client_secret"`
CodeVerifier string `json:"code_verifier,omitempty" form:"code_verifier"`
DeviceCode string `json:"device_code,omitempty" form:"device_code"`
RefreshToken string `json:"refresh_token,omitempty" form:"refresh_token"`
// RFC 8707, repeatable. RFC 6749 section 4.1.3 sends token-endpoint
// parameters as a form body, and a JSON body is accepted too, so the field
// carries both tags.
Resource []string `json:"resource,omitempty" form:"resource,omitempty"`
// RFC 8693 token exchange.
SubjectToken string `json:"subject_token,omitempty" form:"subject_token"`
SubjectTokenType string `json:"subject_token_type,omitempty" form:"subject_token_type"`
ActorToken string `json:"actor_token,omitempty" form:"actor_token"`
ActorTokenType string `json:"actor_token_type,omitempty" form:"actor_token_type"`
RequestedTokenType string `json:"requested_token_type,omitempty" form:"requested_token_type"`
Scope string `json:"scope,omitempty" form:"scope"`
Audience string `json:"audience,omitempty" form:"audience"`
}
TokenRequest is the OAuth2 token request.
type TokenResponse ¶
type TokenResponse struct {
AccessToken string `json:"access_token"`
// TokenType is "Bearer" for an unbound token and "DPoP" for a bound one
// (RFC 9449 section 5). A spec-compliant client reads this to decide
// whether to attach proofs, so getting it wrong makes binding invisible
// to exactly the clients that implement it correctly.
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
// IssuedTokenType is required by RFC 8693 section 2.2.1 and omitted by
// every other grant, which is why it carries omitempty.
IssuedTokenType string `json:"issued_token_type,omitempty"`
// IDToken is the OpenID Connect id_token, present only when the grant
// is for a user and the granted scopes include openid.
IDToken string `json:"id_token,omitempty"`
}
TokenResponse is the OAuth2 token endpoint response.
type UpdateClientRequest ¶
type UpdateClientRequest struct {
ClientID string `path:"clientId" json:"-"`
Name *string `json:"name,omitempty"`
RedirectURIs *[]string `json:"redirect_uris,omitempty"`
Scopes *[]string `json:"scopes,omitempty"`
GrantTypes *[]string `json:"grant_types,omitempty"`
Resources *[]string `json:"resources,omitempty"`
// These three are bound only so they can be refused. Leaving them out of
// the struct would make encoding/json discard them silently, and a
// caller who sends {"public": true} would get a 200 describing a change
// that never happened. app_id is immutable because re-parenting a client
// moves it across a tenancy boundary; public is immutable because
// flipping it mints or destroys a secret, which belongs on the rotation
// route; client_id is the value this endpoint exists to preserve.
BodyClientID *string `json:"client_id,omitempty"`
BodyAppID *string `json:"app_id,omitempty"`
BodyPublic *bool `json:"public,omitempty"`
// Public and TokenEndpointAuthMethod encode one fact between them, and
// models.go is explicit that nothing reads the latter for behaviour:
// every write site derives one from the other instead. Accepting this
// field on its own would let the two disagree, which is the bug that
// comment exists to prevent.
BodyTokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"`
// FirstParty marks a client the operator owns; authorization skips the
// consent page for it.
FirstParty *bool `json:"first_party,omitempty"`
}
UpdateClientRequest is the admin request to edit an existing OAuth2 client.
Every mutable field is a pointer so the handler can distinguish "field absent, leave it alone" from "field present but empty, clear it". A plain []string cannot carry that difference: an omitted key and an explicit [] both unmarshal to nil.
The path-bound client id carries json:"-" so a "client_id" key in the body can never land on it, mirroring UpdateRegistrationRequest. The handler also reads ctx.Param directly rather than trusting this field, so there are two independent protections against the identifier being editable.
type UpdateClientResponse ¶
type UpdateClientResponse struct {
ID string `json:"id"`
ClientID string `json:"client_id"`
Name string `json:"name"`
RedirectURIs []string `json:"redirect_uris"`
Scopes []string `json:"scopes"`
GrantTypes []string `json:"grant_types"`
Resources []string `json:"resources"`
Public bool `json:"public"`
}
UpdateClientResponse echoes the client as it stands after the edit. It carries no client_secret: this route never issues or rotates one.
type UpdateRegistrationRequest ¶
type UpdateRegistrationRequest struct {
ClientID string `path:"clientId" json:"-"`
BodyClientID string `json:"client_id,omitempty"`
BodyClientSecret string `json:"client_secret,omitempty"`
RedirectURIs []string `json:"redirect_uris"`
ClientName string `json:"client_name,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
Scope string `json:"scope,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
TOSURI string `json:"tos_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
SoftwareID string `json:"software_id,omitempty"`
SoftwareVersion string `json:"software_version,omitempty"`
Contacts []string `json:"contacts,omitempty"`
}
UpdateRegistrationRequest is an RFC 7592 client update. The body repeats the full registration; anything omitted is cleared, per RFC 7592 section 2.2, which specifies a replacement and not a merge.
ClientID is tagged json:"-": without it, a body key "clientId" (matched case-insensitively by encoding/json against the Go field name, not against the path parameter name or the RFC wire name "client_id", which binds to the separately tagged BodyClientID below) would land here and overwrite the path-bound value, since BindRequest binds the path first and then decodes the body on top of it. That would let a caller PUT to an arbitrary junk path with {"clientId": "<victim-id>"} in the body and have the section 2.2 client_id check below compare the body against itself instead of against the URL, while the route's rate limiter (keyed on the path segment) charged the junk value instead of the real one.
Two independent protections close this, either sufficient on its own, confirmed by reverting each in isolation and rerunning TestManage_UpdateIgnoresClientIDSmuggledInBody. With the json:"-" tag removed but the handler below still reading ctx.Param("clientId") instead of this field, the test still passes: the handler never looks at this field at all, regardless of what body content lands in it, so restoring the tag was not what made that direction safe, the handler simply never looked here. With the tag restored but the handler changed to read this field instead of ctx.Param, the test also still passes, because the tag alone already keeps this field pinned to the path value. Only removing both at once reproduces the original bug and makes the test fail. So this tag and the handler's ctx.Param read are not a primary fix plus a backup; either one alone already prevents the body from winning.
type UserInfo ¶
type UserInfo struct {
Sub string `json:"sub"`
StandardClaims
}
UserInfo is the OIDC userinfo response.
type UserInfoRequest ¶
type UserInfoRequest struct{}
UserInfoRequest is empty (user is determined from bearer token).
Source Files
¶
- admin_clients.go
- claims.go
- clients_service.go
- consent.go
- consent_gate.go
- contract.go
- idtoken.go
- issuer.go
- jwks.go
- metadata.go
- migrations.go
- migrations_timestamps.go
- models.go
- plugin.go
- register.go
- register_validate.go
- resource.go
- retention.go
- signing.go
- signing_store.go
- store.go
- store_memory.go
- store_models.go
- store_mongo.go
- store_postgres.go
- store_retention.go
- store_sqlite.go
- telemetry.go
- token_exchange.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package contract wires the oauth2provider plugin into the forge-dashboard contract registry: the nine intents that manage OAuth2 clients, the issuer an app's id_tokens name, and that issuer's signing keys.
|
Package contract wires the oauth2provider plugin into the forge-dashboard contract registry: the nine intents that manage OAuth2 clients, the issuer an app's id_tokens name, and that issuer's signing keys. |
|
Package oauth2test provides a backend-agnostic conformance suite for the oauth2provider.Store interface.
|
Package oauth2test provides a backend-agnostic conformance suite for the oauth2provider.Store interface. |