Skip to content

Conversation

@KaveeshaPiumini
Copy link
Contributor

Purpose

This pull request introduces support for usernameless (discoverable credential) passkey authentication, allowing users to authenticate without providing a username or user ID. The changes include updates to the authentication flow, service logic, validation, and comprehensive test coverage to ensure usernameless authentication works correctly alongside traditional username-based flows.

Usernameless Passkey Authentication Support

  • Added a new authentication flow definition for usernameless passkey authentication (authentication/auth_flow_usernameless_passkey.json).
  • Updated the passkeyService logic to detect when the user ID is empty and initiate usernameless authentication using discoverable credentials, including handling both the challenge and verification steps. [1] [2] [3] [4]
  • Extended the WebAuthn service interface and implementation to support usernameless authentication, adding BeginDiscoverableLogin and ValidatePasskeyLogin methods. [1] [2] [3]

Validation and Flow Improvements

  • Relaxed input validation to allow empty user IDs for usernameless authentication, removing the requirement for a user ID in validateAuthenticationStartRequest.
  • Updated flow executor logic to skip optional prerequisites, supporting flows that do not require a user to be pre-authenticated.

Testing and Test Coverage

  • Updated and added unit and integration tests to verify usernameless authentication, including changes to mock expectations and assertions for empty user IDs. [1] [2] [3]

Other Notable Changes

  • Annotated struct fields and refactored code for clarity and to support new authentication modes.

These changes collectively enable usernameless passkey authentication, improve the flexibility of authentication flows, and ensure robust test coverage for both usernameless and traditional username-based authentication.

Related Issues

Related PRs

  • N/A

Checklist

  • Followed the contribution guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
  • Tests provided. (Add links if there are any)
    • Unit Tests
    • Integration Tests
  • Breaking changes. (Fill if applicable)
    • Breaking changes section filled.
    • breaking change label added.

Security checks

  • Followed secure coding standards in WSO2 Secure Coding Guidelines
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets.

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

This pull request aims to introduce usernameless (discoverable credential) passkey authentication support. The changes include updates to the service layer, WebAuthn interface, validation logic, authentication flow definitions, and test coverage.

Changes:

  • Added new WebAuthn service methods (BeginDiscoverableLogin and ValidatePasskeyLogin) to support discoverable credentials
  • Modified the passkey service to detect empty userID and handle usernameless authentication flows, including userID resolution from userHandle
  • Removed userID validation requirement to allow empty userID for usernameless authentication
  • Added prerequisite skipping logic for optional prerequisites in flow execution
  • Added usernameless passkey authentication flow definition
  • Updated tests to cover usernameless authentication scenarios

Reviewed changes

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

Show a summary per file
File Description
backend/cmd/server/bootstrap/flows/authentication/auth_flow_usernameless_passkey.json New authentication flow definition for usernameless passkey authentication
backend/internal/authn/passkey/model.go Added omitempty JSON tag to UserID field to support optional userID
backend/internal/authn/passkey/service.go Added usernameless flow detection and handling in StartAuthentication and FinishAuthentication methods
backend/internal/authn/passkey/webauthn_service.go Added BeginDiscoverableLogin and ValidatePasskeyLogin methods to support discoverable credentials
backend/internal/authn/passkey/utils.go Removed userID validation requirement from authentication start request validation
backend/internal/authn/passkey/service_test.go Updated test to expect usernameless authentication to succeed with empty userID
backend/internal/flow/core/executor.go Added logic to skip optional prerequisites during validation
backend/internal/flow/executor/passkey_executor_test.go Updated test to verify usernameless authentication succeeds when userID is not provided
tests/integration/authn/passkey_auth_test.go Updated integration test to verify usernameless authentication with empty userID

