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

oauth

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrSigningKeyRequired = errors.New("oauth: a signing key (Config.PrivateKey) is required")

ErrSigningKeyRequired is returned by NewProvider when Config.PrivateKey is nil and Config.AllowEphemeralKey is not set.

Functions

func AccessRequestFromContext

func AccessRequestFromContext(ctx context.Context) fosite.AccessRequester

AccessRequestFromContext extracts the access request from context.

func HasScope

func HasScope(ctx context.Context, scope string) bool

HasScope checks if the access request in context has a specific scope.

func LoggerFromContext

func LoggerFromContext(ctx context.Context) *slog.Logger

LoggerFromContext returns the logger from context, or slog.Default() if not set.

func ScopesFromContext

func ScopesFromContext(ctx context.Context) []string

ScopesFromContext extracts the granted scopes from the access request in context.

func UserIDFromContext

func UserIDFromContext(ctx context.Context) string

UserIDFromContext extracts the user ID (subject) from the access request in context.

func WithAccessRequest

func WithAccessRequest(ctx context.Context, ar fosite.AccessRequester) context.Context

WithAccessRequest adds the access request to the context.

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API provides HTTP handlers for OAuth 2.0 endpoints using Huma/Chi. It uses a hybrid approach: discovery endpoints use Huma for full typed handling, while Fosite-integrated endpoints use Chi handlers directly.

func NewAPI

func NewAPI(provider *Provider, opts ...Option) (*API, error)

NewAPI creates a new OAuth API with Chi router and Huma.

func (*API) Huma

func (a *API) Huma() huma.API

Huma returns the underlying Huma API for advanced configuration.

func (*API) Logger

func (a *API) Logger() *slog.Logger

Logger returns the API's logger.

func (*API) Middleware

func (a *API) Middleware(next http.Handler) http.Handler

Middleware provides OAuth token validation middleware. It accepts an access token only when it is valid and unrevoked, was issued to a client accepted by WithAcceptedClientIDs (or WithAnyClient) that is still active, and its principal is still active, the same principal check the introspection endpoint applies.

A DPoP-bound token (RFC 9449) is accepted only with the DPoP authorization scheme and a fresh, unreplayed proof by its key for the request's method and URL, carrying the token's hash; the URL is WithResourceBaseURL (default: the issuer's origin) plus the request path. An unbound token, which only a client registered with the allow_bearer_tokens exception gets, is accepted only with the Bearer scheme. Refusals carry WWW-Authenticate challenges.

Middleware panics when the API was created with neither WithAcceptedClientIDs nor WithAnyClient.

func (*API) Provider

func (a *API) Provider() *Provider

Provider returns the OAuth provider.

func (*API) Router

func (a *API) Router() chi.Router

Router returns the Chi router for mounting or serving.

func (*API) ServeHTTP

func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

type AuthorizeInput

type AuthorizeInput struct {
	ResponseType        string `query:"response_type" required:"true" enum:"code,token" doc:"OAuth 2.0 response type"`
	ClientID            string `query:"client_id" required:"true" doc:"Client identifier"`
	RedirectURI         string `query:"redirect_uri" doc:"URI to redirect after authorization"`
	Scope               string `query:"scope" doc:"Space-separated list of requested scopes"`
	State               string `query:"state" doc:"Opaque value for CSRF protection"`
	CodeChallenge       string `query:"code_challenge" doc:"PKCE code challenge"`
	CodeChallengeMethod string `query:"code_challenge_method" enum:"S256,plain" doc:"PKCE code challenge method"`
	Nonce               string `query:"nonce" doc:"OpenID Connect nonce for replay protection"`
}

AuthorizeInput represents the OAuth 2.0 authorization request parameters.

type AuthorizeOutput

type AuthorizeOutput struct {
	Location string `header:"Location" doc:"Redirect URI with authorization code or token"`
}

AuthorizeOutput represents the authorization response (redirect).

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client implements fosite.Client backed by Ent OAuthApp.

func (*Client) DPoPBoundAccessTokens added in v0.21.0

func (c *Client) DPoPBoundAccessTokens() bool

DPoPBoundAccessTokens reports whether the client must use DPoP (RFC 9449's dpop_bound_access_tokens): every client except one registered with the allow_bearer_tokens legacy exception, which may get unbound bearer tokens.

func (*Client) GetAudience

func (c *Client) GetAudience() fosite.Arguments

GetAudience returns the audience for this client.

func (*Client) GetGrantTypes

func (c *Client) GetGrantTypes() fosite.Arguments

GetGrantTypes returns the allowed grant types.

func (*Client) GetHashedSecret

func (c *Client) GetHashedSecret() []byte

GetHashedSecret returns the hash of the client's newest active secret, or nil for a client without one.

func (*Client) GetID

func (c *Client) GetID() string

GetID returns the client ID.

func (*Client) GetRedirectURIs

func (c *Client) GetRedirectURIs() []string

GetRedirectURIs returns the registered redirect URIs.

func (*Client) GetResponseTypes

func (c *Client) GetResponseTypes() fosite.Arguments

GetResponseTypes returns the allowed response types.

func (*Client) GetRotatedHashes added in v0.21.0

func (c *Client) GetRotatedHashes() [][]byte

GetRotatedHashes returns the hashes of the client's other active secrets, accepted during a secret rotation.

func (*Client) GetScopes

func (c *Client) GetScopes() fosite.Arguments

GetScopes returns the allowed scopes.

func (*Client) IsPublic

func (c *Client) IsPublic() bool

IsPublic returns true if this is a public client (SPA, native app).

type Config

type Config struct {
	// Issuer is the OAuth/OIDC issuer URL: the external URL clients use,
	// from which the endpoint URLs DPoP proofs must name are built.
	Issuer string

	// AccessTokenLifespan is the duration access tokens are valid
	AccessTokenLifespan time.Duration

	// RefreshTokenLifespan is the duration refresh tokens are valid
	RefreshTokenLifespan time.Duration

	// AuthCodeLifespan is the duration authorization codes are valid
	AuthCodeLifespan time.Duration

	// PrivateKey is the RSA private key for signing tokens (required). It
	// must be the same on every instance and across restarts: tokens and
	// the JWKS are tied to it.
	PrivateKey *rsa.PrivateKey

	// KeyID is the "kid" of PrivateKey, published in the JWKS and set on
	// every JWT. Default: the RFC 7638 thumbprint of the key.
	//
	// A token's kid selects the key that verifies it: KeyID selects the
	// signing key, a VerificationKeys entry selects that key, and any other
	// kid is refused. Rotate keeping the current key N and the previous
	// key N-1, as for SystemAuth (systemauth.KeyConfig.KeyID):
	//
	//  1. Pre-stage: add the new key N to VerificationKeys while N-1 still
	//     signs, so verifiers pick it up from the JWKS first.
	//  2. Promote: make N PrivateKey, move N-1 into VerificationKeys, and
	//     drop N-2.
	//
	// Never drop N-1 in the deploy that promotes N: tokens it signed are
	// still in use until they expire.
	KeyID string

	// VerificationKeys are keys published in the JWKS and accepted when
	// verifying tokens, but never used to sign: the previous signing key
	// N-1 after a rotation, and a pre-staged next key before it is
	// promoted.
	VerificationKeys []systemauth.VerificationKeyConfig

	// AllowEphemeralKey lets NewProvider generate a fresh signing key when
	// PrivateKey is nil. Development and tests only: the key changes on
	// every start and differs between instances, so tokens stop verifying
	// after a restart.
	AllowEphemeralKey bool

	// JWTAccessTokens issues access tokens as RS256 JWTs (RFC 9068, typ
	// at+jwt) signed with PrivateKey, carrying cnf.jkt when DPoP-bound,
	// instead of opaque tokens. Either way tokens are stored, so
	// revocation and introspection work.
	JWTAccessTokens bool

	// HashSecret is the secret used for HMAC operations
	HashSecret []byte

	// DPoP configures DPoP proof checking (RFC 9449).
	DPoP DPoPConfig
}

Config holds OAuth 2.0 server configuration.

func DefaultConfig

func DefaultConfig(issuer string, hashSecret []byte) *Config

DefaultConfig returns a default OAuth configuration.

type ConsentFunc added in v0.14.0

type ConsentFunc func(ctx context.Context, userID string, client fosite.Client, scopes []string) (bool, error)

ConsentFunc reports whether userID has consented to client receiving scopes. It is consulted for clients that are not first-party.

type DPoPConfig added in v0.21.0

type DPoPConfig struct {
	// ProofMaxAge is how old a proof's iat may be. Default: 5 minutes.
	ProofMaxAge time.Duration

	// ClockSkew is how far a proof's iat may be in the future, and is added
	// to ProofMaxAge. Default: 30 seconds.
	ClockSkew time.Duration

	// ReplayCache rejects a proof whose jti was used before. Default: an
	// in-memory cache, which suits a single instance; when several
	// instances serve the provider, pass one they share, such as
	// middleware.NewRedisReplayCache.
	ReplayCache dpopproof.ReplayCache

	// Nonces, when set, requires every proof to carry a server nonce
	// (RFC 9449 §8); see dpopproof.NewNonces.
	Nonces *dpopproof.Nonces
}

DPoPConfig configures how the provider checks DPoP proofs.

type IntrospectInput

type IntrospectInput struct {
	Token         string `form:"token" required:"true" doc:"The token to introspect"`
	TokenTypeHint string `form:"token_type_hint" enum:"access_token,refresh_token" doc:"Hint about the token type"`

	// Client authentication
	Authorization string `header:"Authorization" doc:"Basic authentication header (client_id:client_secret)"`
}

IntrospectInput represents the token introspection request.

type IntrospectOutput

type IntrospectOutput struct {
	Body IntrospectResponse
}

IntrospectOutput wraps the introspection response.

type IntrospectResponse

type IntrospectResponse struct {
	Active    bool   `json:"active" doc:"Whether the token is active"`
	Scope     string `json:"scope,omitempty" doc:"Scopes associated with the token"`
	ClientID  string `json:"client_id,omitempty" doc:"Client that requested the token"`
	Username  string `json:"username,omitempty" doc:"Resource owner username"`
	TokenType string `json:"token_type,omitempty" doc:"Token type"`
	Exp       int64  `json:"exp,omitempty" doc:"Token expiration timestamp"`
	Iat       int64  `json:"iat,omitempty" doc:"Token issue timestamp"`
	Nbf       int64  `json:"nbf,omitempty" doc:"Token not-before timestamp"`
	Sub       string `json:"sub,omitempty" doc:"Subject (user ID)"`
	Aud       string `json:"aud,omitempty" doc:"Intended audience"`
	Iss       string `json:"iss,omitempty" doc:"Token issuer"`
	Jti       string `json:"jti,omitempty" doc:"JWT ID"`
}

IntrospectResponse represents the token introspection response.

type JWK

type JWK struct {
	Kty string `json:"kty" doc:"Key type (e.g., RSA, EC)"`
	Use string `json:"use,omitempty" doc:"Key use (sig for signature, enc for encryption)"`
	Kid string `json:"kid,omitempty" doc:"Key ID"`
	Alg string `json:"alg,omitempty" doc:"Algorithm"`
	N   string `json:"n,omitempty" doc:"RSA modulus"`
	E   string `json:"e,omitempty" doc:"RSA exponent"`
}

JWK represents a JSON Web Key.

type JWKS

type JWKS struct {
	Keys []JWK `json:"keys" doc:"Array of JSON Web Keys"`
}

JWKS represents a JSON Web Key Set.

type JWKSOutput

type JWKSOutput struct {
	Body JWKS
}

JWKSOutput is the response for the JWKS endpoint.

type OAuthError

type OAuthError struct {
	Error            string `json:"error" doc:"Error code"`
	ErrorDescription string `json:"error_description,omitempty" doc:"Human-readable error description"`
	ErrorURI         string `json:"error_uri,omitempty" doc:"URI with more information about the error"`
}

OAuthError represents an OAuth 2.0 error response.

type OpenIDConfigOutput

type OpenIDConfigOutput struct {
	Body OpenIDConfiguration
}

OpenIDConfigOutput is the response for the OpenID Configuration endpoint.

type OpenIDConfiguration

type OpenIDConfiguration struct {
	Issuer                            string   `json:"issuer" doc:"OAuth 2.0 Issuer Identifier"`
	AuthorizationEndpoint             string   `json:"authorization_endpoint" doc:"URL of the authorization endpoint"`
	TokenEndpoint                     string   `json:"token_endpoint" doc:"URL of the token endpoint"`
	IntrospectionEndpoint             string   `json:"introspection_endpoint" doc:"URL of the introspection endpoint"`
	RevocationEndpoint                string   `json:"revocation_endpoint" doc:"URL of the revocation endpoint"`
	JWKSURI                           string   `json:"jwks_uri" doc:"URL of the JSON Web Key Set document"`
	ResponseTypesSupported            []string `json:"response_types_supported" doc:"List of supported response types"`
	GrantTypesSupported               []string `json:"grant_types_supported" doc:"List of supported grant types"`
	TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported" doc:"List of supported token endpoint authentication methods"`
	CodeChallengeMethodsSupported     []string `json:"code_challenge_methods_supported" doc:"List of supported PKCE code challenge methods"`
	DPoPSigningAlgValuesSupported     []string `json:"dpop_signing_alg_values_supported" doc:"JWS algorithms accepted for DPoP proofs (RFC 9449)"`
}

OpenIDConfiguration represents the OpenID Connect discovery document.

type Option

type Option func(*API)

Option configures an API.

func WithAcceptedClientIDs added in v0.21.0

func WithAcceptedClientIDs(clientIDs ...string) Option

WithAcceptedClientIDs sets the OAuth clients whose access tokens Middleware accepts. A token issued to any other client is refused, so a client a user authorized for one purpose cannot call APIs meant for another.

func WithAnyClient added in v0.21.0

func WithAnyClient() Option

WithAnyClient makes Middleware accept access tokens issued to any active client of this server. Use it only when every client may call the protected API and scopes alone limit what each may do.

func WithConsent added in v0.14.0

func WithConsent(consent ConsentFunc) Option

WithConsent sets how consent is checked for clients that are not first-party. Without it such clients are refused with access_denied; first-party clients never need consent.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for the API. If not set, slog.Default() is used.

func WithLoginURL added in v0.14.0

func WithLoginURL(loginURL string) Option

WithLoginURL sets where unauthenticated users are sent; the original authorization request is passed back in the redirect query parameter. Without it, an unauthenticated authorization request is answered with login_required.

func WithResourceBaseURL added in v0.21.0

func WithResourceBaseURL(baseURL string) Option

WithResourceBaseURL sets the external base URL of the routes behind Middleware, such as "https://api.example.com" or, behind a proxy that strips a path prefix, "https://example.com/api". A DPoP proof's htu must equal it plus the request path as this server sees it. Default: the issuer's origin (scheme and host). Forwarded headers are never trusted.

func WithTrustedUserIDHeader added in v0.14.0

func WithTrustedUserIDHeader(header string, gate systemauth.RequestGate) Option

WithTrustedUserIDHeader authenticates users from a header written by a gateway that has already authenticated them (a backend-for-frontend). Only requests the gate admits are believed; see systemauth.TrustedProxyNetworks and systemauth.TrustAllRequests. A nil gate or an empty header trusts nothing.

func WithUserAuthenticator added in v0.14.0

func WithUserAuthenticator(authenticate func(r *http.Request) string) Option

WithUserAuthenticator sets how the authorization endpoint learns who the user is: it returns the authenticated user's ID for the request, or "" when there is none, for example by validating a session cookie. Until one is set, the authorization endpoint refuses every request with server_error rather than guess.

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider wraps Fosite and provides OAuth 2.0/OIDC functionality.

It issues DPoP-bound tokens (RFC 9449) by default, as SystemAuth does: every token request must carry a valid DPoP proof, the access token is bound to the proof's key (cnf.jkt, token_type DPoP), and so is the refresh token. Only a client registered with the allow_bearer_tokens exception may call the token endpoint without a proof, and then gets unbound bearer tokens.

func NewProvider

func NewProvider(db *ent.Client, cfg *Config) (*Provider, error)

NewProvider creates a new OAuth provider.

func (*Provider) KeyID added in v0.21.0

func (p *Provider) KeyID() string

KeyID returns the kid of the signing key.

func (*Provider) OAuth2Provider

func (p *Provider) OAuth2Provider() fosite.OAuth2Provider

OAuth2Provider returns the underlying Fosite provider.

func (*Provider) Session

func (p *Provider) Session(subject string) *systemauth.Session

Session creates a new OAuth session for subject. The subject is set on the session itself, which Fosite reads for introspection and token storage, as well as on the ID token claims. The session carries the DPoP binding of the grant (DPoPJKT) and, with JWTAccessTokens, the claims and header of JWT access tokens.

func (*Provider) Storage

func (p *Provider) Storage() *Storage

Storage returns the storage adapter.

func (*Provider) TokenEndpointURL added in v0.21.0

func (p *Provider) TokenEndpointURL() string

TokenEndpointURL is the token endpoint as advertised in discovery: the htu a proof for a token request must name.

type RevokeInput

type RevokeInput struct {
	Token         string `form:"token" required:"true" doc:"The token to revoke"`
	TokenTypeHint string `form:"token_type_hint" enum:"access_token,refresh_token" doc:"Hint about the token type"`

	// Client authentication
	Authorization string `header:"Authorization" doc:"Basic authentication header (client_id:client_secret)"`
}

RevokeInput represents the token revocation request.

type RevokeOutput

type RevokeOutput struct{}

RevokeOutput represents the token revocation response (empty on success).

type Storage

type Storage struct {
	// contains filtered or unexported fields
}

Storage implements the Fosite storage interfaces using Ent.

Clients are this package's Client. Authorization codes, tokens, PKCE, and revocation are delegated to SystemAuth's storage over the same tables, which stores each grant's full request and session, so a code exchange keeps the subject, the PKCE challenge, and the redirect URI it was issued with, and refresh-token reuse revokes the token family.

func NewStorage

func NewStorage(db *ent.Client) *Storage

NewStorage creates a new Fosite storage backed by Ent.

func (*Storage) ClientAssertionJWTValid added in v0.14.0

func (s *Storage) ClientAssertionJWTValid(ctx context.Context, jti string) error

ClientAssertionJWTValid reports fosite.ErrJTIKnown for a client-assertion JWT ID that is already used.

func (*Storage) CreateAccessTokenSession

func (s *Storage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error

CreateAccessTokenSession stores an access token with its request.

func (*Storage) CreateAuthorizeCodeSession

func (s *Storage) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) error

CreateAuthorizeCodeSession stores an authorization code with its request.

func (*Storage) CreatePKCERequestSession

func (s *Storage) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) error

CreatePKCERequestSession records a code's PKCE challenge.

func (*Storage) CreateRefreshTokenSession

func (s *Storage) CreateRefreshTokenSession(ctx context.Context, signature string, accessSignature string, request fosite.Requester) error

CreateRefreshTokenSession stores a refresh token, on the paired access token's row when there is one.

func (*Storage) DeleteAccessTokenSession

func (s *Storage) DeleteAccessTokenSession(ctx context.Context, signature string) error

DeleteAccessTokenSession removes an access token session.

func (*Storage) DeletePKCERequestSession

func (s *Storage) DeletePKCERequestSession(ctx context.Context, signature string) error

DeletePKCERequestSession is a no-op: the challenge lives on the authorization code, which the code exchange invalidates.

func (*Storage) DeleteRefreshTokenSession

func (s *Storage) DeleteRefreshTokenSession(ctx context.Context, signature string) error

DeleteRefreshTokenSession revokes a refresh token.

func (*Storage) GetAccessTokenSession

func (s *Storage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)

GetAccessTokenSession restores the request an access token was issued for.

func (*Storage) GetAuthorizeCodeSession

func (s *Storage) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error)

GetAuthorizeCodeSession restores the request an authorization code was issued for. A code already exchanged returns the request with fosite.ErrInvalidatedAuthorizeCode, so Fosite revokes its tokens.

func (*Storage) GetClient

func (s *Storage) GetClient(ctx context.Context, clientID string) (fosite.Client, error)

GetClient loads an active OAuth client by its client_id, with the hashes of its active secrets for client authentication.

func (*Storage) GetPKCERequestSession

func (s *Storage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)

GetPKCERequestSession restores a code's request with its PKCE challenge.

func (*Storage) GetRefreshTokenSession

func (s *Storage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error)

GetRefreshTokenSession restores the request a refresh token was issued for. A rotated refresh token returns the request with fosite.ErrInactiveToken, so Fosite revokes the whole token family.

func (*Storage) InvalidateAuthorizeCodeSession

func (s *Storage) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error

InvalidateAuthorizeCodeSession marks an authorization code as used.

func (*Storage) RevokeAccessToken

func (s *Storage) RevokeAccessToken(ctx context.Context, requestID string) error

RevokeAccessToken revokes every token of the family identified by the Fosite request ID.

func (*Storage) RevokeRefreshToken

func (s *Storage) RevokeRefreshToken(ctx context.Context, requestID string) error

RevokeRefreshToken revokes every token of the family identified by the Fosite request ID.

