When an operator configures `Email Claim = preferred_username` (e.g. Authentik) the
primary `_resolve_provider_email` correctly rejects the identity value as non-email
shaped and returns None, leaving auto-provisioned users with `email=None` even though
the same token carries a valid standard `email` claim.
Add a narrow fallback in the auto-create-users branch only: when
`provider.email_claim != "email"` and the primary returned None, resolve the standard
`email` claim with the same Fall A/B shape + email_verified enforcement and use it for
`User.email` and `UserOIDCLink.provider_email`.
The auto-link-existing-accounts gate is left on the primary `provider_email`, so the
GHSA Fall-B / Fall-C guards remain intact - the fallback never feeds account matching.
fix(auth): cleanup orphan OIDC/MFA rows on user delete (#1285)
Three User-FK tables (user_oidc_links, user_totp, user_otp_codes)
declare ON DELETE CASCADE in their models, but SQLite ships with
PRAGMA foreign_keys=OFF (the project's existing pattern, mirrored
for APIKey in PR #1182). Without explicit DELETEs, deleting a user
on SQLite leaves orphan rows behind:
fix(oidc): use preferred_username/name claim for auto-created username
When auto-creating an OIDC user without a valid email claim, derive the
username from preferred_username or name IdP claims instead of falling
back to the opaque provider_sub[:30].
feat(oidc): add Azure Entra ID support with configurable email claim resolution
Adds two new OIDC provider fields: email_claim and require_email_verified.
Facebook and some other OAuth providers issue authorization codes that
exceed 512 characters. Pydantic rejected these with 422 string_too_long.
The OAuth spec defines no maximum code length; 2048 aligns with common
provider limits.
Also adds three integration tests to verify 512-char and 2048-char codes
are accepted while 2049-char codes are correctly rejected.
PyJWT compares the iss claim against discovery_issuer with an exact string
match. Authentik (and similar providers) include a trailing slash in the JWT
iss claim while the discovery document issuer may omit it, or vice-versa.
Disable PyJWT built-in issuer validation and compare both sides after
rstrip('/') to make the check slash-agnostic.
Adds a regression test that verifies a login succeeds when the provider is
configured without a trailing slash but the JWT iss claim carries one.