oauth

package
v1.9.0 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: AGPL-3.0 Imports: 21 Imported by: 0

Documentation

Overview

Package oauth implements provider-side OAuth code-exchange flows for authenticating end users into the identity service.

Identity does OAuth FOR LOGIN ONLY: it accepts an authorization code from the frontend, swaps it with the provider for an ID token (or userinfo response), verifies the user's identity, and then mints OUR own JWT. Provider access/refresh tokens are NEVER stored — that's a separate "connections" service concern.

The Exchanger interface decouples the auth flow from any specific provider; a Registry holds the per-provider implementations the service layer dispatches to via the "provider" string in the RPC.

Currently supported providers:

  • "google": OIDC. ID token verified via JWKS (RS256).
  • "microsoft": OIDC via Azure AD common endpoint. ID token verified via JWKS; per-tenant issuer accepted.
  • "github": NOT OIDC. We exchange the code, then call /user and /user/emails to discover the canonical (verified, primary) email.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCodeExchangeFailed indicates the provider rejected our
	// token-endpoint POST or returned an unparseable response.
	ErrCodeExchangeFailed = errors.New("oauth: code exchange failed")

	// ErrIdentityVerification indicates the ID token signature, issuer,
	// audience, or expiry could not be validated.
	ErrIdentityVerification = errors.New("oauth: identity verification failed")

	// ErrEmailNotVerified indicates the provider returned an unverified
	// email; we refuse to log such users in.
	ErrEmailNotVerified = errors.New("oauth: provider reported email is not verified")

	// ErrStateValidation indicates the OAuth callback state could not be
	// validated against the server-minted state token.
	ErrStateValidation = errors.New("oauth: state validation failed")
)

Common error sentinels. Callers (e.g. the service layer) can errors.Is against these to map to RPC error codes.

Functions

func CodeChallengeS256

func CodeChallengeS256(verifier string) string

CodeChallengeS256 returns the PKCE S256 code challenge for verifier.

func GenerateCodeVerifier

func GenerateCodeVerifier() (string, error)

GenerateCodeVerifier returns a PKCE code verifier.

func GenerateState

func GenerateState() (string, error)

GenerateState returns a high-entropy OAuth state string.

func IssueHostedStateToken added in v0.9.0

func IssueHostedStateToken(
	ctx context.Context,
	signer identityjwt.Signer,
	provider, redirectURI, returnTo, state, codeVerifier, csrfToken string,
	expiry time.Duration,
	now time.Time,
) (string, error)

IssueHostedStateToken signs a hosted-flow state token. redirectURI is the identity-owned callback URL registered with the provider; returnTo is the validated app URL the callback redirects to.

func IssueStateToken

func IssueStateToken(
	ctx context.Context,
	signer identityjwt.Signer,
	provider, redirectURI, state, codeVerifier string,
	expiry time.Duration,
	now time.Time,
) (string, error)

Types

type AppleConfig added in v1.5.0

type AppleConfig struct {
	ClientID   string
	TeamID     string
	KeyID      string
	PrivateKey string

	TokenURL string
	JWKSURL  string
	Issuer   string

	HTTPClient   *http.Client
	JWKSCacheTTL time.Duration
	Now          func() time.Time
}

AppleConfig configures an Apple Exchanger. ClientID, TeamID, KeyID, and PrivateKey are required.

type Authorizer

type Authorizer interface {
	AuthorizationURL(ctx context.Context, redirectURI, state, codeChallenge string) (string, error)
}

Authorizer builds the provider authorization URL for the first half of the OAuth authorization-code flow.

type ExchangeParams added in v1.5.0

type ExchangeParams struct {
	Code             string
	RedirectURI      string
	CodeVerifier     string // Optional PKCE code_verifier
	AppleUserPayload string // Optional form-post payload for Apple first-time login
}

ExchangeParams contains the arguments for the OAuth token exchange.

type Exchanger

type Exchanger interface {
	Exchange(ctx context.Context, params ExchangeParams) (*Identity, error)
}

Exchanger swaps an OAuth authorization code for a verified user identity. Implementations are responsible for:

  1. POSTing to the provider's token endpoint with client_id / client_secret / code / redirect_uri.
  2. Verifying the resulting ID token (OIDC providers) OR fetching userinfo (non-OIDC providers like GitHub).
  3. Returning a canonical Identity with EmailVerified guaranteed true.

Errors returned to callers are intentionally generic — they do not leak provider response bodies. Callers that need provider-specific debugging should inspect logs.

func NewApple added in v1.5.0

func NewApple(cfg AppleConfig) Exchanger

NewApple returns an Exchanger that implements Sign-In with Apple.

func NewGitHub

func NewGitHub(cfg GitHubConfig) Exchanger

NewGitHub returns an Exchanger for GitHub. Note that GitHub does not implement OIDC; this exchanger calls the user/userEmails APIs directly using the access token returned by the token endpoint.

func NewGoogle

func NewGoogle(cfg GoogleConfig) Exchanger

NewGoogle returns an Exchanger for Google OIDC.

func NewMicrosoft

func NewMicrosoft(cfg MicrosoftConfig) Exchanger

NewMicrosoft returns an Exchanger for Microsoft Azure AD.

func NewOIDC added in v1.7.6

func NewOIDC(cfg GenericOIDCConfig) Exchanger

NewOIDC returns a generic OIDC Exchanger built on the shared OIDC discovery / userinfo / JWKS helpers. ProviderKey, IssuerURL (or DiscoveryURL), ClientID, and ClientSecret are required.

type GenericOIDCConfig added in v1.7.6

type GenericOIDCConfig struct {
	// ProviderKey is the registry key (e.g. "okta") this exchanger is
	// registered under and reported in Identity.Provider.
	ProviderKey string

	IssuerURL    string
	ClientID     string
	ClientSecret string

	// Scopes overrides the requested OAuth scopes. Optional; defaults
	// to "openid email profile". "openid" is always ensured.
	Scopes []string

	// DiscoveryURL overrides the well-known discovery endpoint. Optional;
	// derived from IssuerURL when empty.
	DiscoveryURL string

	HTTPClient *http.Client
	// DiscoveryCacheTTL bounds how long a fetched discovery document is
	// reused before re-fetching. Optional; defaults to one hour.
	DiscoveryCacheTTL time.Duration
	JWKSCacheTTL      time.Duration
	Now               func() time.Time
}

GenericOIDCConfig configures a config-driven OIDC Exchanger for an arbitrary standards-compliant provider (Okta, Auth0, Keycloak, any self-hosted issuer). It is the additive, code-release-free path: an operator enables a new provider purely via GATEWAY_OAUTH_OIDC_* env vars.

IssuerURL is the provider's issuer (e.g. https://example.okta.com). The exchanger resolves the authorization / token / JWKS / userinfo endpoints from <IssuerURL>/.well-known/openid-configuration unless DiscoveryURL overrides it.

type GitHubConfig

type GitHubConfig struct {
	ClientID     string
	ClientSecret string

	HTTPClient *http.Client

	AuthorizationURL string
	TokenURL         string
	UserURL          string
	UserMailURL      string
}

GitHubConfig configures a GitHub Exchanger.

type GoogleConfig

type GoogleConfig struct {
	ClientID     string
	ClientSecret string

	// HTTPClient overrides the http.Client used for token + JWKS
	// requests. Optional; defaults to a 10s-timeout client.
	HTTPClient *http.Client

	// TokenURL overrides the token endpoint. Optional; defaults to
	// googleTokenURL.
	TokenURL string

	// AuthorizationURL overrides the provider authorization endpoint.
	// Optional; defaults to googleAuthorizationURL or the discovery
	// document's authorization_endpoint when DiscoveryURL is set.
	AuthorizationURL string

	// JWKSURL overrides the JWKS endpoint. Optional; defaults to
	// googleJWKSURL.
	JWKSURL string

	// DiscoveryURL overrides the OIDC discovery endpoint. When set,
	// Exchange resolves token / JWKS / userinfo endpoints from it.
	DiscoveryURL string

	// UserinfoURL overrides the OIDC userinfo endpoint. Optional.
	UserinfoURL string

	// Issuer overrides the expected `iss` claim. Optional; defaults
	// to googleIssuer.
	Issuer string

	// JWKSCacheTTL overrides the JWKS cache TTL. Optional; defaults
	// to 1h.
	JWKSCacheTTL time.Duration

	// Now overrides the clock used for ID token expiry validation.
	// Optional; defaults to time.Now.
	Now func() time.Time
}

GoogleConfig configures a Google Exchanger. ClientID and ClientSecret are required; the rest default to the live Google endpoints and a 1h JWKS cache.

type HostedStateClaims added in v0.9.0

type HostedStateClaims struct {
	Provider     string
	RedirectURI  string
	ReturnTo     string
	State        string
	CodeVerifier string
	CSRFToken    string
	IssuedAt     int64
	ExpiresAt    int64
}

HostedStateClaims are the tamper-proof claims the hosted OAuth flow binds into the state token it carries through the provider redirect. It is a superset of the headless StateClaims with the app's return_to added: the provider sends only `state` + `code` back to the single callback URL, so everything else (the PKCE verifier, the provider name, and where to hand the user back) must round-trip inside this signed artifact.

This is a SEPARATE artifact from the headless state token (state.go). The headless flow's IssueStateToken / VerifyStateToken shape is intentionally left untouched — the SPA still round-trips the verifier itself there — so the hosted return_to binding does not become a breaking change for native/mobile callers (#126 stop condition).

func VerifyHostedStateToken added in v0.9.0

func VerifyHostedStateToken(
	token string,
	kp identityjwt.KeyProvider,
	now time.Time,
) (*HostedStateClaims, error)

VerifyHostedStateToken validates the signature and expiry of the hosted state token (which IS the OAuth `state` parameter the provider echoed back), then returns the recovered claims. The provider, return target, and PKCE verifier are recovered from the token — the callback has no other source for them. The token's signature is the CSRF binding: an attacker cannot forge a state the server will accept.

type Identity

type Identity struct {
	// ProviderUserID is the stable per-provider user identifier.
	// For OIDC providers this is the "sub" claim. For GitHub this is
	// the numeric user id (rendered as a string).
	ProviderUserID string

	// Email is the user's primary email address. Always lowercased.
	Email string

	// EmailVerified is true if the provider asserts the email is
	// verified. Implementations MUST refuse to return an Identity if
	// the provider says the email is not verified.
	EmailVerified bool

	// Name is the user's display name. May be empty.
	Name string

	// AvatarURL is a URL to the user's profile picture. May be empty.
	AvatarURL string

	// Provider is the provider key — "google", "microsoft", "github", "apple".
	Provider string
}

Identity is the canonical, verified user identity returned by an Exchanger after a successful code exchange. Implementations MUST only return an Identity for verified users — i.e. the caller can rely on Email/ProviderUserID being authoritative.

type MicrosoftConfig

type MicrosoftConfig struct {
	ClientID     string
	ClientSecret string

	HTTPClient *http.Client

	// AuthorizationURL overrides the authorization endpoint. Optional.
	AuthorizationURL string

	// TokenURL overrides the token endpoint. Optional.
	TokenURL string

	// JWKSURL overrides the JWKS endpoint. Optional.
	JWKSURL string

	// TenantID controls the default tenant segment in the authorization
	// endpoint when AuthorizationURL is not set. Optional; defaults to
	// "common".
	TenantID string

	// IssuerFormat is a fmt.Sprintf format string into which the
	// token's `tid` (tenant id) claim is interpolated to derive the
	// expected issuer. Optional; defaults to the Microsoft format.
	// In tests, set this to e.g. "%s" plus a fixed test issuer.
	IssuerFormat string

	JWKSCacheTTL time.Duration
	Now          func() time.Time
}

MicrosoftConfig configures a Microsoft Azure AD Exchanger.

type NativeVerification added in v1.9.0

type NativeVerification struct {
	Identity    *Identity
	ReplayKey   string
	ExpiresAtMs int64
}

NativeVerification is the result of verifying a native ID token: the canonical Identity plus the material NativeOAuthLogin needs to enforce single-use (replay protection). ReplayKey uniquely identifies the one issued token — the token's `jti` when the provider stamps one, else a stable digest of (provider|iss|sub|iat|aud|nonce). ExpiresAtMs is the token's `exp` (epoch ms, bounded to a sane max) so the redeemed-key row is retained only as long as the token could still be presented.

type NativeVerifier added in v1.8.0

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

NativeVerifier verifies native mobile-SDK ID tokens (Google idToken / Apple identityToken) WITHOUT an OAuth code exchange, producing a canonical Identity. It reuses the same JWKS cache + JWS verification + claim parsing the hosted Exchangers use; the only difference is that `aud` is matched against the audience set of the REQUESTED PRODUCT (a per-product set when configured, else the global fallback set — never a single web client id) and, for Apple, the request nonce is verified against the id_token claim. Scoping `aud` per product stops a token minted for product A's client id from being redeemed as product B.

func NewNativeVerifier added in v1.8.0

func NewNativeVerifier(cfg NativeVerifierConfig) *NativeVerifier

