Skip to content

feat: Add MFA challenge UI pages and demo support#60

Open
devondragon wants to merge 2 commits intomainfrom
feature/mfa-challenge-ui
Open

feat: Add MFA challenge UI pages and demo support#60
devondragon wants to merge 2 commits intomainfrom
feature/mfa-challenge-ui

Conversation

@devondragon
Copy link
Owner

Summary

Companion to SpringUserFramework#268 / PR #272. Closes #59.

  • WebAuthn challenge page (/user/mfa/webauthn-challenge.html) — shown when a partially-authenticated user needs to verify with their passkey after password login
  • MFA status on profile page — extends the auth-methods card to show MFA badges (active, fully authenticated, satisfied/missing factors) via GET /user/mfa/status
  • Configuration — adds user.mfa block to application.yml with PASSWORD + WEBAUTHN factors
  • Playwright tests — challenge page structure and MFA status endpoint tests
  • Dependency bumpds-spring-user-framework 4.2.1 → 4.2.2-SNAPSHOT

Files changed

File Change
build.gradle Library version bump to 4.2.2-SNAPSHOT
application.yml Added user.mfa config block
application-playwright-test.yml Added user.mfa.enabled: false
PageController.java Added GET route for challenge page
templates/user/mfa/webauthn-challenge.html New — challenge page template
static/js/user/mfa-webauthn-challenge.js New — reuses authenticateWithPasskey()
templates/user/update-user.html Added MFA status container div
static/js/user/webauthn-manage.js Added updateMfaStatusUI() + createBadge()
playwright/tests/mfa/mfa-challenge.spec.ts New — E2E tests

How MFA works

  1. User logs in with password → gets PASSWORD factor
  2. Spring Security sees WEBAUTHN factor is missing → redirects to challenge page
  3. User clicks "Verify with Passkey" → completes WebAuthn assertion
  4. Both factors satisfied → fully authenticated → redirected to original request

Test plan

  • cd SpringUserFramework && ./gradlew publishLocal (version 4.2.2-SNAPSHOT)
  • ./gradlew bootRun --args='--spring.profiles.active=local' — login with password, verify redirect to challenge page, complete passkey verification
  • ./gradlew test — verify no regressions
  • Visit profile page → verify MFA status badges appear in auth-methods card
  • curl http://localhost:8080/user/mfa/status → verify JSON response shape

Add WebAuthn challenge page for MFA second-factor verification,
MFA status display on the user profile page, and Playwright tests.
Companion to SpringUserFramework#268 / PR #272.

- Add user.mfa config block to application.yml (PASSWORD + WEBAUTHN)
- Create /user/mfa/webauthn-challenge.html template and JS module
- Add controller route in PageController for the challenge page
- Extend auth-methods card on profile page with MFA status badges
- Add Playwright tests for challenge page structure and MFA status endpoint
- Disable MFA in playwright-test profile to keep existing tests unaffected
- Bump ds-spring-user-framework to 4.2.2-SNAPSHOT

Closes #59
Copilot AI review requested due to automatic review settings March 2, 2026 18:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds demo-app UI support for Spring Security MFA (password + WebAuthn), including a dedicated WebAuthn challenge page, profile-page MFA status badges, and Playwright coverage to validate the new UI/endpoint wiring.

Changes:

  • Added a new /user/mfa/webauthn-challenge.html Thymeleaf page plus module JS to perform WebAuthn assertion and redirect.
  • Extended the profile “Authentication Methods” card to render MFA status badges sourced from GET /user/mfa/status.
  • Added MFA configuration defaults and Playwright tests; bumped ds-spring-user-framework to 4.2.2-SNAPSHOT.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
build.gradle Bumps demo dependency to framework snapshot that includes MFA support.
src/main/resources/application.yml Enables MFA and configures required factors + entry-point URIs.
src/main/resources/application-playwright-test.yml Disables MFA for Playwright profile runs.
src/main/java/com/digitalsanctuary/spring/demo/controller/PageController.java Adds route to render the MFA WebAuthn challenge page.
src/main/resources/templates/user/mfa/webauthn-challenge.html New challenge page template shown when WebAuthn factor is missing.
src/main/resources/static/js/user/mfa-webauthn-challenge.js New challenge page logic that calls authenticateWithPasskey() and redirects.
src/main/resources/templates/user/update-user.html Adds an MFA status container section for JS-populated badges.
src/main/resources/static/js/user/webauthn-manage.js Fetches MFA status and renders badges in the auth-methods card.
playwright/tests/mfa/mfa-challenge.spec.ts New E2E checks for challenge page structure and MFA status endpoint behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +277 to +280
if (!response.ok) {
container.classList.add('d-none');
return;
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

updateMfaStatusUI() currently hides the MFA section on any non-OK response, which can mask real server-side errors (500s) and make debugging harder. Consider only hiding on 404 (MFA disabled) and treating other statuses as an error state (log + optional user-visible message).

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +68
// With MFA disabled in playwright-test profile, expect 404
// With MFA enabled, expect 200 with proper response shape
expect([200, 404]).toContain(response.status());

if (response.status() === 200) {
const body = await response.json();
expect(typeof body.mfaEnabled).toBe('boolean');
expect(typeof body.fullyAuthenticated).toBe('boolean');
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

This test is non-deterministic for the playwright-test profile: application-playwright-test.yml sets user.mfa.enabled: false, so /user/mfa/status should consistently return 404. Allowing both 200 and 404 weakens the assertion and can hide regressions; consider asserting 404 here and adding a separate test/project/profile where MFA is enabled to assert the 200 + JSON shape case.

Suggested change
// With MFA disabled in playwright-test profile, expect 404
// With MFA enabled, expect 200 with proper response shape
expect([200, 404]).toContain(response.status());
if (response.status() === 200) {
const body = await response.json();
expect(typeof body.mfaEnabled).toBe('boolean');
expect(typeof body.fullyAuthenticated).toBe('boolean');
}
// With MFA disabled in playwright-test profile, this endpoint should return 404
expect(response.status()).toBe(404);
// NOTE: When adding a profile/project with MFA enabled, create a separate test
// that asserts a 200 response and validates the JSON response shape.
// This keeps the playwright-test profile deterministic.

Copilot uses AI. Check for mistakes.
Comment on lines +263 to +266
/**
* Update the MFA Status section in the auth-methods card.
* Silently hides the container if the MFA status endpoint returns 404 (MFA disabled).
*/
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The JSDoc says the MFA status container is hidden only when the endpoint returns 404, but the current implementation hides it for any non-2xx response. Either update the comment to match the behavior, or change the logic to check response.status === 404 specifically and handle other error statuses differently (e.g., log/show a message).

Copilot uses AI. Check for mistakes.
- Check for 404 specifically in updateMfaStatusUI() instead of hiding
  on any non-OK response; log a warning for other error statuses
- Update JSDoc to document both 404 and non-OK handling behaviors
- Make MFA status test assertion deterministic (expect 404 when MFA
  is disabled in playwright-test profile, not [200, 404])
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.

feat: Add MFA challenge UI pages and demo support

2 participants