oidc

package
v0.21.5 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package oidc adds an OIDC sign-in path alongside the static operator credential and the trusted-proxy header in internal/auth.

Why a separate package

Auth's job is "given a request, produce a user_id and plant a session cookie". That contract is upheld by Login (static creds) and Middleware (header). Adding a third issuer in the same package would couple every code path to the OIDC dependency chain (go-oidc, go-jose, golang.org/x/oauth2). Keeping OIDC out here means a self-host build that doesn't enable OIDC pays zero runtime cost and zero binary-size cost beyond the imports themselves.

The flow

GET  /api/auth/oidc/{provider}/start
     → 302 to provider.AuthURL(state) with a state+nonce cookie

GET  /api/auth/oidc/{provider}/callback?code&state
     → verify state cookie matches query param
     → exchange code for tokens
     → verify ID token signature + nonce + audience
     → resolve identity → stable UUID via auth's UserIDFor
     → upsert users row → mint Rivolt session cookie
     → 302 back into the SPA (configurable post-login URL)

Identity resolution

We use the verified email claim as the auth username when present (Google + most enterprise IdPs always emit it for sign- in scopes). Falling back to `iss + ":" + sub` keeps the deterministic-UUID contract working even for IdPs that withhold email. The same UUIDv5 namespace is shared with cookie / header auth, so a user who signs in once by password and once by OIDC (same email) lands on the same tenant key — no data fork.

Security checklist (RFC 6749 §10 + OpenID Connect Core §3.1.2) RFC 6749 §10 + OpenID Connect Core §3.1.2)" aria-label="Go to Security checklist (RFC 6749 §10 + OpenID Connect Core §3.1.2)">¶

  • state: random 32 bytes, set in an HttpOnly+Secure cookie, compared with constant time at callback. CSRF-class defense.
  • nonce: same 32-byte token reused as the OIDC nonce; verified by go-oidc against the ID token claim.
  • PKCE: S256, verifier stored in the same state cookie alongside the state value. Defends against authcode interception on hostile networks.
  • Cookie scope: the state cookie is per-callback only, SameSite=Lax (Strict would block the cross-site redirect itself), Path=/api/auth/oidc, MaxAge=10m.

Multi-provider

One Service hosts a registry of providers keyed by short name (e.g. "google", "authentik"). The router mounts every provider under /api/auth/oidc/{name}/{start|callback}; a provider that fails to bootstrap (DNS, bad issuer URL) is logged and skipped rather than crashing the whole server.

Index

Constants

This section is empty.

Variables

View Source
var ErrCapExceeded = errors.New("oidc: signup cap exceeded")

ErrCapExceeded is the sentinel CapGuard returns when the signup cap would be exceeded by creating a new user. The callback maps it to a redirect to CapOverflowURL with the email pre-filled.

View Source
var ErrUserForbidden = errors.New("oidc: user forbidden")

ErrUserForbidden is the sentinel main returns from EnsureUser when the resolved user row exists but has been admin-disabled. The callback handler maps it to a 403 with a user-facing message; any other error becomes a generic 500.

Functions

This section is empty.

Types

type Config

type Config struct {
	IssueSession func(w http.ResponseWriter, r *http.Request, uid uuid.UUID) error
	EnsureUser   func(ctx context.Context, username, email, displayName string) (uuid.UUID, error)
	UserIDFor    func(string) uuid.UUID
	PostLoginURL string
	SecureCookie bool
	Logger       *slog.Logger
	Providers    []ProviderConfig

	// CapGuard, when non-nil, gates new-account creation in the
	// OIDC callback. Wired to a flags.Store + db.UserExists check
	// in main. nil disables the cap (every sign-in proceeds).
	CapGuard func(ctx context.Context, username, email string) error

	// CapOverflowURL is the SPA path the callback redirects to
	// when CapGuard returns ErrCapExceeded. Defaults to
	// "/signup/full".
	CapOverflowURL string
}

Config bundles the wiring main hands to New. Splitting it from ProviderConfig keeps the per-IdP knobs separate from the service-wide identity / cookie wiring.

type ProviderConfig

type ProviderConfig struct {
	// Name is the short URL slug — e.g. "google". Lowercased and
	// alphanum-only is recommended; we don't enforce it because
	// the operator owns their own URL space.
	Name string

	// Kind selects the wire protocol. "" or "oidc" is the default:
	// standard OIDC discovery + ID-token verification. "oauth2-github"
	// is GitHub's non-OIDC OAuth2 — no ID token, identity comes from
	// the REST /user + /user/emails endpoints. New kinds go behind a
	// single switch in buildProvider/handleCallback so the shared
	// PKCE, state cookie, and cap-guard plumbing keeps applying.
	Kind string

	// DisplayName is what the SPA puts on the button. "Google",
	// "Authentik", "GitHub". Falls back to Name if empty.
	DisplayName string

	// IssuerURL is the OIDC discovery root (without
	// /.well-known/openid-configuration; go-oidc appends it).
	// Examples: "https://accounts.google.com",
	// "https://auth.example.com". Ignored when Kind="oauth2-github".
	IssuerURL string

	// ClientID / ClientSecret are the OAuth2 application
	// credentials. ClientSecret may be empty for public clients
	// (PKCE-only), though most server-side flows ship one.
	ClientID     string
	ClientSecret string

	// RedirectURL is the absolute URL of /api/auth/oidc/{name}/
	// callback as the IdP will redirect the browser there. We
	// build it once at config time so a misconfigured base URL
	// fails fast.
	RedirectURL string

	// Scopes overrides the default scope set. Most IdPs are happy
	// with the default; some (Authentik) need an extra scope to
	// emit groups.
	Scopes []string
}

ProviderConfig is one IdP's static configuration, supplied by the operator via env. All fields are required except Scopes (which defaults to {"openid","email","profile"}).

func ParseProvidersFromEnv

func ParseProvidersFromEnv(getenv func(string) string, baseURL string) ([]ProviderConfig, error)

ParseProvidersFromEnv reads the operator's env soup and produces a slice of ProviderConfig. Format:

RIVOLT_OIDC_PROVIDERS=google,github,authentik
RIVOLT_OIDC_GOOGLE_ISSUER=https://accounts.google.com
RIVOLT_OIDC_GOOGLE_CLIENT_ID=...
RIVOLT_OIDC_GOOGLE_CLIENT_SECRET=...
RIVOLT_OIDC_GOOGLE_DISPLAY_NAME=Google         # optional
RIVOLT_OIDC_GOOGLE_SCOPES=openid,email,profile # optional

RIVOLT_OIDC_GITHUB_KIND=oauth2-github          # required for GitHub
RIVOLT_OIDC_GITHUB_CLIENT_ID=...
RIVOLT_OIDC_GITHUB_CLIENT_SECRET=...
RIVOLT_OIDC_GITHUB_DISPLAY_NAME=GitHub         # optional

KIND defaults to "oidc". Set KIND=oauth2-github for GitHub (which doesn't issue ID tokens); ISSUER is ignored in that case.

baseURL is the public origin Rivolt is reachable at; we build the redirect URL automatically as {baseURL}/api/auth/oidc/{name}/callback.

Missing or empty RIVOLT_OIDC_PROVIDERS yields an empty slice (and OIDC stays disabled). Invalid per-provider config returns an error so a deploy fails fast rather than silently dropping a provider.

type ProviderListing

type ProviderListing struct {
	Name        string `json:"name"`
	DisplayName string `json:"display_name"`
	StartURL    string `json:"start_url"`
}

ProviderListing is the public shape of a registered IdP.

type Service

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

Service is the OIDC handler set, registered under /api/auth/oidc by api.go. Multiple providers live in one Service.

func New

func New(ctx context.Context, cfg Config) (*Service, error)

New builds a Service. Providers that fail to bootstrap (DNS failure, transient discovery error, invalid issuer URL) are logged at WARN and retried in the background with exponential backoff — the rest still come up. That's the right posture for a self-hosted deploy where Rivolt and its IdP (e.g. Hydra) might race at boot: the provider becomes available without an operator restart once discovery succeeds.

func (*Service) Mount

func (s *Service) Mount(r chi.Router)

Mount installs the OIDC routes onto r under /oidc:

GET /oidc/{provider}/start
GET /oidc/{provider}/callback

Caller is api.go, which mounts this whole group under /api/auth.

func (*Service) Providers

func (s *Service) Providers() []ProviderListing

Providers returns the registered providers in a JSON-friendly shape — used by the SPA to decide whether to render any social- login buttons. Returning the start URL pre-built keeps the frontend free of routing knowledge.

Jump to

Keyboard shortcuts

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