Help us shape the future of Go! Take the 2026 Go Developer Survey

oauth2provider

package
v1.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 48 Imported by: 0

README

oauth2provider

Turns Authsome into an OAuth2 authorization server and OpenID Connect provider.

Everything else in this repo helps you consume identity. This plugin makes you the source of it. Third-party apps register as clients, your users approve them, and those apps get tokens scoped to what they were granted. If you have ever added a "Sign in with GitHub" button, this is the plugin that lets someone add "Sign in with your product" to theirs.

Supported grants: authorization code with PKCE (RFC 7636), client credentials, and device authorization for TVs and CLIs that cannot host a browser.

Use it when

  • You are building a platform and third parties need to act on behalf of your users
  • You want first-party mobile and CLI apps to authenticate through a standard flow with short-lived tokens
  • Something in your stack speaks OIDC and expects a discovery document

Skip it when

  • You only need machine-to-machine calls with no delegation. apikey is far less work
  • All your clients are first-party and trusted. Sessions already cover that
  • You cannot commit to a stable issuer URL. It is baked into every token you sign

Wiring

authsome.WithPlugin(oauth2provider.New(oauth2provider.Config{
    Issuer:          "https://auth.example.com",
    AccessTokenTTL:  1 * time.Hour,
    AuthCodeTTL:     10 * time.Minute,
}))

Point clients at /v1/oauth/.well-known/openid-configuration and most libraries configure themselves from there.

Config

Field Type Default What it does
Issuer string none Issuer URL stamped into tokens and discovery
AuthCodeTTL time.Duration 10m Lifetime of an authorization code
AccessTokenTTL time.Duration 1h Lifetime of an access token
DeviceCodeTTL time.Duration 10m Lifetime of a device code
DeviceCodeInterval int 5 Minimum client polling interval, in seconds
VerificationURI string {issuer}/v1/oauth/device Where users go to approve a device

Endpoints

Method Path Purpose
GET /v1/oauth/authorize Authorization endpoint
POST /v1/oauth/token Token endpoint
POST /v1/oauth/revoke Token revocation. The caller is authenticated
GET /v1/oauth/userinfo OIDC UserInfo
GET /v1/oauth/.well-known/openid-configuration Discovery document
POST /v1/oauth/device/authorize Start the device flow
POST /v1/oauth/device/complete Complete the device flow
GET /v1/admin/oauth/clients List registered clients
POST /v1/admin/oauth/clients Register a client
DELETE /v1/admin/oauth/clients/:clientId Delete a client

Lifecycle hooks

None. It is a RouteProvider and a MigrationProvider for its client, code and token tables.

consent records what a user agreed to. social and sso are the consuming side, where you are the client rather than the server.

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

View Source
const (
	DeviceCodeStatusPending    = "pending"
	DeviceCodeStatusAuthorized = "authorized"
	DeviceCodeStatusDenied     = "denied"
	DeviceCodeStatusConsumed   = "consumed"
)

Device code status constants.

Variables

View Source
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.

View Source
var PostgresMigrations = migrate.NewGroup("authsome-oauth2", migrate.DependsOn("authsome"))

PostgresMigrations is the postgres migration group for the OAuth2 provider plugin.

View Source
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 DiscoveryRequest

type DiscoveryRequest struct{}

DiscoveryRequest is empty.

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.

func (*Grant) Covers

func (g *Grant) Covers(scopes []string) bool

Covers reports whether the grant already approves every scope in scopes.

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 (s *MemoryStore) ConsumeAuthCode(_ context.Context, code string) (bool, error)

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 (s *MemoryStore) DeleteExpiredAuthCodes(_ context.Context, before time.Time, batch int) (int64, error)

func (*MemoryStore) DeleteExpiredDeviceCodes

func (s *MemoryStore) DeleteExpiredDeviceCodes(_ context.Context) error

func (*MemoryStore) DeleteExpiredDeviceCodesBefore

func (s *MemoryStore) DeleteExpiredDeviceCodesBefore(_ context.Context, before time.Time, batch int) (int64, error)

func (*MemoryStore) DeleteGrant

func (s *MemoryStore) DeleteGrant(_ context.Context, appID id.AppID, userID id.UserID, clientID string) error

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) GetGrant

func (s *MemoryStore) GetGrant(_ context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)

func (*MemoryStore) ListClients

func (s *MemoryStore) ListClients(_ context.Context, appID id.AppID) ([]*OAuth2Client, error)

func (*MemoryStore) ListGrantsByUser