Comment on lines +484 to +488
userHandler := func(rawID, userHandle []byte) (webauthnUserInterface, error) {
// The user has already been resolved and validated above
// Just return the webAuthnUser we created
return webAuthnUser, nil
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The userHandler implementation for usernameless authentication does not correctly use the rawID and userHandle parameters provided by the WebAuthn library. Instead, it returns a pre-resolved webAuthnUser that was created earlier (lines 439-467). This means the library cannot validate that the credential's userHandle matches the authenticated user, creating a potential security vulnerability.

The userHandler should use the rawID and userHandle parameters to look up the user from the database and return a webAuthnUser based on those credentials. The current implementation defeats the purpose of ValidatePasskeyLogin, which is designed to resolve the user from the credential itself.

Copilot uses AI. Check for mistakes.
Comment on lines 196 to 221
func (suite *PasskeyAuthExecutorTestSuite) TestExecuteChallenge_MissingUserID() {
ctx := createPasskeyNodeContext(passkeyExecutorModeChallenge, common.FlowTypeAuthentication)
// Not setting userID in RuntimeData
// Not setting userID in RuntimeData - this triggers usernameless flow

expectedStartData := &passkey.PasskeyAuthenticationStartData{
SessionToken: testSessionToken,
PublicKeyCredentialRequestOptions: passkey.PublicKeyCredentialRequestOptions{
Challenge: "dGVzdC1jaGFsbGVuZ2U=",
},
}

// Mock passkey service for usernameless authentication (empty UserID)
suite.mockPasskeyService.On("StartAuthentication", mock.MatchedBy(
func(req *passkey.PasskeyAuthenticationStartRequest) bool {
return req.UserID == "" && req.RelyingPartyID == testRelyingPartyID
})).Return(expectedStartData, nil)

resp, err := suite.executor.Execute(ctx)

// Usernameless flow should succeed
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), resp)
assert.Equal(suite.T(), common.ExecFailure, resp.Status)
assert.Contains(suite.T(), resp.FailureReason, "User ID is required")
assert.Equal(suite.T(), common.ExecComplete, resp.Status)
assert.Equal(suite.T(), testSessionToken, resp.RuntimeData[runtimePasskeySessionToken])
assert.NotEmpty(suite.T(), resp.AdditionalData[runtimePasskeyChallenge])
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The test mocks ValidatePrerequisites to always return true (see line 121 in the test setup), which bypasses the actual prerequisite validation logic. This means the test doesn't verify whether usernameless authentication would work with the real prerequisite validation. In production, the passkey executor has a userID prerequisite marked as Required: true, which would cause the validation to fail when userID is empty. The test should either:

  1. Test with the real ValidatePrerequisites logic to ensure the prerequisite is properly marked as optional, or
  2. Mock the prerequisite configuration to mark userID as optional for usernameless flow testing.

Without this, the test gives false confidence that usernameless authentication works when it may fail in production.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +135
// Skip optional prerequisites
if !prerequisite.Required {
continue
}

Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The PR description states "These changes collectively enable usernameless passkey authentication" and the tests suggest it works, but critical code in the executor layer (passkey_executor.go) was not updated to support usernameless flow:

  1. The userID prerequisite is still marked as Required: true in the executor, which will cause prerequisite validation to fail when userID is empty
  2. The executeChallenge method still explicitly checks for empty userID and returns a failure

These issues are not visible in the test results because the tests mock ValidatePrerequisites to always return true, bypassing the real validation logic. While the service layer properly supports usernameless authentication, the flow execution path will fail.

For usernameless authentication to work through flow execution, the executor code needs to be updated to mark the userID prerequisite as optional and remove the userID validation check in executeChallenge.

Copilot uses AI. Check for mistakes.
@codecov
Copy link

codecov bot commented Feb 3, 2026

Codecov Report

❌ Patch coverage is 45.45455% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.81%. Comparing base (a72fbc4) to head (d81c8dd).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
backend/internal/authn/passkey/service.go 48.57% 29 Missing and 7 partials ⚠️
backend/internal/authn/passkey/webauthn_service.go 11.11% 16 Missing ⚠️
backend/internal/flow/core/executor.go 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1268      +/-   ##
==========================================
- Coverage   89.12%   88.81%   -0.31%     
==========================================
  Files         622      622              
  Lines       40357    40441      +84     
  Branches     2371     2371              
==========================================
- Hits        35967    35918      -49     
- Misses       2328     2396      +68     
- Partials     2062     2127      +65     
Flag Coverage Δ
backend-integration-postgres 53.60% <27.08%> (-0.03%) ⬇️
backend-integration-sqlite 53.57% <27.08%> (-0.03%) ⬇️
backend-unit 79.30% <40.40%> (-0.48%) ⬇️
frontend-apps-develop-unit 90.76% <ø> (+0.01%) ⬆️
frontend-apps-gate-unit 84.62% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant