v1.8.1 — Login OIDC tab CSP regression hotfix + login_failure reason capture
v1.8.1 — Login OIDC tab CSP regression hotfix + login_failure reason capture
Production hotfix. Two changes bundled.
Bug fix
Login OIDC/Okta tab unclickable (CSP regression from v1.7.0)
Users with both local login + OIDC (Okta) enabled could not click
the Okta tab on /login. The local form stayed visible; clicks on
the Okta nav-tab silently did nothing. Same applied to the LDAP
tab.
Root cause: the v1.7.0 CSP (#514) is enforced with no
'unsafe-inline' in script-src. The per-page nonce mechanism only
exempts nonce-attributed <script> blocks — NOT inline event-handler
HTML attributes (onclick=, onload=, etc.). The login page's tab
buttons used onclick="switchTab('tab-oidc')" which the browser
silently blocks under the enforced CSP. Shipped with v1.7.0 ~3
versions back; operators worked around it in prod with manual
config tweaks.
Fix:
- app/views/sessions/new.html.erb: dropped inline onclick from
the three tab buttons (Local / OIDC / LDAP); the data-tab
attribute was already present.
- app/views/layouts/login.html.erb: added a click-delegation
listener inside the existing nonce'd <script> block keyed off
[data-tab]. Also fixed a latent selector bug: previous
switchTab JS called querySelectorAll('.login-tab') which
found nothing (buttons are .nav-link, not .login-tab), so
the "remove active from other tabs" step was a no-op.
Regression spec asserts: tab buttons render with data-tab + no
inline onclick; nonce'd script block contains addEventListener;
rendered HTML includes "Sign in with Okta" copy when OIDC enabled.
Enhancement (closes #587)
login_failure audit events now capture the failure reason
Pairs naturally with the bug fix above because the next class of
auth bug (Okta misconfig, callback URL mismatch, unmapped user)
currently emits a generic login_failure audit event with just an
email. Operators had to dig into the user record / Rails console
to triage. NIST 800-53 AU-3 expects audit records to capture the
nature of the failure, not just that it happened.
New service app/services/login_failure_reason.rb:
LoginFailureReason.classify(user:, password:) → enum string
unknown_email no user matches the submitted email
no_local_password user exists, password_digest IS NULL
(OAuth-only account)
suspended user.status == "suspended"
invalid_password user exists, password verified false
other escape hatch (should not normally fire)
service_account_web_login and account_deactivated remain as their
dedicated short-circuit branches in
SessionsController#authenticate_local with their existing
audit-log strings unchanged.
Every login_failure and login_success audit event now also carries
auth_method: "local" (or "ldap") so the discriminator is uniform
across success + failure. LDAP failure gains reason:
"ldap_bind_failed".
User-facing flash stays generic ("Invalid email or password") to
prevent email enumeration. The specific reason lives only in the
audit log, which only admins see.
Admin audit-log UI required no changes — show.html.erb already
JSON-pretty-prints the full metadata, so reason + auth_method
appear automatically.
Test result
bundle exec rspec 2532 examples, 0 failures, 2 pending
(+15 new: 6 classifier + 3 tab regression
+ 6 audit-metadata)
bin/rubocop 0 offenses
Upgrade notes
- No env var, config, or schema changes.
- No data migration.
- Operators on the v1.8.0 line should upgrade — the Okta tab bug
affects every deployment with both local login + OIDC enabled,
regardless of provider (Okta / Auth0 / Keycloak / etc.). The
same bug also disables the LDAP tab if both are enabled.
Closes
- Login OIDC tab unclickable (this PR)
- #587 — login_failure reason capture
🤖 Generated with Claude Code