func (*Storage) RotateRefreshToken added in v0.14.0

func (s *Storage) RotateRefreshToken(ctx context.Context, requestID string, refreshTokenSignature string) error

RotateRefreshToken retires the current tokens of a family before Fosite stores their replacements.

func (*Storage) SetClientAssertionJWT added in v0.14.0

func (s *Storage) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error

SetClientAssertionJWT records a client-assertion JWT ID as used until exp.

type TokenInput

type TokenInput struct {
	GrantType    string `form:"grant_type" required:"true" enum:"authorization_code,refresh_token,client_credentials" doc:"OAuth 2.0 grant type"`
	Code         string `form:"code" doc:"Authorization code (for authorization_code grant)"`
	RedirectURI  string `form:"redirect_uri" doc:"Redirect URI (must match authorization request)"`
	ClientID     string `form:"client_id" doc:"Client identifier (if not using Basic auth)"`
	ClientSecret string `form:"client_secret" doc:"Client secret (if not using Basic auth)"`
	RefreshToken string `form:"refresh_token" doc:"Refresh token (for refresh_token grant)"`
	Scope        string `form:"scope" doc:"Requested scopes (for refresh_token or client_credentials)"`
	CodeVerifier string `form:"code_verifier" doc:"PKCE code verifier"`

	// Basic auth credentials (alternative to form-based client auth)
	Authorization string `header:"Authorization" doc:"Basic authentication header (client_id:client_secret)"`
}

TokenInput represents the OAuth 2.0 token request parameters. Field names follow OAuth 2.0 specification (RFC 6749).

type TokenOutput

type TokenOutput struct {
	Body TokenResponse
}

TokenOutput wraps the token response.

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token" doc:"The access token"`
	TokenType    string `json:"token_type" doc:"Token type (typically 'Bearer')"`
	ExpiresIn    int    `json:"expires_in,omitempty" doc:"Token lifetime in seconds"`
	RefreshToken string `json:"refresh_token,omitempty" doc:"Refresh token for obtaining new access tokens"`
	Scope        string `json:"scope,omitempty" doc:"Granted scopes (may differ from requested)"`
	IDToken      string `json:"id_token,omitempty" doc:"OpenID Connect ID token"`
}

TokenResponse represents the OAuth 2.0 token response. Field names follow OAuth 2.0 specification (RFC 6749).

Jump to

Keyboard shortcuts

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