func (s *MemoryStore) ListGrantsByUser(_ context.Context, appID id.AppID, userID id.UserID) ([]*Grant, 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 (s *MongoStore) ConsumeAuthCode(ctx context.Context, code string) (bool, error)

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 (s *MongoStore) DeleteExpiredAuthCodes(ctx context.Context, before time.Time, batch int) (int64, error)

func (*MongoStore) DeleteExpiredDeviceCodes

func (s *MongoStore) DeleteExpiredDeviceCodes(ctx context.Context) error

func (*MongoStore) DeleteExpiredDeviceCodesBefore

func (s *MongoStore) DeleteExpiredDeviceCodesBefore(ctx context.Context, before time.Time, batch int) (int64, error)

func (*MongoStore) DeleteGrant

func (s *MongoStore) DeleteGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) error

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) GetGrant

func (s *MongoStore) GetGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)

func (*MongoStore) ListClients

func (s *MongoStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)

func (*MongoStore) ListGrantsByUser

func (s *MongoStore) ListGrantsByUser(ctx context.Context, appID id.AppID, userID id.UserID) ([]*Grant, 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 OAuth2Error struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description,omitempty"`
}

OAuth2Error is an RFC 6749 / RFC 8628 error response.

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 New

func New(cfg ...Config) *Plugin

New creates a new 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

func (p *Plugin) MigrationGroups(driverName string) []*migrate.Group

MigrationGroups returns the OAuth2 migration groups for the given driver.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin name.

func (*Plugin) OnInit

func (p *Plugin) OnInit(_ context.Context, engine plugin.Engine) error

OnInit captures dependencies from the engine.

func (*Plugin) RegisterContract

func (p *Plugin) RegisterContract(
	d *dispatcher.Dispatcher,
	reg contract.Registry,
	wreg contract.WardenRegistry,
	engine plugin.Engine,
) error

func (*Plugin) RegisterRootRoutes

func (p *Plugin) RegisterRootRoutes(router forge.Router) error

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

func (p *Plugin) RegisterRoutes(router forge.Router) error

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

func (p *Plugin) SetDynamicRegistrationForTest(enabled bool)

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

func (p *Plugin) SetOAuth2Store(s Store)

SetOAuth2Store allows direct OAuth2 store injection for testing.

func (*Plugin) SetStore

func (p *Plugin) SetStore(s store.Store)

SetStore allows direct store injection for testing.

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.

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 (s *PostgresStore) ConsumeAuthCode(ctx context.Context, code string) (bool, error)

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 (s *PostgresStore) DeleteExpiredAuthCodes(ctx context.Context, before time.Time, batch int) (int64, error)

func (*PostgresStore) DeleteExpiredDeviceCodes

func (s *PostgresStore) DeleteExpiredDeviceCodes(ctx context.Context) error

func (*PostgresStore) DeleteExpiredDeviceCodesBefore

func (s *PostgresStore) DeleteExpiredDeviceCodesBefore(ctx context.Context, before time.Time, batch int) (int64, error)

func (*PostgresStore) DeleteGrant

func (s *PostgresStore) DeleteGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) error

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) GetGrant

func (s *PostgresStore) GetGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)

func (*PostgresStore) ListClients

func (s *PostgresStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)

func (*PostgresStore) ListGrantsByUser

func (s *PostgresStore) ListGrantsByUser(ctx context.Context, appID id.AppID, userID id.UserID) ([]*Grant, 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

type ProtectedResource struct {
	Resource        string
	ScopesSupported []string
}

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 RateLimit

type RateLimit struct {
	Limit  int
	Window time.Duration
}

RateLimit bounds how often an endpoint may be called.

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 (s *SqliteStore) ConsumeAuthCode(ctx context.Context, code string) (bool, error)

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 (s *SqliteStore) DeleteExpiredAuthCodes(ctx context.Context, before time.Time, batch int) (int64, error)

func (*SqliteStore) DeleteExpiredDeviceCodes

func (s *SqliteStore) DeleteExpiredDeviceCodes(ctx context.Context) error

func (*SqliteStore) DeleteExpiredDeviceCodesBefore

func (s *SqliteStore) DeleteExpiredDeviceCodesBefore(ctx context.Context, before time.Time, batch int) (int64, error)

func (*SqliteStore) DeleteGrant

func (s *SqliteStore) DeleteGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) error

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) GetGrant

func (s *SqliteStore) GetGrant(ctx context.Context, appID id.AppID, userID id.UserID, clientID string) (*Grant, error)

func (*SqliteStore) ListClients

func (s *SqliteStore) ListClients(ctx context.Context, appID id.AppID) ([]*OAuth2Client, error)

func (*SqliteStore) ListGrantsByUser

func (s *SqliteStore) ListGrantsByUser(ctx context.Context, appID id.AppID, userID id.UserID) ([]*Grant, 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 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).

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL