Documentation
¶
Overview ¶
Package oidc implements the OIDC/SSO login flow: the Authorization Code + PKCE redirect, keyless ID-token verification against the issuer's public JWKS, fail-closed tenant pinning, and group→role mapping. It ends by handing the caller a verified identity; minting the app's own session token is the caller's job (the JWT authenticator stays the request-path verifier).
Index ¶
- Constants
- Variables
- func ApplyDefaultRole(roles []string, defaultRole string) []string
- func GenerateCodeVerifier() string
- func HTTPClient() *http.Client
- func MapRoles(groups []string, mappings map[string]string) []string
- type Flow
- type StateCodec
- type StatePayload
- type VerifiedIdentity
- type Verifier
Constants ¶
const StateCookieTTL = 10 * time.Minute
StateCookieTTL bounds how long a login may sit at the IdP before the callback must arrive. Short enough to limit the replay window on the signed state cookie, long enough for a human to complete an MFA prompt.
Variables ¶
var ( // ErrIssuerMismatch is returned when the ID token's iss differs from the // pinned issuer (H1a). ErrIssuerMismatch = errors.New("oidc: issuer mismatch") // ErrNonceMismatch is returned when the ID token's nonce does not match the // value bound in the state cookie (H4 — replay/CSRF on the token). ErrNonceMismatch = errors.New("oidc: nonce mismatch") // ErrAzpMismatch is returned when the authorized-party (azp) claim is present // and is not this client (H4). ErrAzpMismatch = errors.New("oidc: azp mismatch") // ErrTokenExpired is returned when exp/iat/nbf fall outside the allowed clock // skew (H4). ErrTokenExpired = errors.New("oidc: token outside validity window") // ErrMissingExpiry is returned when the ID token carries no exp claim; with // go-oidc's own expiry check disabled, an absent exp must fail closed rather // than grant an unbounded session. ErrMissingExpiry = errors.New("oidc: token has no expiry") // ErrEmailNotVerified is returned when email_verified is not true; an absent // claim is treated as false (D6c). ErrEmailNotVerified = errors.New("oidc: email not verified") // ErrTenantNotAllowed is returned when the tenant claim (tid/hd) is absent or // not present in the tenant_claims map (D6b/d) — never a fallback to default. ErrTenantNotAllowed = errors.New("oidc: tenant claim not allowed") // ErrTenantAmbiguous is a tenant claim whose array names MORE THAN ONE // accepted tenant. Picking one would be arbitrary and the choice would decide // which tenant's data the session reaches, so this fails closed instead. ErrTenantAmbiguous = errors.New("oidc: tenant claim names more than one accepted tenant") // ErrTenantClaimShape is a tenant claim that is neither a string nor an array // of strings. It is separate from ErrTenantNotAllowed on purpose: "not // allowed" sends an operator to inspect their tenant_claims map, which in this // case is correct, and nothing would tell them the value had a shape the pin // cannot read. ErrTenantClaimShape = errors.New("oidc: tenant claim is neither a string nor an array of strings") // ErrEmailDomainNotAllowed is returned when a non-empty allowed_email_domains // list does not include the verified email's domain (login-level allowlist). ErrEmailDomainNotAllowed = errors.New("oidc: email domain not allowed") // ErrNoSubject is returned when the ID token carries no subject — there is no // stable identity to key on. ErrNoSubject = errors.New("oidc: token has no subject") // ErrGroupOverage signals the IdP omitted the groups claim and returned a // _claim_names overage pointer instead (Azure Entra does this past ~200 group // memberships). Silently treating that as "no groups" would demote a heavily- // grouped user (often the most privileged) to default-deny / default_role with // no signal, so it fails closed with an actionable message: configure Entra // app roles (the "roles" claim) or narrow group membership. Full app-role // resolution is the EKS-validated follow-up. ErrGroupOverage = errors.New("oidc: group claim overage (IdP returned a _claim_names pointer, not the groups) — configure Entra app roles or the groups scope") )
Verification failures. Each is a fail-closed rejection the caller maps to a 403 and audits by reason; none of them ever falls back to a default tenant or a trusted default identity.
var ErrInvalidState = errors.New("invalid oidc state")
ErrInvalidState is returned when the state cookie is missing, malformed, expired, or not signed by us — every case is a failed CSRF/replay check.
var ErrNoIDToken = errors.New("oidc: token response has no id_token")
ErrNoIDToken is returned when the token endpoint's response carries no id_token — a misbehaving or misconfigured IdP. The login fails closed rather than proceeding without an identity to verify.
Functions ¶
func ApplyDefaultRole ¶
ApplyDefaultRole softens the default-deny without weakening it: when the mapped set is empty and a default role is configured, the user is granted that single fallback role. An empty defaultRole leaves the deny in place (the secure default) and a non-empty mapped set is never overridden.
func GenerateCodeVerifier ¶
func GenerateCodeVerifier() string
GenerateCodeVerifier returns a fresh PKCE code verifier (high-entropy, URL-safe). The caller stores it in the state cookie and passes it to Exchange.
func HTTPClient ¶
HTTPClient is the client every IdP call in this package uses. A new one per call is deliberate over a package-level singleton: these are boot-time and per-login calls, the allocation is irrelevant next to the round trip, and a shared mutable client is a thing tests reach into.
func MapRoles ¶
MapRoles translates the user's IdP group values into Dexaflow role names using the configured mapping. It is DEFAULT-DENY: a group with no entry in the map contributes no role. The result is de-duplicated and preserves first-seen order, so a token's role list is stable regardless of group ordering.
Types ¶
type Flow ¶
type Flow struct {
// contains filtered or unexported fields
}
Flow drives the Authorization Code + PKCE login: it builds the authorization redirect, exchanges the code for tokens (using the client secret and the PKCE verifier), and exposes the keyless ID-token Verifier. Discovery runs once at construction and pins the issuer.
func NewFlow ¶
NewFlow discovers the issuer, pins it, and assembles the OAuth2 client and the ID-token verifier. hs256Secret is the app secret the state cookie is signed with (a derived key). It returns an error if discovery fails, so a misconfigured issuer is a boot failure rather than a per-login surprise.
func (*Flow) AuthCodeURL ¶
AuthCodeURL builds the authorization redirect for the given CSRF state, ID token nonce, and PKCE verifier. The verifier is sent as an S256 challenge (never in the clear); the nonce is echoed back in the ID token and checked on callback.
func (*Flow) Codec ¶
func (f *Flow) Codec() *StateCodec
Codec returns the state-cookie codec so the caller can encode the cookie on login and decode it on callback.
type StateCodec ¶
type StateCodec struct {
// contains filtered or unexported fields
}
StateCodec signs and verifies the state cookie with a key derived from the app's HS256 secret. Deriving a distinct key (rather than reusing the secret directly) keeps a state cookie from ever validating as a session token and vice versa.
func NewStateCodec ¶
func NewStateCodec(hs256Secret string) *StateCodec
NewStateCodec builds a StateCodec from the app's HS256 secret, deriving a dedicated signing key so state cookies and session tokens never cross-verify.
func (*StateCodec) Decode ¶
func (c *StateCodec) Decode(token string) (StatePayload, error)
Decode verifies the cookie's signature and expiry and returns its payload. Any failure — bad signature, wrong issuer, expired, malformed — is ErrInvalidState, so the callback fails closed on a tampered or stale cookie.
func (*StateCodec) Encode ¶
func (c *StateCodec) Encode(p StatePayload, ttl time.Duration) (string, error)
Encode signs the payload into a compact token valid for ttl. The CSRF `state` travels as the JWT ID (jti), so verifying the callback's state query param is a direct comparison against the signed cookie.
type StatePayload ¶
StatePayload is the login-flow state carried statelessly in a signed, HttpOnly cookie between the login redirect and the callback: the CSRF `state`, the ID-token `nonce`, the PKCE `code_verifier`, and the sanitized post-login redirect target. No server-side session store is needed.
type VerifiedIdentity ¶
type VerifiedIdentity struct {
// Provider is the stable provider key stored on the user row (the pinned
// issuer URL), half of the (provider, subject) link key.
Provider string
// Subject is the immutable IdP subject, the other half of the link key.
Subject string
// Email is the user's email; it is only trusted because EmailVerified is true.
Email string
// EmailVerified is always true on a returned identity (a false/absent claim
// fails verification).
EmailVerified bool
// Tenant is the resolved Dexaflow tenant name (from the tid/hd pin).
Tenant string
// Groups are the raw IdP group values, for group→role mapping.
Groups []string
}
VerifiedIdentity is the result of a fully-checked ID token: a trustworthy identity the caller may resolve to a Dexaflow user and mint a session for. It is only ever returned after every H1/H4 check and the tenant pin have passed.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier verifies ID tokens keylessly against the issuer's public JWKS and enforces the fail-closed identity gate (issuer pin, audience, nonce, azp, clock skew, email_verified, tenant pin, email-domain allowlist). It wraps go-oidc but does NOT rely on it for nonce, azp, or clock skew, which go-oidc does not check — those are enforced here explicitly (H4).
func NewVerifier ¶
NewVerifier discovers the issuer's OIDC metadata (which pins the issuer: the discovery document's issuer must equal cfg.Issuer) and builds an ID-token verifier that checks the signature against the published JWKS and the audience against the client id. Expiry is deferred to this package's skew-aware check, so the verifier is configured to skip go-oidc's own expiry check.
func (*Verifier) Verify ¶
func (v *Verifier) Verify(ctx context.Context, rawIDToken, expectedNonce string) (*VerifiedIdentity, error)
Verify performs the full fail-closed check on a raw ID token and returns the resulting identity. expectedNonce is the value bound in the state cookie. The order matters: signature/audience/issuer (go-oidc) → nonce → azp → clock skew → subject → email_verified → tenant pin → email-domain allowlist. The domain allowlist is deliberately last (and only reached once email_verified is true) so a spoofable domain can never substitute for the tid/hd pin.