Skip to content

feat(2fa): implement step-up authentication - #3215

Open
FrankApiyo wants to merge 1 commit into
mainfrom
feat/step-up-authentication
Open

FrankApiyo wants to merge 1 commit into
mainfrom
feat/step-up-authentication

Conversation

@FrankApiyo

@FrankApiyo FrankApiyo commented Aug 17, 2026

Copy link
Copy Markdown
Member

Changes / Features implemented

Builds on #3207 (merged). Depends on onaio/ona-oidc#139.

#3207 puts sign-in behind a second factor; this gates individual account actions. A gated action is refused until the caller presents a single-use grant earned for that action. Each is opt-in via STEP_UP["ACTIONS"]: API and ODK token regeneration, email change, password change, the require-auth toggle and privacy consent. Profile gates fire only on an actual change, and the server-rendered settings and password-change pages are gated too, so they are not a way around the API.

Two modes via STEP_UP["MODE"]: in local, onadata owns the factor and the caller proves a code at /api/v1/totp/verify; in federated, an identity provider does — /api/v1/stepup/start returns the authorization URL and /api/v1/stepup/callback mints the grant. /api/v1/user/capabilities reports what the deployment can manage. A grant travels in the body or the X-Step-Up-Grant header, which CORS_ALLOW_HEADERS now allows.

Grants live in ona-oidc's stepup_grants; the audience whitelist stays here and its default covers every gated audience. Deploy checks catch misconfiguration: stepup.W001 (a gate bypassable under skip_gate), W002 (a gated audience /totp/verify cannot mint), W003 (an unrecognised MODE), W004 (local gating with ENABLE_TWO_FACTOR off). Documented in docs/two_factor.rst.

Steps taken to verify this change does what is intended

Tests cover policy, challenge dialects, capabilities, the federated round trip, the 409s, the deploy checks, and each gate's refused and grant-accepted paths — including the server-rendered pages and the CORS preflight. Codes come from a real HOTP implementation, and the single-use grant is exercised end to end. Verified against a live Keycloak: sign-in takes the password only; a gated action prompts for the factor and returns acr: gold with a fresh auth_time.

Worth a close read:

  • Federated, no user has a local device, so skip_gate would wave every gated action through; federated mode always challenges instead.
  • Only the token says who finished a step-up, so the callback verifies the subject, not just the state.
  • The consent gate is on the effective post-write value, and a PUT cannot drop the record ungated.
  • The single-use grant is spent once per request even though partial_update delegates to update and both run the gate.

Side effects of implementing this change

Before submitting this PR for review, please make sure you have:

  • Included tests
  • Updated documentation

Closes #

@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 4 times, most recently from 997ea08 to 617dffa Compare August 17, 2026 12:05
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 3 times, most recently from afbbc6a to ecc8ee3 Compare August 19, 2026 07:50
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from d3797e6 to 19d2b1c Compare August 19, 2026 09:07
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 2 times, most recently from 5799b9f to af118e4 Compare August 19, 2026 10:15
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 6d33f67 to 0d13046 Compare August 19, 2026 10:19
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from af118e4 to 1bd7149 Compare August 19, 2026 10:21
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 7532dd9 to d29ef14 Compare August 19, 2026 11:10
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from 1bd7149 to e3c5c58 Compare August 19, 2026 12:35
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 00d9c16 to 5a1adfb Compare August 19, 2026 13:03
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from d0c2148 to 0638997 Compare August 19, 2026 13:04
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 5a1adfb to 4042f52 Compare August 19, 2026 13:11
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 2 times, most recently from 35e5214 to 11c8d60 Compare August 19, 2026 13:28
@FrankApiyo FrankApiyo changed the title feat(2fa): step up before weakening a data protection feat(2fa): gate sensitive actions behind a step-up, local or federated Aug 19, 2026
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from b90ed42 to 3f6f571 Compare August 19, 2026 13:38
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from 11c8d60 to 5fe1dc3 Compare August 19, 2026 13:39
@FrankApiyo FrankApiyo changed the title feat(2fa): gate sensitive actions behind a step-up, local or federated feat(2fa): implement step-up authentication Aug 19, 2026
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from 5fe1dc3 to c6281e3 Compare August 19, 2026 13:53
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 9cd079d to 7a0b620 Compare August 20, 2026 10:00
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from 68df7e3 to 3e692d9 Compare August 20, 2026 10:06
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 7a0b620 to 46c4cba Compare August 20, 2026 12:22
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 2 times, most recently from 1cbeb48 to 2518c4b Compare August 20, 2026 12:46
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from 8ebd0e6 to 4ff7399 Compare August 23, 2026 13:07
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from d76deac to 4ff7399 Compare August 27, 2026 08:35
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 3aaa21f to f72ae01 Compare August 27, 2026 09:16
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from 08156f5 to ab4c589 Compare August 27, 2026 09:20
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch 4 times, most recently from 6432a7e to 335865f Compare August 31, 2026 13:26
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from e402046 to 2aa0911 Compare August 31, 2026 13:46
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 335865f to c98d3d5 Compare August 31, 2026 14:15
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 2 times, most recently from fc589f2 to db5fd2f Compare August 31, 2026 16:38
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from c98d3d5 to 2f4b5e8 Compare September 1, 2026 07:40
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from db5fd2f to cfb3411 Compare September 1, 2026 07:40
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch from 2f4b5e8 to d735885 Compare September 1, 2026 08:04
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 5 times, most recently from c13af42 to 2625413 Compare September 1, 2026 10:10
@FrankApiyo
FrankApiyo force-pushed the feat/django-two-factor-auth branch 4 times, most recently from 2567db3 to faa3d1d Compare September 7, 2026 10:57
Base automatically changed from feat/django-two-factor-auth to main September 10, 2026 06:43
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch 2 times, most recently from abb7e79 to 255262a Compare September 17, 2026 13:03
@FrankApiyo
FrankApiyo force-pushed the feat/step-up-authentication branch from 255262a to 3250246 Compare September 17, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant