An OIDC login now resolves the account by the durable (issuer, sub)
pair instead of the email claim. First contact stamps the pair — by
auto-provision or a one-time link against an existing account matched
by verified email — and every later login matches on it alone, so an
IdP-side email change refreshes the display email but can never
re-point the login at a different account. An email that matches an
account already bound to a different identity is rejected and audited
(auth.oidc.link_conflict), never silently re-bound.
The email gate tightens with it: linking and provisioning now require
an explicit email_verified: true — an absent claim is a reject
(oidc_email_unverified) rather than a benefit of the doubt, since an
IdP that does not assert verification cannot anchor the account match.
Provisioned accounts get a lowercased email and the same server-default
timezone resolution the register route uses.
Migration 0240 adds the nullable users.oidc_issuer / users.oidc_sub
columns with a composite unique index.
Adds an OpenID Connect relying-party login path (PKCE authorization-code
flow) against a single env-configured identity provider — additive to
password/passkey login by default, with an operator opt-in (OIDC_ONLY,
enforced server-side on every auth route) to disable them entirely.
First-time sign-in auto-provisions or links an account by verified email.
Closes an open redirect in the post-login `next` parameter (both the new
OIDC path and the pre-existing password/passkey flow shared the same
backslash-normalization bypass) by resolving the target through a real
URL parse and comparing origins instead of a prefix check.