oauth

package
v1.11.7 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package oauth is an OpenID Connect (OIDC) client for authcore — "log in with Google / Microsoft / any OIDC provider".

It implements the security-critical mechanics you only get right once: Authorization Code flow with PKCE (S256), an unguessable state and nonce, and strict ID-token validation (signature against the provider's JWKS, plus issuer, audience, expiry, and nonce). It is a client only — authcore does not act as an OAuth server. It stores nothing and runs no HTTP server; you own the two routes and where you persist the per-request secrets.

Flow

mod, _ := oauth.New(auth, oauth.Config{
    ClientID: id, ClientSecret: secret,
    RedirectURL: "https://app.example.com/callback",
    Provider: oauth.Google(),
})

// 1. Start: build the redirect and persist the returned secrets
//    (state, nonce, verifier) in a short-lived signed cookie or the session.
req, _ := mod.AuthCodeURL()
saveToCookie(w, req.State, req.Nonce, req.Verifier)
http.Redirect(w, r, req.URL, http.StatusFound)

// 2. Callback: check state, exchange the code, validate the ID token.
if subtle.ConstantTimeCompare([]byte(r.FormValue("state")), []byte(savedState)) != 1 { return /* 400 */ }
tok, _ := mod.Exchange(r.Context(), r.FormValue("code"), savedVerifier)
claims, err := mod.VerifyIDToken(r.Context(), tok.IDToken, savedNonce)
if err != nil { return /* 401 */ }
// claims.Subject is the stable user id at this provider; claims.Email etc.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidConfig is returned by New when the Config fails validation
	// (missing client id, endpoints, redirect URL, …).
	//
	// Safety: INTERNAL — a startup/programming error. Treat as a 500.
	ErrInvalidConfig = errors.New("oauth: invalid configuration")

	// ErrExchange is returned when the authorization-code exchange with the
	// provider's token endpoint fails (network error, non-2xx, or an
	// OAuth error response).
	//
	// Safety: INTERNAL — log it; return a generic failure to the user.
	ErrExchange = errors.New("oauth: token exchange failed")

	// ErrNoIDToken is returned by Exchange-derived flows when the provider's
	// response carried no id_token — the provider is not acting as an OIDC
	// provider for this request (e.g. the "openid" scope was dropped).
	//
	// Safety: INTERNAL.
	ErrNoIDToken = errors.New("oauth: response contained no id_token")

	// ErrIDTokenInvalid is returned when the ID token fails validation:
	// bad or unverifiable signature, wrong issuer/audience, expired, an
	// unsupported algorithm, or a nonce mismatch.
	//
	// Safety: INTERNAL — return a generic "unauthorized"; never echo the reason.
	ErrIDTokenInvalid = errors.New("oauth: id token is invalid")

	// ErrJWKS is returned when the provider's signing keys cannot be fetched
	// or parsed.
	//
	// Safety: INTERNAL.
	ErrJWKS = errors.New("oauth: cannot obtain provider signing keys")

	// ErrUserInfo is returned when the userinfo request to a plain-OAuth2
	// provider fails (network error, non-2xx, or undecodable body).
	//
	// Safety: INTERNAL.
	ErrUserInfo = errors.New("oauth: userinfo request failed")

	// ErrNoUserInfo is returned by UserInfo when the provider has no
	// UserInfoURL configured (it is an OIDC provider — use VerifyIDToken).
	//
	// Safety: INTERNAL — a programming error.
	ErrNoUserInfo = errors.New("oauth: no userinfo URL configured")

	// ErrDiscovery is returned by Discover when the OIDC discovery document
	// cannot be fetched or parsed, or its issuer does not match.
	//
	// Safety: INTERNAL.
	ErrDiscovery = errors.New("oauth: OIDC discovery failed")
)

Sentinel errors returned by the oauth package. Use errors.Is to check for these in calling code.

Functions

func AzureMultiTenantIssuer added in v1.11.0

func AzureMultiTenantIssuer() func(issuer string) bool

AzureMultiTenantIssuer returns an IssuerValidator that accepts any Azure AD v2.0 issuer, i.e. tokens from any tenant. Use it with the Microsoft "common" or "organizations" endpoints, where each user's token carries their own tenant id in "iss" and no fixed issuer string can match:

cfg := oauth.Config{
    ClientID: id, ClientSecret: secret, RedirectURL: cb,
    Provider:        oauth.Microsoft("common"),
    IssuerValidator: oauth.AzureMultiTenantIssuer(),
}

This trusts users from EVERY Azure tenant. To restrict to specific tenants, also check the "tid" claim via IDClaims.Raw against your allowlist.

Types

type AuthRequest

type AuthRequest struct {
	// URL is the provider authorization URL to redirect the user to.
	URL string
	// State must be compared against the "state" query parameter on callback;
	// a mismatch means CSRF or a stale request — reject it.
	State string
	// Nonce must be handed to VerifyIDToken; it binds the ID token to this request.
	Nonce string
	// Verifier is the PKCE code_verifier; pass it to Exchange.
	Verifier string
}

AuthRequest is the result of AuthCodeURL. Redirect the user to URL, and persist State, Nonce and Verifier somewhere only this browser can return them (a short-lived signed/HttpOnly cookie or the server session) — all three are needed to validate the callback.

type Client

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

Client is the OIDC client module. Construct one per provider at startup with New and share it; it is safe for concurrent use.

func New

func New(p authcore.Provider, cfg Config) (*Client, error)

New creates an OIDC Client for a single provider.

func (*Client) AuthCodeURL

func (c *Client) AuthCodeURL() (*AuthRequest, error)

AuthCodeURL generates fresh state, nonce and a PKCE verifier, and builds the authorization redirect URL. Every call is independent.

func (*Client) Exchange

func (c *Client) Exchange(ctx context.Context, code, verifier string) (*Tokens, error)

Exchange swaps an authorization code for tokens at the token endpoint, sending the PKCE code_verifier. ctx bounds the request.

It returns ErrExchange on a transport error or a non-2xx response. For an OIDC provider it also returns ErrNoIDToken when the response carried no id_token; validate that token with VerifyIDToken before trusting it. For a plain-OAuth2 provider (no issuer/JWKS) there is no id_token — call UserInfo.

func (*Client) Name

func (c *Client) Name() string

Name returns the module's unique identifier. It implements authcore.Module.

func (*Client) UserInfo added in v1.9.0

func (c *Client) UserInfo(ctx context.Context, accessToken string) (map[string]any, error)

UserInfo fetches the authenticated user's profile from a plain-OAuth2 provider's userinfo endpoint, presenting the access token as a Bearer credential. Use it for providers that do not issue an ID token (GitHub, Discord, …); for OIDC providers prefer VerifyIDToken.

The returned map is the provider's raw JSON — its shape is provider-specific, so read the fields you need (e.g. GitHub "id"/"login", Discord "id"/ "username"). The provider's stable user id is the value to key your account on, never the email or display name.

Returns ErrNoUserInfo if the provider has no UserInfoURL, or ErrUserInfo on a transport error, a non-2xx response, or an undecodable body.

func (*Client) VerifyIDToken

func (c *Client) VerifyIDToken(ctx context.Context, idToken, nonce string) (*IDClaims, error)

VerifyIDToken validates an ID token and returns its claims.

It verifies the signature against the provider's JWKS (asymmetric algorithms only), and enforces the issuer, the audience (must contain the client id), expiry, and that the "nonce" claim equals the nonce from the matching AuthCodeURL request. nonce must be the non-empty value you stored; passing the wrong or an empty nonce fails closed.

On any failure it returns ErrIDTokenInvalid (wrapped). Never expose the wrapped detail to the end user.

type Config

type Config struct {
	// ClientID is the OAuth client identifier registered with the provider.
	ClientID string
	// ClientSecret is the client secret. Leave empty for a public client that
	// relies on PKCE alone (PKCE is always used regardless).
	ClientSecret string
	// RedirectURL is the callback URL registered with the provider; it must
	// match exactly.
	RedirectURL string
	// Provider holds the provider endpoints.
	Provider Provider
	// Scopes requested at authorization. When empty, defaults to the OIDC set
	// {"openid","email","profile"}. When you set it, it is used verbatim — for
	// an OIDC provider include "openid" yourself (without it there is no ID
	// token); for a plain-OAuth2 provider set that provider's own scopes
	// (e.g. GitHub's "read:user user:email").
	Scopes []string
	// HTTPClient optionally overrides the client used for the token and JWKS
	// requests. Defaults to a client with a 10-second timeout.
	HTTPClient *http.Client

	// IssuerValidator optionally replaces the default exact "iss" match with a
	// predicate. It is nil by default (exact match against Provider.Issuer).
	//
	// Set it for a multi-tenant provider whose tokens carry a per-tenant issuer
	// that no single string can match — e.g. Azure AD "common", where each
	// user's token issuer contains their own tenant id (see AzureMultiTenantIssuer).
	// When set, Provider.Issuer is ignored for verification and VerifyIDToken
	// accepts a token whose iss the function approves.
	//
	// Returning true for an issuer means trusting every token it mints, so keep
	// the predicate tight (a fixed pattern), and if you must restrict to certain
	// tenants, additionally check the "tid" claim via IDClaims.Raw.
	IssuerValidator func(issuer string) bool
}

Config configures an OIDC client for a single provider.

type IDClaims

type IDClaims struct {
	// Subject is the "sub" claim — the stable, provider-unique user identifier.
	// Key your account records on (Issuer, Subject), never on email alone.
	Subject string
	// Issuer is the "iss" claim (equals the configured Provider.Issuer).
	Issuer string
	// Audience is the "aud" claim (contains the configured ClientID).
	Audience []string
	// Email is the "email" claim, if present.
	Email string
	// EmailVerified is the "email_verified" claim. Do not treat an unverified
	// email as proof of address ownership.
	EmailVerified bool
	// Name is the "name" claim, if present.
	Name string
	// Raw exposes every claim for provider-specific fields not surfaced above.
	Raw map[string]any
}

IDClaims holds the validated claims of an ID token.

type Provider

type Provider struct {
	// Issuer is the exact "iss" value the provider stamps into its ID tokens.
	// It is enforced on verification, so it must match byte-for-byte.
	Issuer string
	// AuthURL is the authorization endpoint the user is redirected to.
	AuthURL string
	// TokenURL is the token endpoint where the code is exchanged.
	TokenURL string
	// JWKSURL is the endpoint serving the provider's signing keys (JWKS),
	// used to verify ID token signatures. Required for OIDC providers (those
	// that issue an ID token); leave empty for a plain-OAuth2 provider.
	JWKSURL string

	// UserInfoURL is the profile endpoint for a plain-OAuth2 provider that does
	// not issue an ID token (e.g. GitHub, Discord). When set, identity comes
	// from UserInfo(accessToken) instead of VerifyIDToken. Optional for OIDC
	// providers, which carry identity in the ID token.
	UserInfoURL string
}

Provider describes an OIDC provider's endpoints. Use a preset (Google, Microsoft) or fill it in from the provider's discovery document.

func Discord added in v1.9.0

func Discord() Provider

Discord returns the Provider endpoints for Discord's OAuth2 service. Like GitHub it is not OIDC; identity comes from UserInfo (the users/@me endpoint). Request scopes like "identify" and "email".

func Discover added in v1.10.0

func Discover(ctx context.Context, issuer string, httpClient *http.Client) (Provider, error)

Discover builds a Provider from an OIDC issuer's discovery document, instead of hardcoding endpoints. Pass the issuer URL (e.g. "https://accounts.google.com", an Auth0/Okta tenant, a Keycloak realm) and the authorization, token, JWKS and userinfo endpoints are read from the provider itself — always current, never a stale constant.

It enforces that the document's "issuer" equals the requested issuer (per OpenID Connect Discovery), so a substituted discovery document cannot point the client at attacker endpoints under a victim's name. ctx bounds the fetch; httpClient is optional (a 10-second-timeout client is used when nil).

p, err := oauth.Discover(ctx, "https://accounts.google.com", nil)
if err != nil { ... }
mod, _ := oauth.New(auth, oauth.Config{ClientID: id, RedirectURL: cb, Provider: p})

func GitHub added in v1.9.0

func GitHub() Provider

GitHub returns the Provider endpoints for GitHub's OAuth2 service. GitHub is not OIDC — it issues no ID token — so identity comes from UserInfo (the /user endpoint). Request scopes like "read:user" and "user:email".

func Google

func Google() Provider

Google returns the Provider endpoints for Google's OIDC service.

cfg := oauth.Config{
    ClientID:     id, ClientSecret: secret,
    RedirectURL:  "https://app.example.com/callback",
    Provider:     oauth.Google(),
}

func Microsoft

func Microsoft(tenant string) Provider

Microsoft returns the Provider endpoints for the Microsoft identity platform (Azure AD) for a SPECIFIC tenant id.

Pass your concrete tenant id (a GUID, or a verified domain like "contoso.onmicrosoft.com"). Do NOT pass the multi-tenant aliases "common", "organizations", or "consumers": a real ID token's "iss" carries the signed-in user's own tenant GUID, never the alias, and VerifyIDToken enforces an exact issuer match — so an alias-based config rejects every token. To accept users from any tenant, pass Microsoft("common") and set Config.IssuerValidator to AzureMultiTenantIssuer() (which approves any Azure v2.0 per-tenant issuer); otherwise pin a single tenant here.

type Tokens

type Tokens struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	RefreshToken string `json:"refresh_token"`
	IDToken      string `json:"id_token"`
	ExpiresIn    int    `json:"expires_in"`
}

Tokens is the provider's token-endpoint response.

Jump to

Keyboard shortcuts

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