NewNativeVerifier builds a NativeVerifier. JWKS caches are created for both providers regardless of whether their audiences are configured; a provider with no audiences is rejected at Verify time, so an unconfigured provider's cache is never consulted.

func (*NativeVerifier) Verify added in v1.8.0

func (v *NativeVerifier) Verify(ctx context.Context, provider, idToken, rawNonce, product string) (*NativeVerification, error)

Verify validates a native ID token for the given provider and product, and returns a canonical, verified Identity. product selects the audience set the token's `aud` is matched against (see audsFor); rawNonce is the un-hashed nonce from the native request (Apple only); pass "" for Google. Every failure returns an error wrapping ErrIdentityVerification or ErrEmailNotVerified so the caller can map it to a single, safe Unauthenticated response — the reason (bad aud, wrong product, expired, …) is never leaked to the client.

type NativeVerifierConfig added in v1.8.0

type NativeVerifierConfig struct {
	// GoogleAudiences is the GLOBAL fallback set of accepted `aud` values for
	// Google ID tokens — the web client id PLUS every per-platform
	// (iOS/Android) OAuth client id the native SDKs present. It is consulted
	// only for a product with no entry in GoogleAudiencesByProduct. Empty
	// disables Google native login for those products.
	GoogleAudiences []string
	// AppleAudiences is the GLOBAL fallback set of accepted `aud` values for
	// Apple ID tokens — the Services ID PLUS every native bundle id. It is
	// consulted only for a product with no entry in AppleAudiencesByProduct.
	// Empty disables Apple native login for those products.
	AppleAudiences []string

	// GoogleAudiencesByProduct scopes the accepted Google `aud` values PER
	// PRODUCT, keyed by the lower-cased product selector. When a product has an
	// entry here, ONLY that entry's audiences are accepted for it — a token
	// whose `aud` is valid for another product (or only globally) is rejected.
	// A product with no entry falls back to GoogleAudiences.
	GoogleAudiencesByProduct map[string][]string
	// AppleAudiencesByProduct scopes the accepted Apple `aud` values PER
	// PRODUCT, keyed by the lower-cased product selector. Same semantics as
	// GoogleAudiencesByProduct: an entry is exclusive for its product, an
	// absent product falls back to AppleAudiences.
	AppleAudiencesByProduct map[string][]string

	// GoogleJWKSURL / AppleJWKSURL override the default provider JWKS
	// endpoints (used by tests to point at a stub). Empty uses the live URL.
	GoogleJWKSURL string
	AppleJWKSURL  string

	// GoogleIssuer / AppleIssuer override the accepted issuer(s) (tests only).
	// Empty uses the live issuer(s).
	GoogleIssuer string
	AppleIssuer  string

	HTTPClient   *http.Client
	JWKSCacheTTL time.Duration
	Now          func() time.Time
}

NativeVerifierConfig configures a NativeVerifier. At least one provider's audiences must be non-empty for the verifier to accept that provider; a provider with no configured audiences is treated as unsupported.

type Registry

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

Registry maps provider keys ("google", "microsoft", "github", "apple") to their Exchanger implementations. The service layer looks up the Exchanger for the provider named in the OAuthLoginRequest.

A nil *Registry is valid and reports every provider as missing. This lets the service treat "OAuth login disabled" as a registry without any registered providers (or no registry at all).

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry. Callers populate it with Register.

func (*Registry) Get

func (r *Registry) Get(provider string) (Exchanger, bool)

Get returns the Exchanger for the given provider key. The second return value is false if no Exchanger is registered.

func (*Registry) Len

func (r *Registry) Len() int

Len reports how many providers are registered.

func (*Registry) Providers

func (r *Registry) Providers() []string

Providers returns the sorted list of currently-registered provider keys. Useful for startup logging.

func (*Registry) Register

func (r *Registry) Register(provider string, e Exchanger)

Register associates the given Exchanger with a provider key. Calling Register with the same key twice replaces the previous entry. A nil Exchanger is treated as "unregister" so callers can disable a provider at runtime.

type StateClaims

type StateClaims struct {
	Provider     string
	RedirectURI  string
	State        string
	CodeVerifier string
	IssuedAt     int64
	ExpiresAt    int64
}

func VerifyStateToken

func VerifyStateToken(
	token string,
	kp identityjwt.KeyProvider,
	expectedProvider, expectedRedirectURI, returnedState, explicitCodeVerifier string,
	now time.Time,
) (*StateClaims, error)

Jump to

Keyboard shortcuts

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