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.
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 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 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) 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) 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) 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) 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) EvaluateConsent(ctx context.Context, clientID string, userID id.UserID, orgID id.OrgID, ...) error
- func (p *Plugin) MigrationGroups(driverName string) []*migrate.Group
- func (p *Plugin) Name() string
- func (p *Plugin) OnInit(_ 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) 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) SweepRetention(ctx context.Context, cutoff func(kind string) time.Time, batch int) (int64, 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) 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) 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 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) 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) 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 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 PostgresMigrations = migrate.NewGroup("authsome-oauth2", migrate.DependsOn("authsome"))
PostgresMigrations is the postgres migration group for the OAuth2 provider plugin.
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 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"`
}
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"`
}
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"`
}
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
}
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"`
}
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"`
}
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 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) 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) 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) 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) 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) 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) 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) 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.
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) 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) 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 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) 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) 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 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
}
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"`
}
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"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
Name string `json:"name,omitempty"`
Phone string `json:"phone_number,omitempty"`
}
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
- consent.go
- consent_gate.go
- contract.go
- metadata.go
- migrations.go
- migrations_timestamps.go
- models.go
- plugin.go
- register.go
- register_validate.go
- resource.go
- retention.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's deep-link settings page into the forge-dashboard contract registry.
|
Package contract wires the oauth2provider plugin's deep-link settings page into the forge-dashboard contract registry. |
|
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. |