Skip to content

feat(webauthn): add mfa option to passkey enrollment (update)#1594

Merged
dorsha merged 3 commits into
mainfrom
feat/webauthn-update-mfa
Jun 28, 2026
Merged

feat(webauthn): add mfa option to passkey enrollment (update)#1594
dorsha merged 3 commits into
mainfrom
feat/webauthn-update-mfa

Conversation

@dorsha

@dorsha dorsha commented Jun 28, 2026

Copy link
Copy Markdown
Member

What

Adds an optional login_options to the WebAuthn passkey-enrollment flow (sync + async) so it can return a merged-amr session in a single ceremony:

client.webauthn.update_start(login_id, refresh_token, origin, LoginOptions(mfa=True))
# ... browser ceremony ...
jwt = client.webauthn.update_finish(transaction_id, response)  # merged-amr session (e.g. ["pwd","webauthn","mfa"]) or None

Why

Enrolling a passkey for an already-authenticated user registered the credential but minted no session — getting a session reflecting the new passkey required a second WebAuthn ceremony (sign_in_start(mfa=True) + finish). Reported by PerciHealth (descope/etc#16573); pairs with the backend change.

Changes

  • update_start (sync webauthn.py + async webauthn_async.py) gains an optional trailing login_options: Optional[LoginOptions] = None (non-breaking), composed into the request body only when provided (shared _webauthn_base._compose_update_start_body).
  • update_finish gains an optional audience kwarg and now returns Optional[dict]: the JWT response (parsed from the nested jwt) when the enrollment was mfa, else None (default flow). Sync uses generate_jwt_response; async uses prepare_jwt_response.

Both changes are runtime backward-compatible (new optional params; None return for the default flow).

Tests

test_compose_update_start_body (mfa body), test_update_start_with_mfa, test_update_finish_with_mfa, plus the existing default-flow tests — all run in both sync and async modes. tests/test_webauthn.py passes (25 cases).

🤖 Generated with Claude Code

Add an optional login_options to webauthn update_start (sync + async) so enrolling a
passkey for an already-authenticated user can return a single session whose amr merges
the previously-passed factors with the new passkey, instead of a separate sign-in.
update_finish now returns that JWT response (nested under "jwt") when present, else None.

Pairs with descope/backend webauthn add-device mfa support (descope/etc#16573).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shuni-bot-dev

shuni-bot-dev Bot commented Jun 28, 2026

Copy link
Copy Markdown

🐕 Review complete — View session on Shuni Portal 🐾

@shuni-bot-dev

shuni-bot-dev Bot commented Jun 28, 2026

Copy link
Copy Markdown

🐕 Suggested Reviewers

The reviewers were selected based on recent contributions to the key files involved in WebAuthn implementation and testing, ensuring they have relevant and current context to review the changes effectively.

Reviewer Reason
guyp-descope Modified 'descope/authmethod/webauthn.py' and 'tests/test_webauthn.py'; knowledgeable in WebAuthn flow and testing.
itaihanski Contributed to 'descope/authmethod/webauthn.py' and its tests; experienced with WebAuthn implementation and test coverage.
omercnet Worked on 'descope/authmethod/webauthn.py' and its tests; suitable for reviewing updates to WebAuthn logic.
tebeka Contributed to 'descope/authmethod/webauthn.py' and testing modules; familiar with WebAuthn integration.

Suggested by Shuni based on git history and PR context. Names are not @-mentioned to avoid notifying anyone — request a review from whoever fits best.

@shuni-bot-dev shuni-bot-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐕 Shuni's Review

Adds an optional login_options to the WebAuthn passkey-enrollment flow so update_finish can return a merged-amr session in one ceremony (sync + async).

No issues found — good bones! Serialization (login_options.__dict__), the sync/async generate_jwt_response/prepare_jwt_response split, and the optional-param backward compat all match the sibling sign-in/sign-up flows. Tests cover the mfa and default paths in both modes. Woof!

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown

Coverage report

The coverage rate went from 98.04% to 98.05% ⬆️

100% of new lines are covered.

Diff Coverage details (click to unfold)

descope/authmethod/webauthn.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/_webauthn_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/webauthn_async.py

100% of new lines are covered (100% of the complete file).

dorsha and others added 2 commits June 28, 2026 09:52
Fixes the Lint & Type Check (ruff format --check) CI failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the OTP update phone/email API: webauthn update_start (sync + async) takes an
optional mfa bool (sent as the 'mfa' request field) instead of a LoginOptions,
mirroring the backend change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dorsha dorsha enabled auto-merge (squash) June 28, 2026 07:49
@dorsha dorsha merged commit dc94421 into main Jun 28, 2026
34 checks passed
@dorsha dorsha deleted the feat/webauthn-update-mfa branch June 28, 2026 08:21
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.

2 participants