Skip to content

feat: add passkey authentication#286

Open
dmytrocraft wants to merge 136 commits into
mainfrom
codex/issue-221-passkey-auth
Open

feat: add passkey authentication#286
dmytrocraft wants to merge 136 commits into
mainfrom
codex/issue-221-passkey-auth

Conversation

@dmytrocraft

@dmytrocraft dmytrocraft commented May 10, 2026

Copy link
Copy Markdown
Contributor

Description

Adds passkey-based authentication support for sign-up, authenticated passkey registration, and sign-in using WebAuthn.

This includes:

  • REST passkey ceremonies under /api/passkeys/*
  • WebAuthn integration through web-auth/webauthn-lib
  • MongoDB persistence for passkey credentials and challenges
  • passkey service wiring, validation, processors, DTOs, and OpenAPI documentation
  • tests for passkey services, processors, entities, repositories, codecs, and rate-limit behavior

Related Issue

Closes #221

Motivation and Context

Users need a phishing-resistant authentication option that can support sign-up and sign-in without relying only on passwords. The implementation keeps passkey flows in the existing User bounded context and reuses existing session/token issuance patterns.

Performance Changes

  • PasskeyCredential has a unique credential_id index for constant-time assertion credential lookup.
  • PasskeyCredential has a user_id index for authenticated user passkey listing and duplicate checks.
  • PasskeyChallenge has a compound (purpose, user_id) index for challenge cleanup and lookup paths.
  • PasskeyChallenge has a TTL index on expires_at with expireAfterSeconds=0 so MongoDB removes expired challenge records.
  • Passkey completion atomically claims active challenges before verification, preventing replay races without an application-level read/modify/write window.
  • Challenge TTL and ceremony timeout are configurable through PASSKEY_CHALLENGE_TTL_SECONDS and PASSKEY_TIMEOUT_SECONDS.
  • Rate-limit target resolution covers passkey public and authenticated endpoints without user enumeration.

How Has This Been Tested?

Local Docker verification:

  • Unit suite with coverage: 2313 tests, 6425 assertions; Classes 100%, Methods 100%, Lines 100%.
  • Integration suite: 120 tests, 721 assertions.
  • Behat suite: 644 scenarios, 3622 steps.
  • Targeted passkey/repository/rate-limit tests after review fixes: 181 tests, 428 assertions.
  • phpmd src: passed.
  • phpmd tests: passed.
  • phpinsights source: Code 100, Complexity 97.6, Architecture 100, Style 100.
  • phpinsights analyse tests: Code 100, Complexity 97.9, Architecture 100, Style 100.
  • psalm --show-info=false --no-progress: passed.
  • psalm --taint-analysis --show-info=false --no-progress: passed.
  • deptrac analyse --config-file=deptrac.yaml --report-uncovered --fail-on-uncovered: passed.
  • bin/console lint:container: passed.
  • OpenAPI diff against main: backward compatible; six passkey endpoints added.
  • Spectral OpenAPI validation: no hint-or-higher results.
  • git diff --check: passed.

Note: literal make ci could not run locally because another checkout owns the hardcoded development port 8081. Equivalent Docker commands were run with isolated dependency ports, and Behat was run through an internal one-off FrankenPHP server with APP_ENV=test and APP_DEBUG=0.

Screenshots (if appropriate):

N/A

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • I have performed a self-review of my code.
  • I have commented my code, particularly in hard-to-understand areas.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING.md document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • You have only one commit (if not, squash them into one commit).
  • Structurizr documentation has been updated to reflect any architectural changes.

Summary by cubic

Adds WebAuthn passkey sign‑up, authenticated passkey enrollment, and sign‑in over REST and GraphQL with strict validation, hardened rate limits, and a production‑readiness gate. Updates docs, schemas, CI, and tests; implements #221.

  • New Features

    • API: REST /api/passkeys/(signup|signin|register)/(options|complete) and GraphQL passkey mutations; options return challengeId + publicKey, completions return tokens/pendingSessionId or credentialId. OpenAPI/GraphQL specs updated and mutation descriptions normalized; public signup/signin routes allowed.
    • Validation/Storage: OpenAPI transformer enforces request shapes, base64url, and initials; web-auth/webauthn-lib handles attestation/assertion; PasskeyChallenge is TTL and single‑use; PasskeyCredential is unique and discoverable‑only; public suffix list bundled.
    • Rate limiting: global limiter runs first; REST/GraphQL sign‑in keyed by IP+email. GraphQL limiter inspects operation name, variables (incl. defaults), and fragments, scopes checks to the selected operation, supports raw JSON and raw GraphQL POST, and excludes memory probes.
    • CQRS/Infra: command handlers for passkey options/completions; ODM mappings added; access‑token persistence detaches anonymous tokens.
    • Readiness/Docs/CI: production readiness gate (503 until enabled) via env flags; docs and planning evidence added; Schemathesis covers passkey option routes; K6 load covers option routes; workflows pin Actions and restrict permissions; CI asserts test‑ and production‑mode readiness.
  • Migration

    • Set PASSKEY_* envs (RP_ID, RP_NAME, ALLOWED_ORIGINS, TIMEOUT_SECONDS, CHALLENGE_TTL_SECONDS, PRODUCTION_TRAFFIC_ENABLED, PRODUCTION_MONITORING_READY).
    • Ensure MongoDB collections and TTL/unique indexes exist.
    • Update clients to the new REST endpoints or GraphQL mutations; run make passkey-test-readiness and make passkey-production-readiness before enabling production traffic.

Written for commit e9acebc. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Passkey (WebAuthn) support: REST + GraphQL flows for sign‑up, sign‑in, and authenticated enrollment (options + complete), token/cookie issuance, and two‑factor pending‑session handling; API schemas updated.
  • Configuration

    • New passkey environment settings (RP ID/name, allowed origins, timeouts, challenge TTL); public routes for passkey endpoints; WebAuthn dependency added.
  • Documentation

    • Comprehensive passkey docs, architecture, PRD, runbooks, and manual test checklists.
  • Tests

    • Broad unit, integration and load tests covering passkey flows, transformers, validators, repos, and rate‑limit behavior.

Review Change Stack

BMAD Planning Evidence

  • BMAD planning artifacts: specs/passkey-authentication/
  • The planning bundle covers product brief, PRD, research, architecture, epics, implementation readiness, and run summary for issue Feature: Add passkey-based authentication support for sign in and sign up #221 before implementation.
  • The PR maps that plan to passkey registration, sign-in ceremonies, persistence, validation, API docs, and tests.

@dmytrocraft dmytrocraft requested a review from Kravalg May 10, 2026 16:32
@qodo-code-review

Copy link
Copy Markdown
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 195 files, which is 45 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5f6bceb9-139a-4e42-b563-2af06f5b88bf

📥 Commits

Reviewing files that changed from the base of the PR and between f9bc70b and e9acebc.

⛔ Files ignored due to path filters (2)
  • composer.lock is excluded by !**/*.lock
  • config/passkey/public_suffix_list.dat is excluded by !**/*.dat
📒 Files selected for processing (195)
  • .agents/skills/bmad-adversarial-review/SKILL.md
  • .agents/skills/bmad-edge-case-hunter/SKILL.md
  • .agents/skills/bmad-qa-automate/SKILL.md
  • .agents/skills/bmad-qa/SKILL.md
  • .claude/skills/AI-AGENT-GUIDE.md
  • .claude/skills/code-review/reference/fr-nfr-quality-gate.md
  • .claude/skills/code-review/reference/quality-standards.md
  • .claude/skills/quality-standards/SKILL.md
  • .claude/skills/testing-workflow/SKILL.md
  • .devcontainer/workspace-settings.env
  • .env
  • .env.load_test
  • .env.test
  • .github/graphql-spec/spec
  • .github/openapi-spec/spec.yaml
  • .github/workflows/E2Etests.yml
  • .github/workflows/autorelease.yml
  • .github/workflows/bats-tests.yml
  • .github/workflows/cache-performance-tests.yml
  • .github/workflows/code-lint-and-prettify.yml
  • .github/workflows/codecov.yml
  • .github/workflows/copilot-setup-steps.yml
  • .github/workflows/deptrac.yml
  • .github/workflows/eslint.yaml
  • .github/workflows/graphql-diff.yml
  • .github/workflows/infection.yml
  • .github/workflows/load-tests.yml
  • .github/workflows/memory-tests.yml
  • .github/workflows/openapi-diff.yml
  • .github/workflows/openapi-validate.yml
  • .github/workflows/phpinsights.yml
  • .github/workflows/psalm.yml
  • .github/workflows/schemathesis.yml
  • .github/workflows/symfony.yml
  • .github/workflows/template-sync.yml
  • .github/workflows/tests.yml
  • .gitignore
  • Dockerfile
  • Makefile
  • README.md
  • composer.json
  • config/api_platform/resources/AuthPayload.yaml
  • config/api_platform/resources/EmptyResponse.yaml
  • config/services.yaml
  • config/services_test.yaml
  • docker-compose.load-tests.yml
  • docker-compose.prod.yml
  • docker-compose.yml
  • docs/advanced-configuration.md
  • docs/getting-started.md
  • docs/onboarding.md
  • docs/passkey-authentication.md
  • docs/performance.md
  • docs/planning/README.md
  • docs/planning/architecture.md
  • docs/planning/epics.md
  • docs/planning/implementation-readiness.md
  • docs/planning/prd.md
  • scripts/ai-review-loop.sh
  • scripts/ai-review-prompts/fix.md
  • scripts/ai-review-prompts/review.md
  • scripts/get-pr-comments.sh
  • scripts/local-coder/install-bmalph.sh
  • scripts/local-coder/lib/bmalph.sh
  • scripts/normalize-graphql-passkey-descriptions.php
  • scripts/schemathesis-validate.sh
  • specs/passkey-authentication/architecture.md
  • specs/passkey-authentication/current-head-impact-context.md
  • specs/passkey-authentication/epics.md
  • specs/passkey-authentication/manual-browser-evidence.md
  • specs/passkey-authentication/manual-browser-run-1780280604724-451d4e.sanitized.md
  • specs/passkey-authentication/manual-test-checklist.md
  • specs/passkey-authentication/nfr-catalog-evidence.md
  • specs/passkey-authentication/passkey-load-run-20260601T022759Z.sanitized.md
  • specs/passkey-authentication/run-summary.md
  • src/OAuth/Infrastructure/Adapter/AccessTokenManager.php
  • src/Shared/Application/EventListener/ApiRateLimitListener.php
  • src/Shared/Application/OpenApi/Transformer/PasskeyCredentialRequestSchemaTransformer.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitClientIdentityResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlAuthTargetResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlDocumentResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlFieldValueResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlObjectFieldResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlPayloadResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlQueryInspection.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlQueryInspector.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlRootFields.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlVariableValueResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitNestedPayloadStringResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitOAuthSocialTargetResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitPayloadValueResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolver.php
  • src/User/Application/CommandHandler/CompletePasskeyRegistrationCommandHandler.php
  • src/User/Application/CommandHandler/CompletePasskeySignInCommandHandler.php
  • src/User/Application/CommandHandler/CompletePasskeySignUpCommandHandler.php
  • src/User/Application/DTO/PasskeyConfiguration.php
  • src/User/Application/EventListener/PasskeyGraphQlRequestResolver.php
  • src/User/Application/EventListener/PasskeyProductionReadinessListener.php
  • src/User/Application/Factory/IssuedSessionFactory.php
  • src/User/Application/Resolver/PasskeyChallengeResolver.php
  • src/User/Application/Resolver/PasskeyCredentialResolver.php
  • src/User/Application/Service/PasskeyAuthenticationIssuer.php
  • src/User/Application/Validator/PasskeyAllowedOriginNormalizer.php
  • src/User/Application/Validator/PasskeyRpIdValidator.php
  • src/User/Domain/Entity/PasskeyChallenge.php
  • src/User/Domain/Repository/PasskeyChallengeRepositoryInterface.php
  • src/User/Infrastructure/Command/AssertPasskeyProductionReadinessCommand.php
  • src/User/Infrastructure/Repository/MongoDBPasskeyChallengeRepository.php
  • tests/CLI/bats/make_ai_review_loop_tests.bats
  • tests/CLI/bats/make_bmalph_tests.bats
  • tests/CLI/bats/make_negative_tests.bats
  • tests/CLI/bats/make_pr_comments_tests.bats
  • tests/Integration/Auth/ApiRateLimitListenerIntegrationTest.php
  • tests/Integration/Auth/ApiRateLimitListenerPasskeyIntegrationTest.php
  • tests/Integration/Auth/ApiRateLimitListenerTestCase.php
  • tests/Integration/Auth/GraphQLAuthSupportTest.php
  • tests/Integration/Auth/PasskeyAuthEndpointsIntegrationTest.php
  • tests/Integration/Auth/PasskeyGraphQLAuthIntegrationTestCase.php
  • tests/Integration/Auth/PasskeyGraphQLAuthOptionsIntegrationTest.php
  • tests/Integration/Auth/PasskeyGraphQLCompletionFailureTest.php
  • tests/Integration/Auth/PasskeyGraphQLCompletionResponseTest.php
  • tests/Integration/User/Infrastructure/Command/AssertPasskeyProductionReadinessCommandIntegrationTest.php
  • tests/Load/config.json.dist
  • tests/Load/config.prod.json
  • tests/Load/load-tests-prepare-oauth-client.sh
  • tests/Load/recover-load-test-runtime.sh
  • tests/Load/reset-load-test-state.sh
  • tests/Load/run-average-load-tests.sh
  • tests/Load/run-load-tests.sh
  • tests/Load/run-scenario-suite.sh
  • tests/Load/run-smoke-load-tests.sh
  • tests/Load/run-spike-load-tests.sh
  • tests/Load/run-stress-load-tests.sh
  • tests/Load/run-worker-memory-soak.sh
  • tests/Load/scripts/rest-api/getUsers.js
  • tests/Load/scripts/rest-api/oauth.js
  • tests/Load/scripts/rest-api/passkeyRegistrationOptions.js
  • tests/Load/scripts/rest-api/passkeySigninOptions.js
  • tests/Load/scripts/rest-api/passkeySignupOptions.js
  • tests/Load/utils/insertUsersUtils.js
  • tests/Load/utils/utils.js
  • tests/Load/wait-for-oauth-client-pool.sh
  • tests/Memory/GraphQL/GraphQLMemoryWebTestCase.php
  • tests/Memory/Inventory/MemoryCoverageInventory.php
  • tests/Memory/Rest/PasskeyOptionsMemoryTest.php
  • tests/Memory/Rest/RestMemoryScenarioInventory.php
  • tests/Memory/Support/MemoryCoverageCatalog.php
  • tests/Shared/Auth/Support/ControllableCommandBus.php
  • tests/Unit/OAuth/Infrastructure/Adapter/AccessTokenManagerTest.php
  • tests/Unit/Shared/Application/EventListener/ApiRateLimitListenerTest.php
  • tests/Unit/Shared/Application/EventListener/GraphQLBatchRejectListenerTest.php
  • tests/Unit/Shared/Application/OpenApi/PublicPasskeySecuritySpecTest.php
  • tests/Unit/Shared/Application/OpenApi/Transformer/PasskeyCredentialRequestSchemaTransformerTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolverPasskeyTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlQueryInspectionDefaultValueTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitGraphQlQueryInspectionTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitPayloadValueResolverInvalidGraphQlFallbackTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitPayloadValueResolverTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolverGraphQlFragmentLimitersTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolverGraphQlLimitersTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolverGraphQlPayloadLimitersTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolverLimitersTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/GraphQlPasskeySigninLimitersTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/GraphQlRateLimitTestCase.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/RateLimitClientTestCase.php
  • tests/Unit/Shared/Auth/Support/ControllableCommandBusTest.php
  • tests/Unit/Shared/Infrastructure/Serializer/ContextAssertingSerializer.php
  • tests/Unit/Shared/Infrastructure/Validator/AccessTokenValidatorTokenStructureTest.php
  • tests/Unit/User/Application/CommandHandler/PasskeyCredentialSaveFailureCommandHandlerTest.php
  • tests/Unit/User/Application/CommandHandler/PasskeyPostClaimCleanupCommandHandlerTest.php
  • tests/Unit/User/Application/CommandHandler/PasskeyRegistrationCommandHandlerTest.php
  • tests/Unit/User/Application/CommandHandler/PasskeyRegistrationCommandHandlerTestSupport.php
  • tests/Unit/User/Application/CommandHandler/PasskeySignInCommandHandlerTest.php
  • tests/Unit/User/Application/CommandHandler/PasskeySignInCommandHandlerTestSupport.php
  • tests/Unit/User/Application/CommandHandler/PasskeySignUpAuthenticationRollbackTest.php
  • tests/Unit/User/Application/DTO/PasskeyConfigurationProductionTest.php
  • tests/Unit/User/Application/DTO/PasskeyConfigurationTest.php
  • tests/Unit/User/Application/EventListener/PasskeyProductionReadinessListenerTestCase.php
  • tests/Unit/User/Application/EventListener/PasskeyReadinessGraphQlBlockingTest.php
  • tests/Unit/User/Application/EventListener/PasskeyReadinessGraphQlResolutionTest.php
  • tests/Unit/User/Application/EventListener/PasskeyReadinessListenerTest.php
  • tests/Unit/User/Application/Factory/IssuedSessionFactoryTest.php
  • tests/Unit/User/Application/Factory/PasskeyOptionsFactoryTest.php
  • tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
  • tests/Unit/User/Application/Resolver/PasskeyAuthMutationResolverConstructionTest.php
  • tests/Unit/User/Application/Resolver/PasskeyChallengeRegistrationResolverTest.php
  • tests/Unit/User/Application/Resolver/PasskeyResolverTest.php
  • tests/Unit/User/Application/Transformer/PasskeyJsonTransformerTest.php
  • tests/Unit/User/Application/Validator/PasskeyAllowedOriginNormalizerTest.php
  • tests/Unit/User/Application/Validator/PasskeyRpIdValidatorTest.php
  • tests/Unit/User/Infrastructure/Command/AssertPasskeyProductionReadinessCommandMutationTest.php
  • tests/Unit/User/Infrastructure/Command/AssertPasskeyProductionReadinessCommandTest.php
  • tests/Unit/User/Infrastructure/Repository/LoadTestUsersFileRepositoryTest.php
  • tests/Unit/User/Infrastructure/Repository/MongoDBPasskeyRepositoryTest.php

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end WebAuthn/passkey support: new env and DI, MongoDB mappings and repositories, domain entities/DTOs/factories/validators, command handlers and processors, OpenAPI/GraphQL and API wiring, rate-limit updates, extensive tests, and documentation.

Changes

Passkey authentication end-to-end

Layer / File(s) Summary
Config, env, dependency
.env, .env.test, .env.load_test, composer.json, config/services*.yaml, config/services_test.yaml, config/packages/security.yaml
Adds PASSKEY_* env vars, web-auth/webauthn-lib dependency, DI wiring for passkey services, test resolver decoration, and public access rules for passkey signup/signin endpoints.
Domain & persistence
src/User/Domain/*, config/doctrine/User/*.mongodb.xml
Adds PasskeyChallenge, PasskeyCredential, PasskeyChallengeContext, PasskeyCredentialCollection, repository interfaces, and MongoDB ODM mappings including TTL and atomic claim/update semantics.
DTOs & validation
src/User/Application/DTO/*, config/validator/validation.yaml
New DTOs for options/complete flows, PasskeyConfiguration, PasskeyAuthenticationResult, and validation rules for signup/signin/registration DTOs.
Transformers & WebAuthn adapters
src/User/Application/Transformer/*, src/User/Application/Factory/PasskeyWebauthnFactory*, src/Shared/Application/OpenApi/Transformer/*
Adds JSON/encoding transformers, PasskeyWebauthnFactory and interface, OpenAPI request-schema transformer to shape credential request schemas.
Factories & services
src/User/Application/Factory/*, src/User/Application/Service/*
Options/response/credential/user/authentication factories, PasskeyAuthenticationIssuer, PasskeyTwoFactorHandler, PasskeyOptionsFactory, public-key options factory, and credential factories.
Validators & resolvers
src/User/Application/Validator/*, src/User/Application/Resolver/*
Attestation/assertion credential record validators, credential validator, credential/challenge/user resolvers, response resolver, and credential resolver with saveUniqueAndRun semantics.
Commands, handlers & processors
src/User/Application/Command*, src/User/Application/CommandHandler/*, src/User/Application/Processor/*
CQRS Start/Complete commands and handlers for signup/registration/signin; API Platform processors to expose REST endpoints and GraphQL mutation resolvers for same flows.
API surface (REST & GraphQL)
config/api_platform/resources/*.yaml, .github/openapi-spec/spec.yaml, .github/graphql-spec/spec, config/serialization/AuthPayload.yaml
Adds six REST POST /api/passkeys/* endpoints and six GraphQL passkey mutations; updates OpenAPI and GraphQL specs and AuthPayload serialization fields.
Rate-limiting and test context
src/Shared/Application/Resolver/RateLimit/*, tests/Behat/*
Extends sign-in/registration limiter paths to include passkey endpoints; adds Behat test HTTP request IP decorator/header support.
Tests & docs
tests/**, docs/*, specs/passkey-authentication/*, tests/Integration/Auth/*
Extensive unit/integration/Behat tests for DTOs, factories, validators, handlers, repositories, processors, OpenAPI transformer; docs and specs for architecture, PRD, run-summary, and usage notes.

Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels
enhancement, backend, testing

Suggested reviewers

  • Kravalg
  • Derane
  • cubic-dev-ai

"A rabbit hopped to guard your keys,
I stitched the flows with tiny ease.
Challenges bloom and credentials hide,
Tokens hop out, cookies abide.
Nose twitch—your sign-in now is breezy!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-221-passkey-auth

@qltysh

qltysh Bot commented May 10, 2026

Copy link
Copy Markdown

28 new issues

Tool Category Rule Count
qlty Duplication Found 17 lines of similar code in 2 locations (mass = 108) 16
qlty Structure Function with many parameters (count = 7): __construct 6
qlty Duplication Found 25 lines of identical code in 2 locations (mass = 120) 6

Comment thread src/User/Application/Passkey/PasskeyAuthenticationService.php Outdated
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyServiceTestObjects.php Outdated
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php Outdated
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php Outdated
vilnacrm-app Bot pushed a commit that referenced this pull request May 10, 2026
Signed-off-by: Vadym <kostiukdsfv@gmail.com>
cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes May 10, 2026

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 1 file (changes from recent commits).

Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php Outdated
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php Outdated
Comment thread tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php
Comment thread tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php Outdated
Comment thread tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php Outdated

@cubic-dev-ai cubic-dev-ai 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.

7 issues found across 93 files

Confidence score: 3/5

  • There is a concrete integration risk in .github/openapi-spec/spec.yaml: passkey request schemas are under-specified versus backend validation, which can break client generation and runtime requests due to a mismatched API contract.
  • Configuration issues in .env.load_test (variable expansion order) and .env.test (missing local test origin in passkey allowlist) can cause avoidable startup/test failures, so this is not fully low-risk yet.
  • Most remaining findings are lower impact (PHPDoc typing in src/User/Application/Processor/PasskeySignUpCompleteProcessor.php and src/User/Application/Processor/PasskeySignInCompleteProcessor.php, plus import-order style in src/User/Application/Passkey/PasskeyJsonCodec.php), though the TOCTOU note in src/User/Application/Passkey/PasskeyCredentialStore.php could still surface as duplicate-request errors under concurrency.
  • Pay close attention to .github/openapi-spec/spec.yaml, .env.load_test, .env.test, and src/User/Application/Passkey/PasskeyCredentialStore.php - contract drift, env misconfiguration, and concurrent passkey registration behavior are the main merge risks.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/User/Application/Processor/PasskeySignUpCompleteProcessor.php">

<violation number="1" location="src/User/Application/Processor/PasskeySignUpCompleteProcessor.php:35">
P3: PHPDoc type `array<string, scalar|array|null>` for `$context` is too narrow — `$context['request']` is a `Request` object. Use `array<string, mixed>` (as in `SignInProcessor`) to avoid misleading static analysis.</violation>
</file>

<file name="src/User/Application/Processor/PasskeySignInCompleteProcessor.php">

<violation number="1" location="src/User/Application/Processor/PasskeySignInCompleteProcessor.php:35">
P3: The PHPDoc type `array<string, scalar|array|null>` for `$context` is incorrect — `$context['request']` is a Symfony `Request` object. The existing `SignInProcessor` correctly uses `array<string,mixed>` for this parameter.</violation>
</file>

<file name="src/User/Application/Passkey/PasskeyJsonCodec.php">

<violation number="1" location="src/User/Application/Passkey/PasskeyJsonCodec.php:7">
P3: `use function` statement should appear after class imports per PSR-12 and the project's existing convention. Other files (e.g., `OAuthProviderCollectionFactory.php`) place `use function` after class `use` statements.</violation>
</file>

<file name="src/User/Application/Passkey/PasskeyCredentialStore.php">

<violation number="1" location="src/User/Application/Passkey/PasskeyCredentialStore.php:39">
P2: TOCTOU race condition: `existsByCredentialId` check and `save` are not atomic. A concurrent request could pass the same check before either saves. The MongoDB unique index prevents data corruption, but when the race occurs, the user gets an unhandled MongoDB duplicate-key exception instead of the intended `ConflictHttpException`. Consider wrapping the `save` in a try/catch for the duplicate-key exception and re-throwing as `ConflictHttpException`.</violation>
</file>

<file name=".env.load_test">

<violation number="1" location=".env.load_test:28">
P2: Don't reference `LOAD_TEST_API_PORT` before it is defined in this env file. Dotenv expands in file order, so this can resolve incorrectly during load-test startup.</violation>
</file>

<file name=".github/openapi-spec/spec.yaml">

<violation number="1" location=".github/openapi-spec/spec.yaml:2982">
P1: The new passkey request schemas are under-specified in OpenAPI (empty/missing required fields) compared to backend validation, causing a broken API contract for clients.</violation>
</file>

<file name=".env.test">

<violation number="1" location=".env.test:31">
P2: Include the test browser origin in the passkey allowlist; otherwise WebAuthn requests from the standard local test origin are rejected.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Browser Client
    participant API as API Platform Router
    participant Auth as Security / Auth Middleware
    participant Proc as Passkey Processor
    participant Srv as Passkey Application Service
    participant Codec as PasskeyJsonCodec
    participant WA as web-auth/webauthn-lib
    participant DB as MongoDB (passkey_* collections)
    participant UserDB as MongoDB (user collection)

    Note over Client,UserDB: NEW: Passkey Sign-Up Flow

    Client->>API: POST /api/passkeys/signup/options (email, initials, displayName)
    API->>Auth: PUBLIC_ACCESS check
    Auth-->>API: pass
    API->>Proc: PasskeySignUpOptionsProcessor::process()
    Proc->>Srv: PasskeyRegistrationService::startSignup()
    Srv->>UserDB: find by email (assert available)
    UserDB-->>Srv: null
    Srv->>Srv: PasskeyOptionsFactory::createSignupOptions()
    Srv->>Codec: encodeOptions(PublicKeyCredentialCreationOptions)
    Codec->>WA: serializer serialize
    WA-->>Codec: JSON string
    Codec-->>Srv: options JSON
    Srv->>DB: save PasskeyChallenge (signup, challenge, options, email, userId)
    DB-->>Srv: saved
    Srv-->>Proc: PasskeyOptionsResult
    Proc-->>API: { challenge_id, public_key }
    API-->>Client: 200 JSON

    Note over Client,UserDB: Client creates credential via navigator.credentials.create()

    Client->>API: POST /api/passkeys/signup/complete (challengeId, credential JSON, label, rememberMe)
    API->>Auth: PUBLIC_ACCESS check
    Auth-->>API: pass
    API->>Proc: PasskeySignUpCompleteProcessor::process()
    Proc->>Srv: PasskeyRegistrationService::completeSignup()
    Srv->>DB: claimActive(challengeId, signup, now) atomic update consumedAt
    alt atomic claim succeeds
        DB-->>Srv: PasskeyChallenge (consumed)
        Srv->>DB: find by email (assert still available)
        UserDB-->>Srv: null
        Srv->>Codec: decodeCredential(credential JSON)
        Codec->>WA: deserialize to PublicKeyCredential
        WA-->>Codec: PublicKeyCredential
        Codec-->>Srv: PublicKeyCredential
        Srv->>WA: AuthenticatorAttestationResponseValidator::check()
        WA-->>Srv: CredentialRecord (verified)
        Srv->>DB: save User (generated random password hash)
        UserDB-->>Srv: User
        Srv->>DB: save PasskeyCredential (userId, credentialId, record, label)
        DB-->>Srv: saved
        Srv->>DB: delete consumed challenge
        Srv->>Srv: PasskeySessionIssuer::issue() via IssuedSessionFactory
        DB->>DB: create AuthSession, AuthRefreshToken
        Srv-->>Proc: PasskeyAuthenticationResult (tokens)
        Proc->>Proc: set auth cookie
        Proc-->>API: { access_token, refresh_token }
        API-->>Client: 200 JSON + Set-Cookie
    else stale / consumed / expired challenge
        DB-->>Srv: null
        Srv-->>Proc: UnauthorizedHttpException
        Proc-->>API: 401
        API-->>Client: 401
    end

    Note over Client,UserDB: NEW: Authenticated Passkey Registration

    Client->>API: POST /api/passkeys/register/options ({})
    API->>Auth: ROLE_USER check
    Auth-->>API: pass
    API->>Proc: PasskeyRegistrationOptionsProcessor::process()
    Proc->>UserDB: get current user identity
    UserDB-->>Proc: userId, email
    Proc->>Srv: PasskeyRegistrationService::startRegistration()
    Srv->>UserDB: findById(userId)
    UserDB-->>Srv: User
    Srv->>DB: find by userId (existing credentials)
    DB-->>Srv: list of PasskeyCredential
    Srv->>Srv: create options with excludeCredentials
    Srv->>DB: save PasskeyChallenge (registration)
    DB-->>Srv: saved
    Srv-->>Proc: PasskeyOptionsResult
    Proc-->>API: { challenge_id, public_key }
    API-->>Client: 200 JSON

    Client->>API: POST /api/passkeys/register/complete (challengeId, credential JSON, label)
    API->>Auth: ROLE_USER check
    Auth-->>API: pass
    API->>Proc: PasskeyRegistrationCompleteProcessor::process()
    Proc->>Srv: completeRegistration()
    Srv->>DB: claimActive(challengeId, registration, now)
    alt valid challenge
        DB-->>Srv: PasskeyChallenge (consumed, belongs to current user)
        Srv->>WA: verify attestation
        WA-->>Srv: CredentialRecord
        Srv->>DB: check existsByCredentialId (prevents duplicates)
        alt unique
            DB-->>Srv: false
            Srv->>DB: save PasskeyCredential
            DB-->>Srv: saved
            Srv->>DB: delete challenge
            Srv-->>Proc: PasskeyCredential
            Proc-->>API: { credential_id }
            API-->>Client: 200 JSON
        else duplicate
            DB-->>Srv: true
            Srv-->>Proc: ConflictHttpException
            Proc-->>API: 409
            API-->>Client: 409
        end
    else invalid challenge
        DB-->>Srv: null
        Srv-->>Proc: 401
        Proc-->>API: 401
        API-->>Client: 401
    end

    Note over Client,UserDB: NEW: Passkey Sign-In Flow

    Client->>API: POST /api/passkeys/signin/options (email, rememberMe)
    API->>Auth: PUBLIC_ACCESS check
    Auth-->>API: pass
    API->>Proc: PasskeySignInOptionsProcessor::process()
    Proc->>Srv: PasskeyAuthenticationService::start()
    Srv->>UserDB: find by email
    alt user exists
        UserDB-->>Srv: User
        Srv->>DB: find by userId (credentials)
        DB-->>Srv: list of PasskeyCredential
    else unknown user
        UserDB-->>Srv: null
        Srv->>Srv: empty credential list (no user enumeration leak)
    end
    Srv->>Srv: create request options (allowCredentials from existing)
    Srv->>DB: save PasskeyChallenge (authentication, email, rememberMe, userId)
    DB-->>Srv: saved
    Srv-->>Proc: PasskeyOptionsResult
    Proc-->>API: { challenge_id, public_key }
    API-->>Client: 200 JSON

    Note over Client,UserDB: Client gets credential via navigator.credentials.get()

    Client->>API: POST /api/passkeys/signin/complete (challengeId, credential JSON)
    API->>Auth: PUBLIC_ACCESS check
    Auth-->>API: pass
    API->>Proc: PasskeySignInCompleteProcessor::process()
    Proc->>Srv: PasskeyAuthenticationService::complete()
    Srv->>DB: claimActive(challengeId, authentication, now)
    alt valid challenge
        DB-->>Srv: PasskeyChallenge (consumed, has userId)
        Srv->>Codec: extractCredentialId from credential JSON
        Srv->>DB: findByCredentialId(encodedId)
        alt credential found and owned by challenge userId
            DB-->>Srv: PasskeyCredential
            Srv->>WA: AuthenticatorAssertionResponseValidator::check()
            WA-->>Srv: CredentialRecord (verified)
            Srv->>DB: markUsed (update credential record and counter)
            DB-->>Srv: updated
            Srv->>UserDB: findById(userId)
            UserDB-->>Srv: User
            Srv->>DB: delete consumed challenge
            Srv->>Srv: issue session tokens
            DB->>DB: create AuthSession, AuthRefreshToken
            Srv-->>Proc: PasskeyAuthenticationResult
            Proc->>Proc: set auth cookie
            Proc-->>API: { access_token, refresh_token }
            API-->>Client: 200 JSON + Set-Cookie
        else credential mismatch / not found
            DB-->>Srv: null
            Srv-->>Proc: 401
            Proc-->>API: 401
            API-->>Client: 401
        end
    else stale/consumed challenge
        DB-->>Srv: null
        Srv-->>Proc: 401
        Proc-->>API: 401
        API-->>Client: 401
    end
Loading

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Comment thread .github/openapi-spec/spec.yaml
Comment thread src/User/Application/Resolver/PasskeyCredentialResolver.php
Comment thread .env.load_test
Comment thread .env.test Outdated
Comment thread src/User/Application/Processor/PasskeySignUpCompleteProcessor.php Outdated
Comment thread src/User/Application/Processor/PasskeySignInCompleteProcessor.php Outdated
Comment thread src/User/Application/Transformer/PasskeyJsonTransformer.php
@codecov

codecov Bot commented May 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (e5eb83e) to head (e9acebc).

Additional details and impacted files
@@              Coverage Diff              @@
##                main      #286     +/-   ##
=============================================
  Coverage     100.00%   100.00%             
- Complexity         0      3553   +3553     
=============================================
  Files            551       627     +76     
  Lines           9541     11782   +2241     
=============================================
+ Hits            9541     11782   +2241     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

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

☔ View full report in Codecov by Harness.
📢 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.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 10, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.env.load_test (1)

28-35: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move LOAD_TEST_API_PORT before PASSKEY_ALLOWED_ORIGINS to ensure the variable is defined before interpolation.

In Symfony Dotenv, variable expansion depends on definition order. Since PASSKEY_ALLOWED_ORIGINS on line 28 references ${LOAD_TEST_API_PORT}, which is not defined until line 35, the substitution will fail and leave the literal string ${LOAD_TEST_API_PORT} in the value, breaking WebAuthn origin validation during load tests.

Proposed patch
 AUTH_STANDARD_COOKIE_MAX_AGE=900
 AUTH_REMEMBER_ME_COOKIE_MAX_AGE=2592000
+LOAD_TEST_API_PORT=18081
 PASSKEY_RP_ID=localhost
 PASSKEY_RP_NAME="VilnaCRM User Service"
 PASSKEY_ALLOWED_ORIGINS="http://localhost,https://localhost,http://localhost:${LOAD_TEST_API_PORT}"
 PASSKEY_TIMEOUT_SECONDS=300
 PASSKEY_CHALLENGE_TTL_SECONDS=300
 REQUEST_BODY_MAX_SIZE_BYTES=65536
 CONFIRMATION_TOKEN_LENGTH=32
 PASSWORD_RESET_TOKEN_LENGTH=32
 PASSWORD_RESET_TOKEN_EXPIRATION_HOURS=1
-LOAD_TEST_API_PORT=18081
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.load_test around lines 28 - 35, PASSKEY_ALLOWED_ORIGINS references
${LOAD_TEST_API_PORT} but LOAD_TEST_API_PORT is defined after it, so move the
LOAD_TEST_API_PORT declaration above the PASSKEY_ALLOWED_ORIGINS line to ensure
Symfony Dotenv expands the variable; update the file so LOAD_TEST_API_PORT=18081
appears before
PASSKEY_ALLOWED_ORIGINS="http://localhost,https://localhost,http://localhost:${LOAD_TEST_API_PORT}"
and keep the rest of the variables unchanged.
♻️ Duplicate comments (1)
src/User/Application/Passkey/PasskeyEncoding.php (1)

7-15: ⚠️ Potential issue | 🟡 Minor

Fix import grouping (already flagged by linter).

The class import InvalidArgumentException at line 10 splits the function import group, violating PSR-12. Group all function imports together before class imports.

📦 Proposed fix for import ordering
 namespace App\User\Application\Passkey;
 
 use function base64_decode;
 use function base64_encode;
-
-use InvalidArgumentException;
-
 use function rtrim;
 use function str_repeat;
 use function strlen;
 use function strtr;
+
+use InvalidArgumentException;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/User/Application/Passkey/PasskeyEncoding.php` around lines 7 - 15, The
import order violates PSR-12 because the class InvalidArgumentException is
inserted between function imports; in PasskeyEncoding.php move
InvalidArgumentException so all function imports (base64_decode, base64_encode,
rtrim, str_repeat, strlen, strtr) are grouped together first and then add the
class import InvalidArgumentException after them, ensuring function use
statements are contiguous and class imports follow.
🟡 Minor comments (3)
specs/passkey-authentication/prd.md-92-106 (1)

92-106: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consolidate duplicate NFR sections to avoid ambiguity.

Line 92 (## Nonfunctional Requirements) and Line 100 (## Non-Functional Requirements) split one concept into two headings with inconsistent naming. Please merge them into a single NFR section so implementation and test traceability stays clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/passkey-authentication/prd.md` around lines 92 - 106, Merge the two
duplicate headings "## Nonfunctional Requirements" and "## Non-Functional
Requirements" into a single unified section titled consistently (e.g., "##
Non-Functional Requirements"), and consolidate all bullets from both headers
under that single heading so there is one authoritative NFR list; remove the
redundant header and ensure the combined content preserves all items (Domain
layer remains framework-free, YAML/XML config items, MongoDB TTL requirement,
configurable WebAuthn params, CI checks,
Security/Privacy/Compatibility/Observability/Maintainability bullets) and
consistent naming for traceability.
tests/Unit/User/Application/Passkey/PasskeyStoreTest.php-44-49 (1)

44-49: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace hardcoded test data with Faker.

This test uses hardcoded IDs ('user-id', 'credential-id', 'passkey-id') and other test values. As per coding guidelines, all test data must be generated using Faker, and hardcoded IDs are not permitted in tests.

🎲 Proposed fix using Faker
     public function testRegisterRejectsDuplicateCredentialId(): void
     {
+        $credentialId = $this->faker->uuid();
+        
         $this->credentialRepository->expects($this->once())
             ->method('existsByCredentialId')
-            ->with('credential-id')
+            ->with($credentialId)
             ->willReturn(true);
         $this->credentialRepository->expects($this->never())->method('save');

         $this->expectException(ConflictHttpException::class);
         $this->expectExceptionMessage('Passkey credential is already registered.');

         $this->createStore()->register(
-            'user-id',
-            new VerifiedPasskeyCredential('credential-id', '{"record":true}'),
-            'Laptop',
+            $this->faker->uuid(),
+            new VerifiedPasskeyCredential($credentialId, json_encode(['record' => true], JSON_THROW_ON_ERROR)),
+            $this->faker->words(2, true),
             new DateTimeImmutable()
         );
     }

Apply the same pattern to testFindByUserIdDelegatesToRepository().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Application/Passkey/PasskeyStoreTest.php` around lines 44 -
49, In testFindByUserIdDelegatesToRepository() replace hardcoded values used in
the createStore()->register(...) call (e.g. 'user-id', 'credential-id',
'Laptop', any 'passkey-id' usages and static DateTime values) with
Faker-generated data obtained from the test's faker instance (e.g.
$this->faker->uuid(), $this->faker->uuid(), $this->faker->word(),
$this->faker->dateTimeImmutable()) and use those variables when constructing the
VerifiedPasskeyCredential and calling register(), ensuring all test IDs and
strings come from Faker rather than being hardcoded.
tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php-16-23 (1)

16-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace hardcoded test data with Faker.

The test uses hardcoded IDs ('passkey-id', 'user-id', 'credential-id'), JSON strings, and labels. As per coding guidelines, tests must use Faker for all test data generation and never use hardcoded IDs or other test values.

🎲 Proposed fix using Faker
     public function testCredentialStoresMetadataAndSerializedRecord(): void
     {
         $createdAt = new DateTimeImmutable();
+        $passkeyId = $this->faker->uuid();
+        $userId = $this->faker->uuid();
+        $credentialId = $this->faker->uuid();
+        $credentialRecord = json_encode(['record' => true], JSON_THROW_ON_ERROR);
+        $label = $this->faker->words(2, true);
+        
         $credential = new PasskeyCredential(
-            'passkey-id',
-            'user-id',
-            'credential-id',
-            '{"record":true}',
-            'Work laptop',
+            $passkeyId,
+            $userId,
+            $credentialId,
+            $credentialRecord,
+            $label,
             $createdAt
         );

-        self::assertSame('passkey-id', $credential->getId());
-        self::assertSame('user-id', $credential->getUserId());
-        self::assertSame('credential-id', $credential->getCredentialId());
-        self::assertSame('{"record":true}', $credential->getCredentialRecord());
-        self::assertSame('Work laptop', $credential->getLabel());
+        self::assertSame($passkeyId, $credential->getId());
+        self::assertSame($userId, $credential->getUserId());
+        self::assertSame($credentialId, $credential->getCredentialId());
+        self::assertSame($credentialRecord, $credential->getCredentialRecord());
+        self::assertSame($label, $credential->getLabel());
         self::assertSame($createdAt, $credential->getCreatedAt());
         self::assertNull($credential->getLastUsedAt());
     }

Apply the same pattern to testMarkUsedUpdatesCredentialRecordAndLastUsedAt().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php` around lines 16 -
23, Replace hardcoded test values in the PasskeyCredential instantiation with
Faker-generated values: use $this->faker (e.g., ->uuid() for IDs) for
'passkey-id', 'user-id', and 'credential-id'; generate the JSON payload by
encoding an array built from Faker values (instead of the literal
'{"record":true}'); and replace the fixed label 'Work laptop' with a Faker
string (e.g., ->sentence() or ->word()). Apply this same Faker pattern inside
the test method testMarkUsedUpdatesCredentialRecordAndLastUsedAt() to ensure all
IDs, JSON payloads, and labels are generated by Faker rather than hardcoded.
🧹 Nitpick comments (10)
tests/Unit/User/Application/Passkey/PasskeyEncodingTest.php (1)

11-25: ⚡ Quick win

Add test coverage for unpadded base64url input and round-trip encoding.

The current tests only cover already-padded input and invalid input. Base64url typically omits padding (= characters), so the primary use case—decoding unpadded base64url values—is untested. Additionally, there are no tests for the encode() method or round-trip behavior.

🧪 Suggested additional test cases
public function testDecodeHandlesUnpaddedBase64UrlValue(): void
{
    // 'dGVzdA' is base64url for 'test' without padding (main use case)
    self::assertSame('test', (new PasskeyEncoding())->decode('dGVzdA'));
}

public function testEncodeProducesUrlSafeBase64WithoutPadding(): void
{
    $encoded = (new PasskeyEncoding())->encode('test');
    self::assertSame('dGVzdA', $encoded);
    self::assertStringNotContainsString('=', $encoded);
    self::assertStringNotContainsString('+', $encoded);
    self::assertStringNotContainsString('/', $encoded);
}

public function testRoundTripEncodingAndDecoding(): void
{
    $encoding = new PasskeyEncoding();
    $original = 'Hello, WebAuthn!';
    $encoded = $encoding->encode($original);
    $decoded = $encoding->decode($encoded);
    self::assertSame($original, $decoded);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Application/Passkey/PasskeyEncodingTest.php` around lines 11
- 25, Add tests to PasskeyEncodingTest to cover decoding unpadded base64url
input, encoding behavior, and round-trip consistency: add a test method (e.g.,
testDecodeHandlesUnpaddedBase64UrlValue) that asserts (new
PasskeyEncoding())->decode('dGVzdA') === 'test'; add a test (e.g.,
testEncodeProducesUrlSafeBase64WithoutPadding) that asserts (new
PasskeyEncoding())->encode('test') returns 'dGVzdA' and contains no '=', '+' or
'/'; and add a round-trip test (e.g., testRoundTripEncodingAndDecoding) that
encodes a string with PasskeyEncoding::encode and then decodes it with
PasskeyEncoding::decode and asserts the result equals the original.
tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php (1)

14-74: ⚡ Quick win

Prefer Faker for test data generation.

The test uses hardcoded values for emails ('person@example.com'), IDs ('challenge-id', 'user-id'), and other test data throughout. Using Faker would improve maintainability and eliminate magic values.

Example refactor using Faker
+use Faker\Factory;
+
 final class PasskeyChallengeTest extends UnitTestCase
 {
+    private function faker(): \Faker\Generator
+    {
+        return Factory::create();
+    }
+
     public function testChallengeStoresCeremonyContext(): void
     {
+        $faker = $this->faker();
         $createdAt = new DateTimeImmutable();
-        $expiresAt = $createdAt->modify('+5 minutes');
+        $ttlMinutes = 5;
+        $expiresAt = $createdAt->modify(sprintf('+%d minutes', $ttlMinutes));

Apply similar changes for email addresses, names, and IDs using $faker->email(), $faker->name(), $faker->uuid(), etc.

As per coding guidelines: "Always use Faker for all test data generation; never use hardcoded emails, passwords, tokens, or IDs in tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php` around lines 14 - 74,
Replace hardcoded test data in testChallengeStoresCeremonyContext and
createRememberedSignupChallenge with Faker-generated values: initialize a Faker
instance in the test (e.g., in setUp) and use $faker->uuid() for IDs,
$faker->email() for email, $faker->name() for display name, and appropriate
faker calls for initials and challenge/options; update
createRememberedSignupChallenge to accept or capture these generated values and
assert against them (use variables like $id, $email, $displayName, $userId when
constructing PasskeyChallenge and in the assertions), keeping the existing
methods and PasskeyChallenge/PasskeyChallengeContext usage intact.
tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php (1)

25-72: ⚡ Quick win

Prefer Faker for test data generation.

The test uses hardcoded emails ('new@example.com'), IDs ('challenge-id', '018f33bb-1111-7222-8333-111111111111'), and other literal values. Using Faker would eliminate these magic values and improve test maintainability.

As per coding guidelines: "Always use Faker for all test data generation; never use hardcoded emails, passwords, tokens, or IDs in tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php` around lines
25 - 72, The test uses hardcoded literals in createSignupChallenge() (like
'new@example.com', 'challenge-id', UUID string, etc.); replace those magic
values with Faker-generated data by instantiating or using an existing
Faker\Generator in the test and using it to generate the email, uuid, challenge
id/string and any other token values before constructing the PasskeyChallenge
and PasskeyChallengeContext; update createSignupChallenge() to accept or
reference Faker and pass the generated values into the PasskeyChallenge and
PasskeyChallengeContext constructors so all emails, IDs and tokens come from
Faker rather than hardcoded literals.
tests/Unit/User/Application/Passkey/PasskeyDtoTest.php (1)

21-108: ⚡ Quick win

Prefer Faker for test data generation.

The test methods use hardcoded emails, IDs, tokens, and other literal values throughout. Using Faker would eliminate magic values and improve maintainability.

As per coding guidelines: "Always use Faker for all test data generation; never use hardcoded emails, passwords, tokens, or IDs in tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Application/Passkey/PasskeyDtoTest.php` around lines 21 -
108, Tests in PasskeyDtoTest use hardcoded literal values; replace them with
Faker-generated data to comply with the guideline. Update
testSignUpOptionsExposeInputValues,
testSignUpCompleteExposesCredentialAndRememberMe,
testSignInOptionsExposeEmailAndRememberMe,
testCompleteDtosExposeCredentialValues, testResultDtosExposeValues and
createChallenge to use Faker for emails, IDs, tokens, labels, challenge strings
and option arrays (e.g., replace 'person@example.com', 'credential-id',
'challenge-id', 'access-token', 'refresh-token', 'Security key', options array
values, and challenge JSON) — instantiate Faker in setUp (or use existing
$this->faker) and use its methods (email, word, uuid, text) when constructing
PasskeySignUpOptionsDto, PasskeySignUpCompleteDto, PasskeySignInOptionsDto,
PasskeySignInCompleteDto, PasskeyRegistrationCompleteDto, PasskeyOptionsResult,
PasskeyAuthenticationResult, VerifiedPasskeyCredential and in createChallenge to
produce non-hardcoded created values.
src/User/Application/Passkey/PasskeyRegistrationService.php (1)

22-23: 💤 Low value

Inconsistent ID-generation abstraction.

PasskeyOptionsFactory (and the test support) uses App\User\Application\Factory\IdFactoryInterface to generate identifiers, while this service depends directly on Symfony's UuidFactory to seed userId. Routing user-id generation through the same IdFactoryInterface keeps the abstraction consistent and makes unit testing easier (single mock). Not a correctness issue — just dependency-direction hygiene.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/User/Application/Passkey/PasskeyRegistrationService.php` around lines 22
- 23, PasskeyRegistrationService currently injects Symfony's UuidFactory for
seeding userId while PasskeyOptionsFactory and tests use
App\User\Application\Factory\IdFactoryInterface; change the service to depend on
IdFactoryInterface instead of UuidFactory, update the constructor signature and
property (replace UuidFactory $uuidFactory with IdFactoryInterface $idFactory),
and use $idFactory->createId() (or the existing IdFactoryInterface method) to
generate the userId wherever $this->uuidFactory was used (keeping
PasskeyUserCreator and userCreator usage unchanged) so ID generation is routed
through the same abstraction for consistency and easier testing.
src/User/Application/Passkey/PasskeyCredentialVerifier.php (1)

47-88: ⚡ Quick win

DRY: extract the wrap-and-throw helper for attestation/assertion.

verifyAttestation and verifyAssertion share an identical try/catch shape (rethrow BadRequestHttpException, wrap any other Throwable as UnauthorizedHttpException('Bearer', 'Invalid passkey credential.', …)). Extracting that into a small private helper will reduce duplication and keep the error mapping in one place.

♻️ Proposed refactor
-    public function verifyAttestation(
-        PasskeyChallenge $challenge,
-        array $credential
-    ): VerifiedPasskeyCredential {
-        try {
-            return $this->verifiedCredentialFactory->create(
-                $this->attestationVerifier->verify($challenge, $credential)
-            );
-        } catch (BadRequestHttpException $exception) {
-            throw $exception;
-        } catch (Throwable $exception) {
-            throw new UnauthorizedHttpException(
-                'Bearer',
-                'Invalid passkey credential.',
-                $exception
-            );
-        }
-    }
+    public function verifyAttestation(
+        PasskeyChallenge $challenge,
+        array $credential
+    ): VerifiedPasskeyCredential {
+        return $this->wrapVerification(
+            fn () => $this->attestationVerifier->verify($challenge, $credential)
+        );
+    }
@@
-    public function verifyAssertion(
-        PasskeyChallenge $challenge,
-        array $credential,
-        PasskeyCredential $storedCredential
-    ): VerifiedPasskeyCredential {
-        try {
-            return $this->verifiedCredentialFactory->create(
-                $this->assertionVerifier->verify($challenge, $storedCredential, $credential)
-            );
-        } catch (BadRequestHttpException $exception) {
-            throw $exception;
-        } catch (Throwable $exception) {
-            throw new UnauthorizedHttpException(
-                'Bearer',
-                'Invalid passkey credential.',
-                $exception
-            );
-        }
-    }
+    public function verifyAssertion(
+        PasskeyChallenge $challenge,
+        array $credential,
+        PasskeyCredential $storedCredential
+    ): VerifiedPasskeyCredential {
+        return $this->wrapVerification(
+            fn () => $this->assertionVerifier->verify($challenge, $storedCredential, $credential)
+        );
+    }
+
+    private function wrapVerification(callable $verify): VerifiedPasskeyCredential
+    {
+        try {
+            return $this->verifiedCredentialFactory->create($verify());
+        } catch (BadRequestHttpException $exception) {
+            throw $exception;
+        } catch (Throwable $exception) {
+            throw new UnauthorizedHttpException(
+                'Bearer',
+                'Invalid passkey credential.',
+                $exception
+            );
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/User/Application/Passkey/PasskeyCredentialVerifier.php` around lines 47 -
88, verifyAttestation and verifyAssertion duplicate the same try/catch logic;
extract that error-wrapping into a private helper (e.g. wrapVerificationCall or
handleVerificationExceptions) that accepts a callable which executes the
verifier call and returns the VerifiedPasskeyCredential, rethrows
BadRequestHttpException, and wraps any other Throwable into new
UnauthorizedHttpException('Bearer','Invalid passkey credential.', $exception);
replace the try/catch blocks in verifyAttestation and verifyAssertion to call
this helper with the appropriate lambda that calls
$this->attestationVerifier->verify(...) and
$this->assertionVerifier->verify(...), then pass the verifier result into
$this->verifiedCredentialFactory->create(...) inside the callable or after the
helper returns.
src/User/Infrastructure/Repository/MongoDBPasskeyCredentialRepository.php (1)

50-54: 💤 Low value

Consider using a count query for existence check.

existsByCredentialId hydrates a full PasskeyCredential document just to check presence. For a hot path (every sign-in/sign-up complete), a projection or countDocuments(['credentialId' => $id], ['limit' => 1]) would avoid the unmarshalling cost. Not strictly required since the field is uniquely indexed and lookups are O(1), but it's a small win.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/User/Infrastructure/Repository/MongoDBPasskeyCredentialRepository.php`
around lines 50 - 54, The existsByCredentialId method currently calls
findByCredentialId and hydrates a full PasskeyCredential just to test presence;
change it to perform a lightweight existence check (e.g., use the MongoDB
collection's countDocuments/filter with ['credentialId' => $credentialId] and
limit 1 or use findOne with a projection that only returns _id) and return a
boolean based on count>0 or non-null result; update the method in
MongoDBPasskeyCredentialRepository (replace the call to findByCredentialId) so
the hot-path sign-in checks avoid unmarshalling the full PasskeyCredential.
tests/Unit/User/Application/Passkey/PasskeyCredentialVerifierTest.php (1)

54-341: 🏗️ Heavy lift

Replace hardcoded passkey test data with Faker-generated values.

The verifier tests rely heavily on fixed emails/IDs/tokens/challenges, which conflicts with the repository test-data rule.

As per coding guidelines tests/**/*.php: “Always use Faker for all test data generation; never use hardcoded emails, passwords, tokens, or IDs in tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Application/Passkey/PasskeyCredentialVerifierTest.php` around
lines 54 - 341, Tests use fixed literals (emails, user IDs, challenge strings,
hostnames, credential IDs) in helper methods like createCreationOptions,
createConfiguration, createChallenge, createStoredCredential,
createCredentialRecord and createPublicKeyCredential; replace those hardcoded
values with Faker-generated values by adding/using a Faker instance (e.g.
$this->faker or injecting Faker in the test setup) and call appropriate methods
(safeEmail/uuid/uuidV4/text/word) to produce the email, user-id, challenge
strings, host, credential ids, and device name so all helpers return dynamic,
non-hardcoded data while keeping the same structure and types expected by the
tests.
tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php (1)

69-137: ⚡ Quick win

Apply Faker in this test support helper as well.

This helper centralizes hardcoded test identities/metadata and propagates them into multiple tests. Converting these fixtures to Faker will keep policy compliance and reduce brittle coupling.

As per coding guidelines tests/**/*.php: “Always use Faker for all test data generation; never use hardcoded emails, passwords, tokens, or IDs in tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php`
around lines 69 - 137, Replace hardcoded literals in this test helper with
Faker-generated values: instantiate Faker in the helper (e.g., in the class
constructor or as a private $faker property) and use it to produce the passkey
id, raw-credential-id (encoded via PasskeyEncoding), challenge id, device name,
IP, browser/user-agent, access-token and refresh-token, and host/application
values used in createPasskeyCredential, assertRegistrationOptionsStarted,
completeSignup, assertSignupCompleted, createConfiguration and any expectations;
store those generated values as properties on the helper so assertion methods
(assertRegistrationOptionsStarted, assertSignupCompleted) compare against the
same generated values instead of fixed strings, and update
createOptionsFactory/createConfiguration to use faker-generated host/title/urls
and timeout values.
tests/Unit/User/Application/Passkey/PasskeyJsonCodecTest.php (1)

39-290: 🏗️ Heavy lift

Switch this test fixture setup to Faker-based values.

The suite uses multiple hardcoded emails/IDs/challenges/origins. Please generate these through Faker to align with repo testing policy.

As per coding guidelines tests/**/*.php: “Always use Faker for all test data generation; never use hardcoded emails, passwords, tokens, or IDs in tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Application/Passkey/PasskeyJsonCodecTest.php` around lines 39
- 290, Tests use hardcoded emails, IDs, challenges and origins; replace those
literals with Faker-generated values by adding a Faker\Generator instance (e.g.
$this->faker) in the test class setup and use it in methods that currently
return constants: createPasskeyCredential (id, user-id, name),
createClientDataJson (origin and challenge), createAssertionPayload (credential
id/rawId), createConfiguration (rpId and origin) and createRequestOptions
(challenge); update assertCreationOptionsRoundTrip to use the faker-generated
email when building registration options and ensure any encoding calls
(encoding->encode('...')) use the corresponding faker strings so all test
fixtures are created from Faker instead of hardcoded values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/openapi-spec/spec.yaml:
- Around line 2994-3002: The OpenAPI schema places the rememberMe boolean on
EmptyResponse.PasskeySignUpCompleteDto but it belongs on
EmptyResponse.PasskeySignUpOptionsDto; move the rememberMe property definition
(default: false, type: boolean) from EmptyResponse.PasskeySignUpCompleteDto into
EmptyResponse.PasskeySignUpOptionsDto and remove it from
PasskeySignUpCompleteDto so the /api/passkeys/signup/options contract and
generated SDKs match the intended request/response shape.

In `@config/api_platform/resources/EmptyResponse.yaml`:
- Around line 83-102: The OpenAPI config and processor leak email-existence by
returning 409 for passkey_signup_options_http; change the behavior so
/passkeys/signup/options (passkey_signup_options_http) does not return 409:
update the EmptyResponse.yaml responses (remove or replace the 409 entry with a
generic 200/202) and modify
App\User\Application\Processor\PasskeySignUpOptionsProcessor to stop performing
an email-uniqueness rejection there (defer duplicate-account checks to
/passkeys/signup/complete), ensuring the options endpoint always returns a
generic success payload and only the completion endpoint enforces/returns
duplicate-account errors.

In `@config/services.yaml`:
- Line 380: The current regex entry "{ pattern:
'#^/api/passkeys/(signup|signin)#', methods: ['POST'] }" is too broad and may
match future prefixed routes; replace it with explicit exact-path entries for
the two endpoints used for public POST access (e.g., exact-match patterns for
"/api/passkeys/signup" and "/api/passkeys/signin" or use anchored regexes that
include the end-of-string like "^/api/passkeys/signup$" and
"^/api/passkeys/signin$") so only those two POST routes are whitelisted; update
the service YAML entry that contains the pattern string to add two precise rules
instead of the single broad alternate-group pattern.

In `@deptrac.yaml`:
- Line 12: The Application collector regex in deptrac.yaml was widened by adding
"Passkey" to the Application type list (the regex string containing
Transformer|Command|...|Passkey), which weakens architecture enforcement; revert
the change by removing "Passkey" from that regex and instead move any new
Passkey-related classes into the appropriate existing Application subdirectories
(e.g., Processor/Resolver/Factory) or create a narrowly scoped collector with a
clear intent if truly needed, updating references to the Application collector
accordingly.

In `@src/User/Application/DTO/PasskeyRegistrationCompleteDto.php`:
- Around line 17-40: Change the DTO PasskeyRegistrationCompleteDto to expose its
data as public properties instead of private fields with getters: replace the
private constructor properties (challengeId, credential, label) with public
properties and remove the accessor methods challengeIdValue(),
credentialValue(), and labelValue(); then update any callers/processors that
used those methods to read the fields directly (e.g. $dto->challengeId,
$dto->credential, $dto->label) and ensure validation for these fields is
provided in config/validator/validation.yaml as per the application-layer DTO
guideline.

In `@src/User/Application/DTO/PasskeySignInCompleteDto.php`:
- Around line 17-34: Replace the private DTO properties and their accessor
methods with public properties so the Application DTO exposes data directly:
change the constructor signature to accept and assign public properties
$challengeId and $credential (remove private and the methods challengeIdValue()
and credentialValue()), then update any callers/processors to read
$dto->challengeId and $dto->credential instead of the getters; ensure validation
for these fields is moved to and configured in config/validator/validation.yaml
per the Application layer DTO guideline.

In `@src/User/Application/DTO/PasskeySignInOptionsDto.php`:
- Around line 10-37: Change the PasskeySignInOptionsDto to use public properties
per project DTO conventions: make $email and $rememberMe public (remove private
visibility), remove or stop using the emailValue(), isRememberMe() and
setRememberMe() accessor methods and instead rely on the public properties (or
update constructor to no longer encapsulate $email as a private parameter),
ensuring default values remain (email = '' and rememberMe = false) so YAML
validation can target the public properties; update any callers that used
emailValue(), isRememberMe() or setRememberMe() to access the public $email and
$rememberMe directly.

In `@src/User/Application/DTO/PasskeySignUpCompleteDto.php`:
- Around line 12-55: The DTO PasskeySignUpCompleteDto currently declares its
properties as private with accessor methods; change them to public properties so
the application layer DTO exposes fields directly and can be validated via YAML:
update the constructor property promotion from "private string $challengeId",
"private array $credential", "private string $label" and the "private bool
$rememberMe" field to public visibility, and remove or replace the accessor
methods (challengeIdValue, credentialValue, labelValue, isRememberMe) so callers
use $dto->challengeId, $dto->credential, $dto->label, $dto->rememberMe; if you
keep setRememberMe(), have it assign the public $rememberMe property instead of
a private one. Ensure the `@psalm-api/docblocks` on credential remain accurate.

In `@src/User/Application/DTO/PasskeySignUpOptionsDto.php`:
- Around line 15-35: Change the DTO to expose public properties instead of
private properties with accessors: replace the private constructor properties in
PasskeySignUpOptionsDto with public properties (email, initials, displayName)
and remove the emailValue(), initialsValue(), and displayNameValue() accessor
methods; then update any callers to read $dto->email, $dto->initials, and
$dto->displayName (and rely on YAML validation in
config/validator/validation.yaml) so the DTO conforms to the application-layer
guideline.

In `@src/User/Application/Passkey/PasskeyCredentialStore.php`:
- Around line 64-82: The pre-check using
credentialRepository->existsByCredentialId in PasskeyCredentialStore is not
atomic and can race; remove reliance on that separate check and instead handle
unique-index violations at save-time by catching the repository/DB unique
constraint exception thrown by credentialRepository->save (or underlying
persistence layer) and rethrowing a ConflictHttpException('Passkey credential is
already registered.'); ensure the new flow still constructs the
PasskeyCredential object and calls save, but wraps save in a try/catch that maps
the specific unique constraint exception to ConflictHttpException while letting
other exceptions bubble up.

In `@src/User/Application/Passkey/PasskeyVerifiedCredentialFactory.php`:
- Around line 1-28: Move the PasskeyVerifiedCredentialFactory class file into a
Factory directory and update its namespace and any references: relocate
PasskeyVerifiedCredentialFactory.php from the Passkey folder into a Factory
folder, change the class namespace from App\User\Application\Passkey to
App\User\Application\Factory, and update all callers/imports that reference
PasskeyVerifiedCredentialFactory to the new namespace; ensure the file name
remains PasskeyVerifiedCredentialFactory.php and that autoloading
(composer/PSR-4) still resolves the new namespace.

In `@src/User/Domain/Entity/PasskeyChallenge.php`:
- Around line 103-106: isExpired in PasskeyChallenge treats a challenge as valid
when $now equals $this->expiresAt; change the comparison in
PasskeyChallenge::isExpired to treat the boundary as expired (use >= rather than
>) so a challenge with $now === $expiresAt returns true (expired) to prevent the
acceptance window at the exact deadline.

In
`@tests/Unit/User/Application/Passkey/PasskeyAuthenticationServiceTestSupport.php`:
- Around line 55-63: In the PasskeyAuthenticationServiceTestSupport::complete
method replace the hardcoded test literals with Faker-generated values: use
$this->faker->uuid instead of 'challenge-id', $this->faker->ipv4 instead of
'203.0.113.10', and $this->faker->userAgent instead of 'Test Browser'; update
the createService()->complete call to pass those faker variables so tests follow
the repo convention of generating IDs, IPs and user agents dynamically.

In `@tests/Unit/User/Application/Passkey/PasskeyConfigurationTest.php`:
- Around line 16-96: Tests in PasskeyConfigurationTest use hardcoded RP
IDs/names/origins and dates; replace those literals with Faker-generated values
(seed the Faker generator for deterministic assertions) and use the Faker
strings when constructing PasskeyConfiguration and when asserting
getAllowedOrigins(), getRpId(), getRpName(), getTimeoutMilliseconds(), and
challengeExpiresAt(DateTimeImmutable). Instantiate a Faker\Generator at the
start of the test class (or in setUp), seed it (e.g., $faker->seed(1234)),
generate rpId, rpName, allowed origins (trim/normalize commas as current
production code expects) and a createdAt DateTimeImmutable via Faker, then use
those generated values in the constructors and assertions for the
PasskeyConfiguration methods (getAllowedOrigins, getRpId, getRpName,
getTimeoutMilliseconds, challengeExpiresAt) so tests follow the repository
policy.

In `@tests/Unit/User/Application/Passkey/PasskeyOptionsFactoryTest.php`:
- Around line 40-119: Replace hardcoded literals in the tests with
Faker-generated values: assign $this->faker->safeEmail(), $this->faker->name(),
$this->faker->lexify() or $this->faker->bothify()/->uuid() (for initials,
user-id, challenge ids, credential raw id, passkey id) to local variables at the
top of each test and use those variables when calling createSignupOptions,
createAuthenticationOptions, createRegistrationOptions and when building
credentials via createCredential and encoding->encode; ensure assertions compare
against the same local variables (e.g., assertSame($email,
$publicKey['user']['name']) and assertSame($challengeId,
$result->getChallenge()->getId())) so no hardcoded strings remain and
duplicate-literal S1192 warnings are avoided.

In `@tests/Unit/User/Application/Passkey/PasskeyServiceTestObjects.php`:
- Around line 23-114: The test helpers (methods createAuthenticationContext,
createCredential, createSignupChallenge, createIncompleteSignupChallenge,
createRegistrationChallenge, createUser) use hardcoded emails, IDs, tokens and
other identity-like values; replace those literals with values generated by
Faker (e.g., $faker->email, $faker->uuid, $faker->userName, $faker->word,
$faker->password) either by injecting a Faker\Generator into this test object or
constructing one in the class constructor, and use the generated values for
challenge ids, emails, user ids, display names, device names, raw credential ids
and passwords while only keeping fixed values when a test assertion explicitly
requires them. Ensure you update all occurrences in the listed methods so tests
no longer contain hardcoded identity data.

In `@tests/Unit/User/Application/Processor/PasskeyProcessorTest.php`:
- Around line 300-342: Replace hardcoded test data in createOptionsResult(),
createIdentityResolver(), and createCredential() with Faker-generated values:
instantiate a Faker\Generator (or use $this->faker if available) and generate
email, uuids, challenge id, credential id, rpId, and human-readable name; use
the generated uuid string with UuidTransformer->transformFromString(...) for the
user id and use Faker data for JSON fields (e.g., json_encode or Faker->json)
and timestamps (DateTimeImmutable from now) so createOptionsResult(),
createIdentityResolver(), and createCredential() consume only Faker data instead
of fixed literals.
- Around line 67-83: Replace hardcoded test values in
testSignUpOptionsProcessorReturnsChallengeOptions with Faker-generated data:
create a fake email, country code and full name (use the same Faker instance
used elsewhere in tests or add one to this test class) and use those variables
in the registrationService expectation (.with(...)), when constructing the
PasskeySignUpOptionsDto, and when asserting behavior; ensure the helper method
createOptionsResult() still matches the generated values or is fed the generated
inputs so the mocked startSignup call and PasskeySignUpOptionsProcessor.process
remain consistent.

In `@tests/Unit/User/Infrastructure/Repository/MongoDBPasskeyRepositoryTest.php`:
- Around line 190-215: Replace hardcoded literals in createChallenge() and
createCredential() with Faker-generated values: use Faker to produce a realistic
email for PasskeyChallengeContext, unique IDs for 'challenge-id', 'user-id', and
'credential-id', a random challenge/token string for the challenge field, and a
device/name for the PasskeyCredential's device label; maintain the same
constructors (PasskeyChallenge, PasskeyChallengeContext, PasskeyCredential) and
DateTimeImmutable usage but pass Faker values instead of fixed strings so tests
no longer rely on hardcoded data.

---

Outside diff comments:
In @.env.load_test:
- Around line 28-35: PASSKEY_ALLOWED_ORIGINS references ${LOAD_TEST_API_PORT}
but LOAD_TEST_API_PORT is defined after it, so move the LOAD_TEST_API_PORT
declaration above the PASSKEY_ALLOWED_ORIGINS line to ensure Symfony Dotenv
expands the variable; update the file so LOAD_TEST_API_PORT=18081 appears before
PASSKEY_ALLOWED_ORIGINS="http://localhost,https://localhost,http://localhost:${LOAD_TEST_API_PORT}"
and keep the rest of the variables unchanged.

---

Minor comments:
In `@specs/passkey-authentication/prd.md`:
- Around line 92-106: Merge the two duplicate headings "## Nonfunctional
Requirements" and "## Non-Functional Requirements" into a single unified section
titled consistently (e.g., "## Non-Functional Requirements"), and consolidate
all bullets from both headers under that single heading so there is one
authoritative NFR list; remove the redundant header and ensure the combined
content preserves all items (Domain layer remains framework-free, YAML/XML
config items, MongoDB TTL requirement, configurable WebAuthn params, CI checks,
Security/Privacy/Compatibility/Observability/Maintainability bullets) and
consistent naming for traceability.

In `@tests/Unit/User/Application/Passkey/PasskeyStoreTest.php`:
- Around line 44-49: In testFindByUserIdDelegatesToRepository() replace
hardcoded values used in the createStore()->register(...) call (e.g. 'user-id',
'credential-id', 'Laptop', any 'passkey-id' usages and static DateTime values)
with Faker-generated data obtained from the test's faker instance (e.g.
$this->faker->uuid(), $this->faker->uuid(), $this->faker->word(),
$this->faker->dateTimeImmutable()) and use those variables when constructing the
VerifiedPasskeyCredential and calling register(), ensuring all test IDs and
strings come from Faker rather than being hardcoded.

In `@tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php`:
- Around line 16-23: Replace hardcoded test values in the PasskeyCredential
instantiation with Faker-generated values: use $this->faker (e.g., ->uuid() for
IDs) for 'passkey-id', 'user-id', and 'credential-id'; generate the JSON payload
by encoding an array built from Faker values (instead of the literal
'{"record":true}'); and replace the fixed label 'Work laptop' with a Faker
string (e.g., ->sentence() or ->word()). Apply this same Faker pattern inside
the test method testMarkUsedUpdatesCredentialRecordAndLastUsedAt() to ensure all
IDs, JSON payloads, and labels are generated by Faker rather than hardcoded.

---

Duplicate comments:
In `@src/User/Application/Passkey/PasskeyEncoding.php`:
- Around line 7-15: The import order violates PSR-12 because the class
InvalidArgumentException is inserted between function imports; in
PasskeyEncoding.php move InvalidArgumentException so all function imports
(base64_decode, base64_encode, rtrim, str_repeat, strlen, strtr) are grouped
together first and then add the class import InvalidArgumentException after
them, ensuring function use statements are contiguous and class imports follow.

---

Nitpick comments:
In `@src/User/Application/Passkey/PasskeyCredentialVerifier.php`:
- Around line 47-88: verifyAttestation and verifyAssertion duplicate the same
try/catch logic; extract that error-wrapping into a private helper (e.g.
wrapVerificationCall or handleVerificationExceptions) that accepts a callable
which executes the verifier call and returns the VerifiedPasskeyCredential,
rethrows BadRequestHttpException, and wraps any other Throwable into new
UnauthorizedHttpException('Bearer','Invalid passkey credential.', $exception);
replace the try/catch blocks in verifyAttestation and verifyAssertion to call
this helper with the appropriate lambda that calls
$this->attestationVerifier->verify(...) and
$this->assertionVerifier->verify(...), then pass the verifier result into
$this->verifiedCredentialFactory->create(...) inside the callable or after the
helper returns.

In `@src/User/Application/Passkey/PasskeyRegistrationService.php`:
- Around line 22-23: PasskeyRegistrationService currently injects Symfony's
UuidFactory for seeding userId while PasskeyOptionsFactory and tests use
App\User\Application\Factory\IdFactoryInterface; change the service to depend on
IdFactoryInterface instead of UuidFactory, update the constructor signature and
property (replace UuidFactory $uuidFactory with IdFactoryInterface $idFactory),
and use $idFactory->createId() (or the existing IdFactoryInterface method) to
generate the userId wherever $this->uuidFactory was used (keeping
PasskeyUserCreator and userCreator usage unchanged) so ID generation is routed
through the same abstraction for consistency and easier testing.

In `@src/User/Infrastructure/Repository/MongoDBPasskeyCredentialRepository.php`:
- Around line 50-54: The existsByCredentialId method currently calls
findByCredentialId and hydrates a full PasskeyCredential just to test presence;
change it to perform a lightweight existence check (e.g., use the MongoDB
collection's countDocuments/filter with ['credentialId' => $credentialId] and
limit 1 or use findOne with a projection that only returns _id) and return a
boolean based on count>0 or non-null result; update the method in
MongoDBPasskeyCredentialRepository (replace the call to findByCredentialId) so
the hot-path sign-in checks avoid unmarshalling the full PasskeyCredential.

In `@tests/Unit/User/Application/Passkey/PasskeyCredentialVerifierTest.php`:
- Around line 54-341: Tests use fixed literals (emails, user IDs, challenge
strings, hostnames, credential IDs) in helper methods like
createCreationOptions, createConfiguration, createChallenge,
createStoredCredential, createCredentialRecord and createPublicKeyCredential;
replace those hardcoded values with Faker-generated values by adding/using a
Faker instance (e.g. $this->faker or injecting Faker in the test setup) and call
appropriate methods (safeEmail/uuid/uuidV4/text/word) to produce the email,
user-id, challenge strings, host, credential ids, and device name so all helpers
return dynamic, non-hardcoded data while keeping the same structure and types
expected by the tests.

In `@tests/Unit/User/Application/Passkey/PasskeyDtoTest.php`:
- Around line 21-108: Tests in PasskeyDtoTest use hardcoded literal values;
replace them with Faker-generated data to comply with the guideline. Update
testSignUpOptionsExposeInputValues,
testSignUpCompleteExposesCredentialAndRememberMe,
testSignInOptionsExposeEmailAndRememberMe,
testCompleteDtosExposeCredentialValues, testResultDtosExposeValues and
createChallenge to use Faker for emails, IDs, tokens, labels, challenge strings
and option arrays (e.g., replace 'person@example.com', 'credential-id',
'challenge-id', 'access-token', 'refresh-token', 'Security key', options array
values, and challenge JSON) — instantiate Faker in setUp (or use existing
$this->faker) and use its methods (email, word, uuid, text) when constructing
PasskeySignUpOptionsDto, PasskeySignUpCompleteDto, PasskeySignInOptionsDto,
PasskeySignInCompleteDto, PasskeyRegistrationCompleteDto, PasskeyOptionsResult,
PasskeyAuthenticationResult, VerifiedPasskeyCredential and in createChallenge to
produce non-hardcoded created values.

In `@tests/Unit/User/Application/Passkey/PasskeyEncodingTest.php`:
- Around line 11-25: Add tests to PasskeyEncodingTest to cover decoding unpadded
base64url input, encoding behavior, and round-trip consistency: add a test
method (e.g., testDecodeHandlesUnpaddedBase64UrlValue) that asserts (new
PasskeyEncoding())->decode('dGVzdA') === 'test'; add a test (e.g.,
testEncodeProducesUrlSafeBase64WithoutPadding) that asserts (new
PasskeyEncoding())->encode('test') returns 'dGVzdA' and contains no '=', '+' or
'/'; and add a round-trip test (e.g., testRoundTripEncodingAndDecoding) that
encodes a string with PasskeyEncoding::encode and then decodes it with
PasskeyEncoding::decode and asserts the result equals the original.

In `@tests/Unit/User/Application/Passkey/PasskeyJsonCodecTest.php`:
- Around line 39-290: Tests use hardcoded emails, IDs, challenges and origins;
replace those literals with Faker-generated values by adding a Faker\Generator
instance (e.g. $this->faker) in the test class setup and use it in methods that
currently return constants: createPasskeyCredential (id, user-id, name),
createClientDataJson (origin and challenge), createAssertionPayload (credential
id/rawId), createConfiguration (rpId and origin) and createRequestOptions
(challenge); update assertCreationOptionsRoundTrip to use the faker-generated
email when building registration options and ensure any encoding calls
(encoding->encode('...')) use the corresponding faker strings so all test
fixtures are created from Faker instead of hardcoded values.

In
`@tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php`:
- Around line 69-137: Replace hardcoded literals in this test helper with
Faker-generated values: instantiate Faker in the helper (e.g., in the class
constructor or as a private $faker property) and use it to produce the passkey
id, raw-credential-id (encoded via PasskeyEncoding), challenge id, device name,
IP, browser/user-agent, access-token and refresh-token, and host/application
values used in createPasskeyCredential, assertRegistrationOptionsStarted,
completeSignup, assertSignupCompleted, createConfiguration and any expectations;
store those generated values as properties on the helper so assertion methods
(assertRegistrationOptionsStarted, assertSignupCompleted) compare against the
same generated values instead of fixed strings, and update
createOptionsFactory/createConfiguration to use faker-generated host/title/urls
and timeout values.

In `@tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php`:
- Around line 25-72: The test uses hardcoded literals in createSignupChallenge()
(like 'new@example.com', 'challenge-id', UUID string, etc.); replace those magic
values with Faker-generated data by instantiating or using an existing
Faker\Generator in the test and using it to generate the email, uuid, challenge
id/string and any other token values before constructing the PasskeyChallenge
and PasskeyChallengeContext; update createSignupChallenge() to accept or
reference Faker and pass the generated values into the PasskeyChallenge and
PasskeyChallengeContext constructors so all emails, IDs and tokens come from
Faker rather than hardcoded literals.

In `@tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php`:
- Around line 14-74: Replace hardcoded test data in
testChallengeStoresCeremonyContext and createRememberedSignupChallenge with
Faker-generated values: initialize a Faker instance in the test (e.g., in setUp)
and use $faker->uuid() for IDs, $faker->email() for email, $faker->name() for
display name, and appropriate faker calls for initials and challenge/options;
update createRememberedSignupChallenge to accept or capture these generated
values and assert against them (use variables like $id, $email, $displayName,
$userId when constructing PasskeyChallenge and in the assertions), keeping the
existing methods and PasskeyChallenge/PasskeyChallengeContext usage intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b28f2f51-78cd-4300-a3ad-b1102281c498

📥 Commits

Reviewing files that changed from the base of the PR and between 202ec50 and 38aaa15.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (92)
  • .env
  • .env.load_test
  • .env.test
  • .github/openapi-spec/spec.yaml
  • composer.json
  • config/api_platform/resources/EmptyResponse.yaml
  • config/doctrine/User/PasskeyChallenge.mongodb.xml
  • config/doctrine/User/PasskeyCredential.mongodb.xml
  • config/packages/security.yaml
  • config/services.yaml
  • config/services_test.yaml
  • config/validator/validation.yaml
  • deptrac.yaml
  • docs/advanced-configuration.md
  • docs/main.md
  • docs/passkey-authentication.md
  • specs/passkey-authentication/architecture.md
  • specs/passkey-authentication/epics.md
  • specs/passkey-authentication/implementation-readiness.md
  • specs/passkey-authentication/prd.md
  • specs/passkey-authentication/product-brief.md
  • specs/passkey-authentication/research.md
  • specs/passkey-authentication/run-summary.md
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolver.php
  • src/User/Application/DTO/PasskeyAuthenticationResult.php
  • src/User/Application/DTO/PasskeyOptionsResult.php
  • src/User/Application/DTO/PasskeyRegistrationCompleteDto.php
  • src/User/Application/DTO/PasskeyRegistrationOptionsDto.php
  • src/User/Application/DTO/PasskeySignInCompleteDto.php
  • src/User/Application/DTO/PasskeySignInOptionsDto.php
  • src/User/Application/DTO/PasskeySignUpCompleteDto.php
  • src/User/Application/DTO/PasskeySignUpOptionsDto.php
  • src/User/Application/DTO/VerifiedPasskeyCredential.php
  • src/User/Application/Passkey/PasskeyAssertionCredentialRecordVerifier.php
  • src/User/Application/Passkey/PasskeyAttestationCredentialRecordVerifier.php
  • src/User/Application/Passkey/PasskeyAuthenticationService.php
  • src/User/Application/Passkey/PasskeyAuthenticationServiceInterface.php
  • src/User/Application/Passkey/PasskeyChallengeStore.php
  • src/User/Application/Passkey/PasskeyConfiguration.php
  • src/User/Application/Passkey/PasskeyCredentialResponseResolver.php
  • src/User/Application/Passkey/PasskeyCredentialStore.php
  • src/User/Application/Passkey/PasskeyCredentialVerifier.php
  • src/User/Application/Passkey/PasskeyCredentialVerifierInterface.php
  • src/User/Application/Passkey/PasskeyEncoding.php
  • src/User/Application/Passkey/PasskeyJsonCodec.php
  • src/User/Application/Passkey/PasskeyJsonCodecInterface.php
  • src/User/Application/Passkey/PasskeyOptionsFactory.php
  • src/User/Application/Passkey/PasskeyPublicKeyOptionsFactory.php
  • src/User/Application/Passkey/PasskeyRegistrationService.php
  • src/User/Application/Passkey/PasskeyRegistrationServiceInterface.php
  • src/User/Application/Passkey/PasskeyResponseFactory.php
  • src/User/Application/Passkey/PasskeySessionIssuer.php
  • src/User/Application/Passkey/PasskeyUserCreator.php
  • src/User/Application/Passkey/PasskeyUserResolver.php
  • src/User/Application/Passkey/PasskeyVerifiedCredentialFactory.php
  • src/User/Application/Passkey/PasskeyWebauthnFactory.php
  • src/User/Application/Passkey/PasskeyWebauthnFactoryInterface.php
  • src/User/Application/Processor/PasskeyRegistrationCompleteProcessor.php
  • src/User/Application/Processor/PasskeyRegistrationOptionsProcessor.php
  • src/User/Application/Processor/PasskeySignInCompleteProcessor.php
  • src/User/Application/Processor/PasskeySignInOptionsProcessor.php
  • src/User/Application/Processor/PasskeySignUpCompleteProcessor.php
  • src/User/Application/Processor/PasskeySignUpOptionsProcessor.php
  • src/User/Domain/Entity/PasskeyChallenge.php
  • src/User/Domain/Entity/PasskeyCredential.php
  • src/User/Domain/Repository/PasskeyChallengeRepositoryInterface.php
  • src/User/Domain/Repository/PasskeyCredentialRepositoryInterface.php
  • src/User/Domain/ValueObject/PasskeyChallengeContext.php
  • src/User/Infrastructure/Repository/MongoDBPasskeyChallengeRepository.php
  • src/User/Infrastructure/Repository/MongoDBPasskeyCredentialRepository.php
  • tests/Behat/Support/TestClientIpHttpRequestContextResolver.php
  • tests/Behat/UserContext/UserRequestContext.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolverTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolverLimitersTest.php
  • tests/Unit/User/Application/Passkey/PasskeyAuthenticationServiceTest.php
  • tests/Unit/User/Application/Passkey/PasskeyAuthenticationServiceTestSupport.php
  • tests/Unit/User/Application/Passkey/PasskeyConfigurationTest.php
  • tests/Unit/User/Application/Passkey/PasskeyCredentialVerifierTest.php
  • tests/Unit/User/Application/Passkey/PasskeyDtoTest.php
  • tests/Unit/User/Application/Passkey/PasskeyEncodingTest.php
  • tests/Unit/User/Application/Passkey/PasskeyJsonCodecTest.php
  • tests/Unit/User/Application/Passkey/PasskeyOptionsFactoryTest.php
  • tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTest.php
  • tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php
  • tests/Unit/User/Application/Passkey/PasskeyServiceTestObjects.php
  • tests/Unit/User/Application/Passkey/PasskeyStoreTest.php
  • tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php
  • tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
  • tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php
  • tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php
  • tests/Unit/User/Infrastructure/Repository/MongoDBPasskeyRepositoryTest.php

Comment thread .github/openapi-spec/spec.yaml
Comment thread config/api_platform/resources/EmptyResponse.yaml
Comment thread config/services.yaml Outdated
Comment thread deptrac.yaml Outdated
Comment thread src/User/Application/DTO/PasskeyRegistrationCompleteDto.php Outdated
Comment thread tests/Unit/User/Application/Factory/PasskeyOptionsFactoryTest.php
Comment thread tests/Unit/User/Application/Passkey/PasskeyServiceTestObjects.php Outdated
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
Comment thread tests/Unit/User/Infrastructure/Repository/MongoDBPasskeyRepositoryTest.php Outdated
dmytrocraft and others added 2 commits May 10, 2026 20:15
Comment thread tests/Unit/User/Application/Passkey/PasskeyConfigurationTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyConfigurationTest.php Outdated
Comment thread tests/Unit/User/Application/Transformer/PasskeyJsonTransformerTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php Outdated
Comment thread tests/Unit/User/Application/Factory/PasskeyUserFactoryTest.php
Comment thread tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php Outdated
Comment thread tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php Outdated
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/Transformer/PasskeyEncodingTransformer.php
Comment thread src/User/Application/Transformer/PasskeyEncodingTransformer.php
Comment thread src/User/Application/Transformer/PasskeyJsonTransformer.php
Comment thread tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php Outdated
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php Outdated
Comment thread tests/Unit/User/Application/Processor/PasskeyProcessorTest.php

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 42 files (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 10, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

♻️ Duplicate comments (1)
.github/openapi-spec/spec.yaml (1)

3041-3042: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

rememberMe placement is inconsistent between sign-in and sign-up flows.

The OpenAPI schema places rememberMe on different DTOs depending on the flow:

  • Sign-in flow: rememberMe is on PasskeySignInOptionsDto (options request)
  • Sign-up flow: rememberMe is on PasskeySignUpCompleteDto (completion request)

This inconsistency forces API consumers to remember different patterns for each flow. For consistency and developer experience, both flows should follow the same pattern.

The previous review suggested moving rememberMe from PasskeySignUpCompleteDto to PasskeySignUpOptionsDto to align with the sign-in pattern. However, verify with the team which pattern is preferred before making the change, as moving it would also require updating the backend processor and tests.

Also applies to: 3068-3069

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/openapi-spec/spec.yaml around lines 3041 - 3042, The OpenAPI spec is
inconsistent: rememberMe is on PasskeySignInOptionsDto but on
PasskeySignUpCompleteDto for sign-up; to fix, decide with the team to
standardize on one pattern (recommended: options DTO like
PasskeySignUpOptionsDto to mirror PasskeySignInOptionsDto), then move the
rememberMe property from PasskeySignUpCompleteDto to PasskeySignUpOptionsDto in
the spec, and update all related references; also update the backend
handler/processor that consumes sign-up requests (the sign-up completion flow),
plus any affected tests and fixtures to accept rememberMe on the sign-up options
DTO (or conversely, if the team prefers completion DTO, move it from
PasskeySignInOptionsDto to PasskeySignUpCompleteDto and update backend/tests
accordingly).
🧹 Nitpick comments (5)
tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTest.php (1)

284-292: 💤 Low value

Prefix unused callback parameters with underscore.

The callback parameters $id and $purpose match the claimActive signature but aren't used in the callback body. Prefix them with underscores to indicate they're intentionally unused and silence the static analysis warning.

Proposed fix
 ->willReturnCallback(static function (
-    string $id,
-    string $purpose,
+    string $_id,
+    string $_purpose,
     DateTimeImmutable $consumedAt
 ) use ($challenge): PasskeyChallenge {
     $challenge->consume($consumedAt);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTest.php`
around lines 284 - 292, In PasskeyRegistrationServiceTest update the anonymous
callback used in the willReturnCallback to mark the unused parameters as
intentionally unused by prefixing $id and $purpose with underscores (e.g. change
parameters in the static function signature to $_id and $_purpose while keeping
DateTimeImmutable $consumedAt unchanged); this will silence static analysis
warnings while preserving the callback behavior in the claimActive mock setup
that consumes $consumedAt and returns $challenge.
.env (1)

84-84: ⚡ Quick win

Remove unnecessary quotes from PASSKEY_ALLOWED_ORIGINS.

The value contains no spaces, so quotes are unnecessary and may cause environment parsers to include them literally in the value, breaking origin validation. Line 83 correctly uses quotes because the value contains a space.

🔧 Proposed fix
-PASSKEY_ALLOWED_ORIGINS="http://localhost,https://localhost,http://localhost:8080"
+PASSKEY_ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:8080
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env at line 84, The PASSKEY_ALLOWED_ORIGINS environment variable is wrapped
in unnecessary quotes which may be read literally by env parsers and break
origin validation; update the .env entry for PASSKEY_ALLOWED_ORIGINS by removing
the surrounding double quotes so the value is unquoted
(PASSKEY_ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:8080),
ensuring it matches how other no-space variables are defined.
.env.test (1)

31-31: ⚡ Quick win

Remove unnecessary quotes from PASSKEY_ALLOWED_ORIGINS.

The value contains no spaces, so quotes are unnecessary and may cause the parsed value to include literal quotes, breaking origin validation.

🔧 Proposed fix
-PASSKEY_ALLOWED_ORIGINS="http://localhost,https://localhost,http://localhost:8080,http://localhost:8081"
+PASSKEY_ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:8080,http://localhost:8081
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.test at line 31, The PASSKEY_ALLOWED_ORIGINS value in the .env.test
file is wrapped in double quotes which can lead to literal quotes being included
when parsed and break origin validation; update the PASSKEY_ALLOWED_ORIGINS
entry by removing the surrounding quotes so the value is an unquoted
comma-separated list (refer to the PASSKEY_ALLOWED_ORIGINS variable to locate
the line) and ensure no spaces or extra characters remain.
.env.load_test (1)

29-29: ⚡ Quick win

Remove unnecessary quotes from PASSKEY_ALLOWED_ORIGINS.

The value contains no spaces, so quotes are unnecessary and may cause the parsed value to include literal quotes, breaking origin validation.

🔧 Proposed fix
-PASSKEY_ALLOWED_ORIGINS="http://localhost,https://localhost,http://localhost:${LOAD_TEST_API_PORT}"
+PASSKEY_ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:${LOAD_TEST_API_PORT}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.load_test at line 29, The PASSKEY_ALLOWED_ORIGINS value is wrapped in
quotes which can become literal characters when loaded; remove the surrounding
double quotes so the environment entry for PASSKEY_ALLOWED_ORIGINS is an
unquoted comma-separated list (e.g.
PASSKEY_ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:${LOAD_TEST_API_PORT}),
ensure there are no extra spaces, save the change and re-run the origin
validation to confirm it parses correctly.
specs/passkey-authentication/product-brief.md (1)

21-21: 💤 Low value

Consider using "Non-Goals" with hyphen for consistency with product brief conventions.

Most product briefs use "Non-Goals" with a hyphen. While "Non Goals" is not incorrect, the hyphenated form is more standard in technical product documentation.

📝 Suggested style fix
-## Non Goals
+## Non-Goals
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/passkey-authentication/product-brief.md` at line 21, Replace the header
text "Non Goals" with the hyphenated form "Non-Goals" to match product brief
conventions and maintain consistency; locate the heading "Non Goals" in the
document (the section header) and update it to "Non-Goals".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@specs/passkey-authentication/epics.md`:
- Around line 13-14: The acronym "BMALPH" is inconsistent with "BMAD" in the
same story; update the text so both occurrences use the same acronym (replace
"BMALPH" with "BMAD" in the two bullet lines) and run a quick grep/scan for any
other "BMALPH" occurrences to ensure all references (e.g., the lines containing
"BMALPH doctor passes." and "BMALPH implementation transition succeeds.") are
aligned to "BMAD".

In `@tests/Unit/User/Application/Passkey/PasskeyAuthenticationServiceTest.php`:
- Around line 64-398: This test file uses many hardcoded literals (emails,
UUIDs, challenge/session IDs, IPs, browser strings) in methods like
testStartUsesExistingUserCredentialsWhenUserExists,
testStartCreatesOptionsWithoutCredentialDescriptorsForUnknownEmail,
testCompleteVerifiesCredentialUpdatesRecordAndIssuesSession and helper methods
expectExistingUserCredentials, expectAuthenticationOptionsChallenge,
expectAuthenticationCompleted, expectSessionIssue, etc.; replace those hardcoded
values with Faker-generated values (e.g. $this->faker->email,
$this->faker->uuid, $this->faker->ipv4, $this->faker->userAgent or custom
string) when creating User, PasskeyChallenge, PasskeyCredential, challenge IDs
and session IDs and when setting expectations on mocks (idFactory->create,
sessionFactory->create, repositories and publishers) so tests consume dynamic
fake data while keeping assertions that compare against the generated variables
(store faker outputs in local variables and use them in both setup and
assertions).

In `@tests/Unit/User/Application/Passkey/PasskeyDtoTest.php`:
- Around line 113-116: The test contains an unrelated assertInstanceOf for
PasskeyRegistrationOptionsDto that doesn't exercise the optionsResult under
test; remove the redundant assertion (the new PasskeyRegistrationOptionsDto()
check) or replace it with an assertion that examines the actual variable under
test (e.g. assertInstanceOf(PasskeyRegistrationOptionsDto::class,
$optionsResult) or assertions against its properties); update the test method
that references $optionsResult so it verifies the expected behavior/state
instead of instantiating a fresh DTO.

In `@tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTest.php`:
- Line 86: The tests in PasskeyRegistrationServiceTest use hardcoded emails and
IDs (e.g., the startSignup call with 'new@example.com' and hardcoded UUIDs like
'018f33bb-1111-7222-8333-111111111111'); replace these literals with
Faker-generated values (use $this->faker->safeEmail(), $this->faker->uuid(), or
Str::uuid()->toString() where appropriate), assign them to variables at the top
of each test (e.g., $email, $userId, $ownerId) and reuse those variables in
setup, the call to startSignup and all assertions/mocks (look for methods
startSignup, finishSignup, and any occurrences of 'person@example.com',
'user-id', 'owner-id', 'missing-user-id' in this test class) so every test uses
consistent, generated test data instead of hardcoded strings.

In `@tests/Unit/User/Application/Passkey/PasskeyStoreTest.php`:
- Around line 58-60: Replace all hardcoded test identifiers and the email in
PasskeyStoreTest with Faker-generated values: use Faker to create unique IDs for
'user-id' and 'credential-id' (e.g., $this->faker->uuid or unique()->uuid),
generate the email instead of 'person@example.com' (e.g.,
$this->faker->safeEmail), and replace hardcoded device label 'Laptop' with a
Faker string (e.g., $this->faker->word or userAgent). Update the test
instantiation that uses new VerifiedPasskeyCredential('credential-id', ...) and
any other places in the test class where IDs/tokens/emails are hardcoded to
reference the Faker variables (ensure Faker is available on the test class,
e.g., via setUp or a trait). Ensure all occurrences flagged in the review
(IDs/tokens/emails in the test) are replaced consistently.

In `@tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php`:
- Around line 35-183: The tests use hardcoded fixtures (emails, UUIDs, challenge
IDs/tokens, event IDs, and password literals) across helpers like
createSignupChallenge, createExpectedUser, createUserFactoryExpectingSignup,
createPasswordHasherExpectingRandomPassword, createEventIdFactory and
createRegisteredEventFactory; replace those literals by generating values once
with $this->faker (e.g. in setUp or class properties) and reuse those variables
in all helper methods so assertions/mocks still match (keep the same UUID
string, email, event-id, and hashed-password values but sourced from faker), and
ensure the password-hasher callback still validates a 64-char hex string while
the hasher mock returns the faker-derived hashed-password.

---

Duplicate comments:
In @.github/openapi-spec/spec.yaml:
- Around line 3041-3042: The OpenAPI spec is inconsistent: rememberMe is on
PasskeySignInOptionsDto but on PasskeySignUpCompleteDto for sign-up; to fix,
decide with the team to standardize on one pattern (recommended: options DTO
like PasskeySignUpOptionsDto to mirror PasskeySignInOptionsDto), then move the
rememberMe property from PasskeySignUpCompleteDto to PasskeySignUpOptionsDto in
the spec, and update all related references; also update the backend
handler/processor that consumes sign-up requests (the sign-up completion flow),
plus any affected tests and fixtures to accept rememberMe on the sign-up options
DTO (or conversely, if the team prefers completion DTO, move it from
PasskeySignInOptionsDto to PasskeySignUpCompleteDto and update backend/tests
accordingly).

---

Nitpick comments:
In @.env:
- Line 84: The PASSKEY_ALLOWED_ORIGINS environment variable is wrapped in
unnecessary quotes which may be read literally by env parsers and break origin
validation; update the .env entry for PASSKEY_ALLOWED_ORIGINS by removing the
surrounding double quotes so the value is unquoted
(PASSKEY_ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:8080),
ensuring it matches how other no-space variables are defined.

In @.env.load_test:
- Line 29: The PASSKEY_ALLOWED_ORIGINS value is wrapped in quotes which can
become literal characters when loaded; remove the surrounding double quotes so
the environment entry for PASSKEY_ALLOWED_ORIGINS is an unquoted comma-separated
list (e.g.
PASSKEY_ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:${LOAD_TEST_API_PORT}),
ensure there are no extra spaces, save the change and re-run the origin
validation to confirm it parses correctly.

In @.env.test:
- Line 31: The PASSKEY_ALLOWED_ORIGINS value in the .env.test file is wrapped in
double quotes which can lead to literal quotes being included when parsed and
break origin validation; update the PASSKEY_ALLOWED_ORIGINS entry by removing
the surrounding quotes so the value is an unquoted comma-separated list (refer
to the PASSKEY_ALLOWED_ORIGINS variable to locate the line) and ensure no spaces
or extra characters remain.

In `@specs/passkey-authentication/product-brief.md`:
- Line 21: Replace the header text "Non Goals" with the hyphenated form
"Non-Goals" to match product brief conventions and maintain consistency; locate
the heading "Non Goals" in the document (the section header) and update it to
"Non-Goals".

In `@tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTest.php`:
- Around line 284-292: In PasskeyRegistrationServiceTest update the anonymous
callback used in the willReturnCallback to mark the unused parameters as
intentionally unused by prefixing $id and $purpose with underscores (e.g. change
parameters in the static function signature to $_id and $_purpose while keeping
DateTimeImmutable $consumedAt unchanged); this will silence static analysis
warnings while preserving the callback behavior in the claimActive mock setup
that consumes $consumedAt and returns $challenge.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 538942b4-774d-4d75-9bf5-07847e9fc0e8

📥 Commits

Reviewing files that changed from the base of the PR and between 38aaa15 and 13bd760.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (92)
  • .env
  • .env.load_test
  • .env.test
  • .github/openapi-spec/spec.yaml
  • composer.json
  • config/api_platform/resources/EmptyResponse.yaml
  • config/doctrine/User/PasskeyChallenge.mongodb.xml
  • config/doctrine/User/PasskeyCredential.mongodb.xml
  • config/packages/security.yaml
  • config/services.yaml
  • config/services_test.yaml
  • config/validator/validation.yaml
  • deptrac.yaml
  • docs/advanced-configuration.md
  • docs/main.md
  • docs/passkey-authentication.md
  • specs/passkey-authentication/architecture.md
  • specs/passkey-authentication/epics.md
  • specs/passkey-authentication/implementation-readiness.md
  • specs/passkey-authentication/prd.md
  • specs/passkey-authentication/product-brief.md
  • specs/passkey-authentication/research.md
  • specs/passkey-authentication/run-summary.md
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolver.php
  • src/User/Application/DTO/PasskeyAuthenticationResult.php
  • src/User/Application/DTO/PasskeyOptionsResult.php
  • src/User/Application/DTO/PasskeyRegistrationCompleteDto.php
  • src/User/Application/DTO/PasskeyRegistrationOptionsDto.php
  • src/User/Application/DTO/PasskeySignInCompleteDto.php
  • src/User/Application/DTO/PasskeySignInOptionsDto.php
  • src/User/Application/DTO/PasskeySignUpCompleteDto.php
  • src/User/Application/DTO/PasskeySignUpOptionsDto.php
  • src/User/Application/DTO/VerifiedPasskeyCredential.php
  • src/User/Application/Factory/PasskeyVerifiedCredentialFactory.php
  • src/User/Application/Passkey/PasskeyAssertionCredentialRecordVerifier.php
  • src/User/Application/Passkey/PasskeyAttestationCredentialRecordVerifier.php
  • src/User/Application/Passkey/PasskeyAuthenticationService.php
  • src/User/Application/Passkey/PasskeyAuthenticationServiceInterface.php
  • src/User/Application/Passkey/PasskeyChallengeStore.php
  • src/User/Application/Passkey/PasskeyConfiguration.php
  • src/User/Application/Passkey/PasskeyCredentialResponseResolver.php
  • src/User/Application/Passkey/PasskeyCredentialStore.php
  • src/User/Application/Passkey/PasskeyCredentialVerifier.php
  • src/User/Application/Passkey/PasskeyCredentialVerifierInterface.php
  • src/User/Application/Passkey/PasskeyEncoding.php
  • src/User/Application/Passkey/PasskeyJsonCodec.php
  • src/User/Application/Passkey/PasskeyJsonCodecInterface.php
  • src/User/Application/Passkey/PasskeyOptionsFactory.php
  • src/User/Application/Passkey/PasskeyPublicKeyOptionsFactory.php
  • src/User/Application/Passkey/PasskeyRegistrationService.php
  • src/User/Application/Passkey/PasskeyRegistrationServiceInterface.php
  • src/User/Application/Passkey/PasskeyResponseFactory.php
  • src/User/Application/Passkey/PasskeySessionIssuer.php
  • src/User/Application/Passkey/PasskeyUserCreator.php
  • src/User/Application/Passkey/PasskeyUserResolver.php
  • src/User/Application/Passkey/PasskeyWebauthnFactory.php
  • src/User/Application/Passkey/PasskeyWebauthnFactoryInterface.php
  • src/User/Application/Processor/PasskeyRegistrationCompleteProcessor.php
  • src/User/Application/Processor/PasskeyRegistrationOptionsProcessor.php
  • src/User/Application/Processor/PasskeySignInCompleteProcessor.php
  • src/User/Application/Processor/PasskeySignInOptionsProcessor.php
  • src/User/Application/Processor/PasskeySignUpCompleteProcessor.php
  • src/User/Application/Processor/PasskeySignUpOptionsProcessor.php
  • src/User/Domain/Entity/PasskeyChallenge.php
  • src/User/Domain/Entity/PasskeyCredential.php
  • src/User/Domain/Repository/PasskeyChallengeRepositoryInterface.php
  • src/User/Domain/Repository/PasskeyCredentialRepositoryInterface.php
  • src/User/Domain/ValueObject/PasskeyChallengeContext.php
  • src/User/Infrastructure/Repository/MongoDBPasskeyChallengeRepository.php
  • src/User/Infrastructure/Repository/MongoDBPasskeyCredentialRepository.php
  • tests/Behat/Support/TestClientIpHttpRequestContextResolver.php
  • tests/Behat/UserContext/UserRequestContext.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolverTest.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolverLimitersTest.php
  • tests/Unit/User/Application/Passkey/PasskeyAuthenticationServiceTest.php
  • tests/Unit/User/Application/Passkey/PasskeyAuthenticationServiceTestSupport.php
  • tests/Unit/User/Application/Passkey/PasskeyConfigurationTest.php
  • tests/Unit/User/Application/Passkey/PasskeyCredentialVerifierTest.php
  • tests/Unit/User/Application/Passkey/PasskeyDtoTest.php
  • tests/Unit/User/Application/Passkey/PasskeyEncodingTest.php
  • tests/Unit/User/Application/Passkey/PasskeyJsonCodecTest.php
  • tests/Unit/User/Application/Passkey/PasskeyOptionsFactoryTest.php
  • tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTest.php
  • tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php
  • tests/Unit/User/Application/Passkey/PasskeyServiceTestObjects.php
  • tests/Unit/User/Application/Passkey/PasskeyStoreTest.php
  • tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php
  • tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
  • tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php
  • tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php
  • tests/Unit/User/Infrastructure/Repository/MongoDBPasskeyRepositoryTest.php
✅ Files skipped from review due to trivial changes (17)
  • src/User/Application/DTO/PasskeyAuthenticationResult.php
  • specs/passkey-authentication/implementation-readiness.md
  • src/User/Application/Factory/PasskeyVerifiedCredentialFactory.php
  • src/User/Application/DTO/PasskeyRegistrationOptionsDto.php
  • src/User/Application/Passkey/PasskeyResponseFactory.php
  • src/User/Domain/Repository/PasskeyChallengeRepositoryInterface.php
  • config/doctrine/User/PasskeyCredential.mongodb.xml
  • docs/main.md
  • docs/advanced-configuration.md
  • src/User/Application/Passkey/PasskeyChallengeStore.php
  • docs/passkey-authentication.md
  • specs/passkey-authentication/prd.md
  • src/User/Application/DTO/VerifiedPasskeyCredential.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolverLimitersTest.php
  • specs/passkey-authentication/research.md
  • src/User/Application/Passkey/PasskeyJsonCodecInterface.php
  • specs/passkey-authentication/architecture.md
🚧 Files skipped from review as they are similar to previous changes (49)
  • composer.json
  • src/User/Application/Passkey/PasskeyWebauthnFactoryInterface.php
  • src/User/Application/DTO/PasskeyOptionsResult.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolver.php
  • src/Shared/Application/Resolver/RateLimit/ApiRateLimitRequestResolver.php
  • src/User/Application/Passkey/PasskeySessionIssuer.php
  • config/doctrine/User/PasskeyChallenge.mongodb.xml
  • src/User/Application/Passkey/PasskeyUserResolver.php
  • config/packages/security.yaml
  • src/User/Application/Passkey/PasskeyAuthenticationServiceInterface.php
  • src/User/Application/Passkey/PasskeyAttestationCredentialRecordVerifier.php
  • src/User/Domain/Entity/PasskeyCredential.php
  • src/User/Application/Passkey/PasskeyAuthenticationService.php
  • src/User/Infrastructure/Repository/MongoDBPasskeyCredentialRepository.php
  • config/services_test.yaml
  • src/User/Application/Passkey/PasskeyAssertionCredentialRecordVerifier.php
  • deptrac.yaml
  • src/User/Application/Passkey/PasskeyCredentialResponseResolver.php
  • tests/Behat/UserContext/UserRequestContext.php
  • config/services.yaml
  • tests/Behat/Support/TestClientIpHttpRequestContextResolver.php
  • src/User/Domain/ValueObject/PasskeyChallengeContext.php
  • tests/Unit/User/Application/Passkey/PasskeyAuthenticationServiceTestSupport.php
  • tests/Unit/User/Domain/Entity/PasskeyCredentialTest.php
  • src/User/Application/Passkey/PasskeyWebauthnFactory.php
  • config/api_platform/resources/EmptyResponse.yaml
  • src/User/Domain/Entity/PasskeyChallenge.php
  • src/User/Application/Passkey/PasskeyConfiguration.php
  • src/User/Infrastructure/Repository/MongoDBPasskeyChallengeRepository.php
  • tests/Unit/User/Domain/Entity/PasskeyChallengeTest.php
  • src/User/Application/Passkey/PasskeyCredentialVerifierInterface.php
  • tests/Unit/Shared/Application/Resolver/RateLimit/ApiRateLimitAuthTargetResolverTest.php
  • src/User/Application/Passkey/PasskeyRegistrationService.php
  • src/User/Domain/Repository/PasskeyCredentialRepositoryInterface.php
  • tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php
  • src/User/Application/Passkey/PasskeyCredentialVerifier.php
  • tests/Unit/User/Application/Processor/PasskeyProcessorTest.php
  • src/User/Application/Passkey/PasskeyPublicKeyOptionsFactory.php
  • src/User/Application/Passkey/PasskeyOptionsFactory.php
  • src/User/Application/Passkey/PasskeyJsonCodec.php
  • src/User/Application/Passkey/PasskeyRegistrationServiceInterface.php
  • tests/Unit/User/Infrastructure/Repository/MongoDBPasskeyRepositoryTest.php
  • src/User/Application/Passkey/PasskeyUserCreator.php
  • tests/Unit/User/Application/Passkey/PasskeyServiceTestObjects.php
  • tests/Unit/User/Application/Passkey/PasskeyCredentialVerifierTest.php
  • src/User/Application/Passkey/PasskeyCredentialStore.php
  • tests/Unit/User/Application/Passkey/PasskeyConfigurationTest.php
  • tests/Unit/User/Application/Passkey/PasskeyOptionsFactoryTest.php
  • tests/Unit/User/Application/Passkey/PasskeyJsonCodecTest.php

Comment thread specs/passkey-authentication/epics.md Outdated
Comment thread tests/Unit/User/Application/CommandHandler/PasskeySignInCommandHandlerTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyDtoTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyStoreTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyUserCreatorTest.php Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 10, 2026
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/DTO/PasskeyConfiguration.php
Comment thread src/User/Application/Transformer/PasskeyEncodingTransformer.php
Comment thread tests/Unit/User/Application/Factory/PasskeyOptionsFactoryTest.php
Comment thread tests/Unit/User/Application/Factory/PasskeyOptionsFactoryTest.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyRegistrationServiceTestSupport.php Outdated
Comment thread tests/Unit/User/Application/Passkey/PasskeyServiceTestObjects.php Outdated
cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes May 10, 2026

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 10 files (changes from recent commits).

cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes May 10, 2026

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 1 file (changes from recent commits).

@dmytrocraft

Copy link
Copy Markdown
Contributor Author

Automated re-review requested after all review threads were resolved and CI is green. @coderabbitai review

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown

@dmytrocraft: Understood! I'll kick off a fresh review of PR #286 now that all threads are resolved and CI is green.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Code Review: FAIL

Review Output
=== Agent: codex ===
STATUS: FAIL
Issues:

1. `scripts/ai-review-prompts/review.md:31`: The default `make ai-review-loop` prompt tells reviewers to return `PENDING_REMOTE` / `PENDING_REMOTE_CI` so the runner keeps status pending, but `scripts/ai-review-loop.sh:1519-1568` treats every `STATUS: FAIL` as a normal failure and runs the fix agent. Expected fix: either remove remote-pending/BMAD-only instructions from the default local review prompt, or add explicit runner handling for pending-remote markers that stops without applying fixes.

Reviewed scope:
- Diff against `refs/heads/main`, focused on passkey OpenAPI schema, DDD/deptrac boundaries, repositories, tests, and review/CI scripts.

Evidence checked:
- `var/ai-review/local-ci-evidence-pr286.md`
- `git diff`, `git diff --check`, `rg` conflict-marker scan
- Passkey OpenAPI transformer/spec/tests and WebAuthn Level 3 shape from W3C: https://www.w3.org/TR/webauthn-3/
- Domain entities/repository mappings/services wiring
- `scripts/ai-review-loop.sh`, `scripts/bmad-fr-nfr-review-gate.sh`, prompt files, and Bats coverage references

Residual risk:
- I did not run CI, Bats, Docker, GitHub checks, or any long-running command per constraints. Local evidence otherwise looks sufficient for pre-push review once the script prompt/runner contract issue is fixed.
Verification Output

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Code Review: FAIL

Review Output
=== Agent: codex ===
STATUS: FAIL
Issues:
1. `config/api_platform/resources/EmptyResponse.yaml:83` / `.github/openapi-spec/spec.yaml:480` - public passkey sign-up/sign-in REST endpoints inherit the top-level OAuth2 requirement in the generated OpenAPI spec. Runtime marks `/api/passkeys/(signup|signin)` public in `config/packages/security.yaml:9`, but the four public passkey operations lack `security: []`, so clients see them as requiring authentication. Expected fix: add OpenAPI `security: []` for signup/signin options and complete operations, regenerate `.github/openapi-spec/spec.yaml`, and cover this with an OpenAPI test.

Reviewed scope:
Passkey OpenAPI transformer/generated spec, API Platform passkey resources, validation config, passkey command/repository boundaries, MongoDB mappings/index readiness, ai-review/BMAD script conflict-resolution paths.

Evidence checked:
Read `var/ai-review/local-ci-evidence-pr286.md`; reviewed diffs against `refs/heads/main`; checked no merge markers/new suppressions; inspected passkey schema tests, DDD dependency scans, repository tests, and ai-review pending-remote Bats coverage.

Residual risk:
I did not run prohibited long commands or Docker. Remote CI and reviewer approval remain out of scope for this local review.
Verification Output

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Code Review: PASS

Review Output
STATUS: PASS
0 issues.

Reviewed scope:
- Current worktree diff against `refs/heads/main`.
- Passkey OpenAPI spec/resource/schema transformer changes.
- Passkey DTO validation, processors, command handlers, domain entities, repositories, ODM mappings.
- AI review loop, BMAD gate wrapper, Makefile/workflow merge-resolution areas.

Evidence checked:
- Read `var/ai-review/local-ci-evidence-pr286.md`: full local CI, OpenAPI validation/diff, Schemathesis, unit/integration/Behat, Infection, Bats, and script syntax evidence all recorded as PASS.
- Ran read-only `git diff`, `git status`, `rg`, targeted file reads, and `git diff --check refs/heads/main`; no whitespace/conflict-marker issues found.
- Confirmed Domain remains framework-free for added passkey entities and persistence stays in Infrastructure/XML mappings.

Residual risk:
- Remote GitHub CI, branch protection, and reviewer approval were not checked here by design; separate publishing gates cover those after commit/push.```
</details>

<details>
<summary>Verification Output</summary>

```text

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

BMAD FR/NFR Review Gate: FAIL

Review Output
=== Agent: codex ===
Gate validation failure: PASS output has required fixes
STATUS: PASS
0 issues.
FR_NFR_SCORECARD: PASS
NFR_CATALOG_SCORECARD: PASS
EXPANDED_QUALITY_SCORECARD: PASS
SYSTEM_QUALITY_ATTRIBUTES_SCORECARD: PASS
WHOLE_CODEBASE_IMPACT: PASS
GRAPH_IMPACT_CONTEXT: PASS
TEST_CASE_MATRIX: PASS
AUTO_TEST_COVERAGE: PASS
FLAKY_TEST_RISK: PASS
MANUAL_TEST_EVIDENCE: PASS
QA_BEST_PRACTICES: PASS
GITHUB_COMPLETION_GATE: PASS
CI_GATE: PASS
FR_NFR_MIN_SCORE: 5/5
NFR_CATALOG_MIN_SCORE: 5/5
EXPANDED_QUALITY_MIN_SCORE: 5/5
SYSTEM_QUALITY_ATTRIBUTES_MIN_SCORE: 5/5
IMPACT_ANALYSIS_MIN_SCORE: 5/5
TEST_CASE_COVERAGE_MIN_SCORE: 5/5
AUTO_TEST_COVERAGE_MIN_SCORE: 5/5
FLAKY_TEST_RISK_MIN_SCORE: 5/5
GITHUB_COMPLETION_STATE: PASSING
GITHUB_HUMAN_APPROVAL_STATE: REVIEW_REQUIRED
CI_CHECK_ROLLUP: PASSING

Requirement Scorecard: source requirement, evidence, score, status

| Requirement | Evidence | Score | Status |
|---|---|---:|---|
| PRD FR-1 start signup options | DTO/YAML validation, existing-email conflict, RP/userVerification, TTL challenge; REST/GraphQL tests and browser evidence | 5/5 | PASS |
| PRD FR-2 complete signup | WebAuthn attestation validation, user/credential persistence, token issuance, rollback/replay tests | 5/5 | PASS |
| PRD FR-3 authenticated registration options | ROLE_USER, user-bound challenge, excludeCredentials, no credential-record leak | 5/5 | PASS |
| PRD FR-4 complete registration | user-bound claim, duplicate conflict, credential metadata/counter state, wrong-user tests | 5/5 | PASS |
| PRD FR-5 signin options | known/unknown privacy shape, empty allowCredentials, rememberMe challenge, TTL | 5/5 | PASS |
| PRD FR-6 complete signin | assertion verification, credential update, IssuedSessionFactory, SignInPublisher, 2FA parity | 5/5 | PASS |
| PRD NFRs and Epics 1-5 | Domain purity, YAML/XML config, env config, CI evidence, docs, load/manual evidence | 5/5 | PASS |
| Implementation readiness watch items | GraphQL Iterable support, skill/CI evidence, docs for no frontend UI scope | 5/5 | PASS |

NFR Catalog Scorecard: every pinned NFR category with checked subdimensions, evidence or not-applicable reason, source, score, status

Performance: response/throughput/latency/resource/concurrency/load/index/monitoring checked; evidence K6 passkey option scenarios 100% checks and p99 below thresholds, TTL/credential indexes, GitHub K6 SUCCESS; source PRD, nfr-catalog-evidence, graph load-test CI path, NonFunctionals catalog https://nonfunctionals.com/catalog.html; 5/5 PASS. Usability: task success/recovery/learnability/accessibility/API ergonomics checked; evidence REST/GraphQL contracts, docs, problem responses, manual browser ceremonies; source PRD/docs/manual/graph; 5/5 PASS. Maintainability: complexity/coupling/docs/static/mutation checked; evidence DDD boundaries, adapters, XML mappings, Psalm/Deptrac/PHPInsights/Infection; source source scan/run-summary/graph; 5/5 PASS. Availability: fallback/recovery/TTL/runbook/alerts checked; evidence password/OAuth/TOTP fallback, TTL expiry, 503 prod readiness gate, runbook; source docs/NFR/graph; 5/5 PASS. Interoperability: REST/GraphQL/OpenAPI/WebAuthn/backward compatibility checked; evidence API Platform YAML, generated OpenAPI/GraphQL, browser JSON, Schemathesis/Spectral/OpenAPI diff; source PRD/docs/CI; 5/5 PASS. Security: authn/authz/privacy/replay/rate-limit/vuln scan checked; evidence WebAuthn lib, atomic claim, ROLE_USER, privacy tests, Snyk, Psalm taint, rate-limit GraphQL tests; source code/tests/graph/CI; 5/5 PASS. Manageability: config/health/metrics/logging/runbook/release flags checked; evidence env vars, production readiness listener/command, docs monitoring thresholds; source services.yaml/docs/tests; 5/5 PASS. Automatability: deterministic CI/headless/evidence checked; evidence PHPUnit/Behat/Schemathesis/K6/Infection/Bats/GitHub checks, browser evidence only for authenticator boundary; source run-summary/CI; 5/5 PASS. Dependability: correctness/integrity/rollback/replay/edge tests checked; evidence atomic claim, duplicate rejection, rollback tests, replay/expiry/manual ceremonies; source tests/manual/graph; 5/5 PASS.

Expanded Quality Scorecard: every pinned expanded quality dimension with checked subdimensions, evidence or not-applicable reason, source, score, status

Functional Suitability: complete/correct/appropriate passkey ceremonies; evidence FR rows; source PRD/tests/manual; 5/5 PASS. Performance Resource Sustainability: bounded latency/storage/TTL/load; evidence K6, TTL, memory tests; source NFR/CI/graph; 5/5 PASS. Compatibility Coexistence: additive APIs and fallback auth; evidence OpenAPI diff, GraphQL Inspector, existing auth tests; source CI/graph; 5/5 PASS. Interaction Capability Accessibility: browser JSON/docs/errors/API ergonomics; evidence docs/manual/problem responses; source docs/manual; 5/5 PASS. Reliability Resilience: expiry/replay/rollback/recovery; evidence unit/integration/manual tests; source tests/graph; 5/5 PASS. Security Privacy Accountability: RP/origin/UV/privacy/rate limits/audit path; evidence WebAuthn validators, privacy tests, SignInPublisher, Snyk/taint; source code/CI; 5/5 PASS. Maintainability Testability: modularity/static/mutation/edge coverage; evidence factories/resolvers/validators, Deptrac/Infection/Psalm; source code/run-summary; 5/5 PASS. Flexibility Portability: env config, RP/origin, Docker/GitHub, fallback auth; evidence envs/docs/workflows; source config/docs/CI; 5/5 PASS. Safety Harm Prevention: no token on invalid/2FA pending, single-use challenges; evidence negative tests/manual 2FA/replay; source tests/manual; 5/5 PASS. Data Quality Integrity: credential uniqueness/counter/user binding/TTL; evidence XML indexes, repository tests, readiness command; source code/tests/graph; 5/5 PASS. Operational Excellence Releaseability: release flags/readiness/runbook/rollback; evidence prod readiness gate, CI rollup, docs; source docs/CI; 5/5 PASS. Observability Diagnosability: EMF endpoint metrics/logging/runbook/no secret evidence; evidence docs and existing metrics path; source docs/graph; 5/5 PASS. Supply-Chain Integrity: pinned lock/workflows/security checks; evidence composer lock, Snyk, checkout pin, CI; source diff/CI; 5/5 PASS. Compliance Governance: privacy/security evidence, sanitized manual artifacts, no token leakage; source manual/docs/tests; 5/5 PASS. Sustainability Resource Impact: bounded TTL, no sweep worker, K6/memory evidence; source docs/tests; 5/5 PASS. AI Automation Governance: BMAD loop/status, Bats tests, no high-risk autonomous writes beyond BMAD status; source scripts/tests/CI; 5/5 PASS.

System Quality Attributes Scorecard: every pinned Wikipedia system quality attribute with checked meaning, evidence or concrete not-applicable reason, source, score, status, improvement recommendation

External source checked: Wikipedia current page lists the pinned attributes and was last edited 2026-04-21, https://en.wikipedia.org/wiki/List_of_system_quality_attributes. Rows use Source=Wiki+PRD/graph/tests unless stated. Accessibility API/docs recoverability evidence docs/OpenAPI/GraphQL/manual 5/5 PASS Improvement: none; Accountability audit/sign-in event/GitHub evidence 5/5 PASS Improvement: none; Accuracy WebAuthn verification/data binding tests 5/5 PASS Improvement: none; Adaptability env RP/origin/fallback docs 5/5 PASS Improvement: none; Administrability readiness command/env/runbook 5/5 PASS Improvement: none; Affordability bounded infra/no new workers/K6 resource evidence 5/5 PASS Improvement: none; Agility modular DDD/CI/mutation 5/5 PASS Improvement: none; Analyzability graph/static/tests/docs 5/5 PASS Improvement: none; Auditability SignInPublisher/manual sanitized/GitHub checks 5/5 PASS Improvement: none; Autonomy deterministic automation with BMAD safeguards 5/5 PASS Improvement: none; Availability fallback/TTL/readiness 5/5 PASS Improvement: none; Compatibility additive REST/GraphQL/OpenAPI diff 5/5 PASS Improvement: none; Composability command bus/factories/resolvers 5/5 PASS Improvement: none; Confidentiality privacy shape/no token leak/manual sanitization 5/5 PASS Improvement: none; Configurability PASSKEY env/readiness flags 5/5 PASS Improvement: none; Convenience stable browser JSON/docs 5/5 PASS Improvement: none; Correctness FR tests/manual 5/5 PASS Improvement: none; Credibility CI/manual/graph traceability 5/5 PASS Improvement: none; Customizability RP/origin/timeout/env 5/5 PASS Improvement: none; Debuggability problem responses/logging/runbook 5/5 PASS Improvement: none; Degradability password/OAuth/TOTP fallback and 503 gate 5/5 PASS Improvement: none; Determinability deterministic tests and immutable evidence 5/5 PASS Improvement: none; Demonstrability manual browser plus CI artifacts 5/5 PASS Improvement: none; Dependability replay/rollback/duplicate/counter tests 5/5 PASS Improvement: none; Deployability Docker/workflows/readiness command 5/5 PASS Improvement: none; Discoverability docs/OpenAPI/GraphQL descriptions 5/5 PASS Improvement: none; Distributability container/env portability 5/5 PASS Improvement: none; Durability credential persistence/indexes/backups inherited 5/5 PASS Improvement: none; Effectiveness ceremonies meet outcomes 5/5 PASS Improvement: none; Efficiency K6/memory/indexes 5/5 PASS Improvement: none; Elasticity load profiles/capacity formula 5/5 PASS Improvement: none; Evolvability adapters/DDD/docs 5/5 PASS Improvement: none; Extensibility WebAuthn adapters/factories 5/5 PASS Improvement: none; Failure Transparency RFC7807/GraphQL errors/no leaks 5/5 PASS Improvement: none; Familiarity API Platform/AuthPayload patterns 5/5 PASS Improvement: none; Fault-Tolerance TTL/retry fresh challenge/fallback auth 5/5 PASS Improvement: none; Fidelity browser WebAuthn evidence 5/5 PASS Improvement: none; Flexibility env/config/additive APIs 5/5 PASS Improvement: none; Inspectability graph/OpenAPI/docs/tests 5/5 PASS Improvement: none; Installability composer/Docker/env docs 5/5 PASS Improvement: none; Integrity unique indexes/user binding/counter 5/5 PASS Improvement: none; Interactivity browser JSON/manual ceremonies 5/5 PASS Improvement: none; Interchangeability standard WebAuthn/browser JSON 5/5 PASS Improvement: none; Interoperability REST/GraphQL/WebAuthn/OpenAPI 5/5 PASS Improvement: none; Intuitiveness docs/examples/error recovery 5/5 PASS Improvement: none; Learnability docs/manual checklist/examples 5/5 PASS Improvement: none; Localizability no new user-facing localization surface beyond existing API errors 5/5 PASS Improvement: none; Maintainability static/mutation/DDD 5/5 PASS Improvement: none; Manageability readiness/runbook/metrics 5/5 PASS Improvement: none; Mobility browser standard passkeys 5/5 PASS Improvement: none; Modifiability layered factories/resolvers 5/5 PASS Improvement: none; Modularity Domain/Application/Infrastructure separation 5/5 PASS Improvement: none; Observability EMF path/docs/logs 5/5 PASS Improvement: none; Operability make targets/readiness/runbook 5/5 PASS Improvement: none; Orthogonality passkeys additive, existing auth unaffected 5/5 PASS Improvement: none; Portability Docker/env/WebAuthn standards 5/5 PASS Improvement: none; Precision validation/schema/tests 5/5 PASS Improvement: none; Predictability deterministic errors/TTL/rate limits 5/5 PASS Improvement: none; Process Capabilities BMAD skill sweep/CI evidence 5/5 PASS Improvement: none; Producibility generated specs/docs/make targets 5/5 PASS Improvement: none; Provability graph/manual/CI traceability 5/5 PASS Improvement: none; Recoverability fallback auth/fresh challenge/runbook 5/5 PASS Improvement: none; Redundancy fallback auth paths 5/5 PASS Improvement: none; Relevance PRD scoped to backend passkeys 5/5 PASS Improvement: none; Reliability integration/unit/manual/CI 5/5 PASS Improvement: none; Repairability modular source/runbook 5/5 PASS Improvement: none; Repeatability make/CI deterministic tests 5/5 PASS Improvement: none; Reproducibility sanitized artifacts/run commands 5/5 PASS Improvement: none; Resilience replay/expiry/rollback/rate-limit 5/5 PASS Improvement: none; Responsiveness K6 p99 evidence 5/5 PASS Improvement: none; Reusability shared command/session/rate-limit patterns 5/5 PASS Improvement: none; Robustness negative/edge/security tests 5/5 PASS Improvement: none; Safety no token leakage/single-use/2FA parity 5/5 PASS Improvement: none; Scalability load/capacity formula/indexes 5/5 PASS Improvement: none; Seamlessness existing auth payload/session patterns 5/5 PASS Improvement: none; Self-Sustainability TTL cleanup/no sweep worker 5/5 PASS Improvement: none; Serviceability readiness command/runbook/logging 5/5 PASS Improvement: none; Securability WebAuthn/rate-limit/Snyk/taint 5/5 PASS Improvement: none; Simplicity adapter isolation/no hand-rolled crypto 5/5 PASS Improvement: none; Stability CI/passkey tests/manual evidence 5/5 PASS Improvement: none; Standards Compliance W3C WebAuthn/OpenAPI/GraphQL 5/5 PASS Improvement: none; Survivability fallback auth and production gate 5/5 PASS Improvement: none; Sustainability bounded TTL/load/memory 5/5 PASS Improvement: none; Tailorability env RP/origin/timeouts 5/5 PASS Improvement: none; Testability unit/integration/Behat/K6/mutation 5/5 PASS Improvement: none; Timeliness timeout/TTL/K6 p99 5/5 PASS Improvement: none; Traceability PRD-to-code/tests/graph/manual 5/5 PASS Improvement: none; Transparency docs/error responses/GitHub checks 5/5 PASS Improvement: none; Ubiquity WebAuthn browser standards 5/5 PASS Improvement: none; Understandability docs/naming/architecture 5/5 PASS Improvement: none; Upgradability composer lock/adapter isolation 5/5 PASS Improvement: none; Usability docs/browser JSON/recovery 5/5 PASS Improvement: none; Vulnerability Snyk/Psalm taint/security tests 5/5 PASS Improvement: none.

Whole-Codebase Impact Analysis: every pinned impact surface, related files or concrete not-applicable reason, mandatory graph/relationship evidence, source, score, status

Runtime paths: passkey REST/GraphQL, rate-limit parser, session issuance, readiness gate; graph edges inspect processors->commands->validators->repos and ApiRateLimit* AST path; source graph context + code; 5/5 PASS. Architecture and layer boundaries: Domain pure, XML/YAML config, Deptrac success; source rg/domain scan/CI; 5/5 PASS. Domain model: PasskeyChallenge/Credential/Collection/context one-way consumption; source domain files/tests/graph; 5/5 PASS. Persistence and database: ODM XML, TTL/unique/user indexes, atomic claim; source mapping/repositories/readiness tests; 5/5 PASS. Public API and schema: REST OpenAPI, AuthPayload GraphQL, generated specs, backward-compatible diffs; source config/specs/CI; 5/5 PASS. Async events and queues: no new queue; sign-in publisher reused; source graph/PRD; 5/5 PASS. Configuration and environment: PASSKEY_* envs, Docker/workflows/prod flags; source services/env/docs; 5/5 PASS. Dependencies and lockfiles: web-auth/webauthn-lib in composer lock, Snyk/security checks; source composer/CI; 5/5 PASS. CI and workflows: PHPUnit, Behat, K6, Infection, Psalm, Deptrac, Schemathesis, Bats all green; source wrapper CI rollup; 5/5 PASS. Tests and fixtures: passkey unit/integration/GraphQL/memory/load/manual evidence; source tests/run-summary; 5/5 PASS. Documentation: docs/passkey-authentication, performance, planning, manual/NFR evidence; source docs/specs; 5/5 PASS. Operations and observability: EMF path, readiness 503, runbook/alerts; source docs/listener/graph; 5/5 PASS. Security and privacy: WebAuthn, rate limits, privacy shape, no token leak, taint/Snyk; source tests/CI; 5/5 PASS. Backward compatibility: additive APIs, existing auth routes still covered, OpenAPI diff compatible; source CI/graph; 5/5 PASS.

Graph Impact Context: graph artifact path, graph provider, changed-file relationship edges inspected, source files validated, score, status

Graph artifact path: `/home/kravtsov/Projects/user-service-pr286-finish/var/ai-review/bmad-final/bmad-required-impact-and-github-context.md`; provider: wrapper-generated bounded relationship graph plus Graphify JSON path `/graphify-out/graph.json`, Deptrac config, and GitHub/CI corroboration. Edges inspected: REST/GraphQL processors->command bus->handlers->PasskeyOptionsFactory/PasskeyCredentialValidator->MongoDB repositories; ApiRateLimitListener->ApiRateLimitRequestResolver->GraphQL AST/payload/value resolvers; PasskeyProductionReadinessListener->GraphQL request resolver/inspector; load-test workflow->make load-tests->K6 passkey scenarios. Source files validated: domain entities, XML mappings, processors/resolvers, service config, validator YAML, OpenAPI/GraphQL configs, tests, docs, workflows. Score 5/5, status PASS.

Test Case Matrix: every FR/NFR/acceptance/quality requirement with generated positive, negative, and edge cases; mapped automated/manual evidence; missing tests; score, status

FR-1 positive new signup options, negative invalid/existing email, edge TTL/browser-safe resident key; evidence REST/GraphQL integration, DTO validation, manual browser; missing none; 5/5 PASS. FR-2 positive browser signup complete, negative invalid attestation/replay/existing-after-options, edge rollback/session failure; evidence manual, command-handler/GraphQL/repository tests; missing none; 5/5 PASS. FR-3 positive authenticated registration options, negative unauthenticated, edge existing descriptors; evidence REST/GraphQL/access tests; missing none; 5/5 PASS. FR-4 positive complete registration, negative wrong-user/duplicate/invalid credential, edge challenge not consumed for wrong user then consumed by owner invalid credential; evidence REST/GraphQL/unit tests; missing none; 5/5 PASS. FR-5 positive known/unknown signin options, negative invalid email, edge empty allowCredentials privacy and rememberMe; evidence REST/GraphQL/manual; missing none; 5/5 PASS. FR-6 positive signin tokens, negative unknown/mismatched credential/replay, edge 2FA pending/no token and counter update; evidence manual/unit/GraphQL tests; missing none; 5/5 PASS. NFR/quality positive green CI, negative boundary/security/rate-limit invalid inputs, edge concurrency/atomic claim/load/memory/readiness disabled/monitoring false; evidence CI rollup, Infection, K6, readiness tests; missing none; 5/5 PASS.

Automated Test And CI Coverage: every repeatable FR/NFR/quality case mapped to automated tests and CI checks; uncovered gaps; score, status

Repeatable coverage maps to `PasskeyAuthEndpointsIntegrationTest`, `PasskeyGraphQLAuthOptionsIntegrationTest`, `PasskeyGraphQLCompletionFailureTest`, `PasskeyGraphQLCompletionResponseTest`, passkey command-handler/factory/validator/repository/unit tests, rate-limit GraphQL tests, production-readiness tests, memory tests, K6 scripts, OpenAPI/Spectral/Schemathesis, Deptrac, Psalm/taint, PHPInsights, Infection, Bats, and GitHub full visible check rollup. Browser authenticator completion is the only manual-supported surface, with server-side repeatable behavior automated. Uncovered gaps: none. Score 5/5, status PASS.

Flaky Test Risk: changed and impacted tests, nondeterminism sources, mitigation/evidence, score, status

Changed-test scan found no sleeps in passkey unit/integration suites; Faker data is unique/deterministic per test isolation; DateTimeImmutable usage is object state, not wall-clock assertion; GraphQL memory IP sequence avoids shared rate-limit interference; load-test sleeps are bounded retry/setup behavior and pass under K6 CI. GitHub CI, full unit/integration/Behat, K6, memory, Infection, and Bats passed. Score 5/5, status PASS.

Manual Test Evidence: tester/date/scenario/steps/observed result/artifacts, score, status

Tester Codex; latest browser rerun 2026-06-01T02:23:28Z; Chrome headless DevTools virtual CTAP2 authenticators; runtime source base `69af2cf13c46f797da7076bff272fa7736e01ce9`; artifact `specs/passkey-authentication/manual-browser-run-1780280604724-451d4e.sanitized.md`; source bridge documented in `manual-browser-evidence.md`. Scenarios: signup success, existing-email rejection, replay rejection, signin before 2FA, authenticated registration, 2FA setup/confirm, signin after 2FA pending response, expiration rejection. Observed results match PRD; secrets/tokens sanitized. Score 5/5, status PASS.

QA Verification: commands, tests, CI, coverage, mutation, static analysis, score, status

Local sandbox verification performed: read-only `git diff --check` passed; no added suppressions; domain dependency scan clean; `gh` direct query failed due network, so wrapper GitHub evidence used. Recorded repository verification: full unit 2,579 tests/7,644 assertions, integration 126/745, Behat 644 scenarios/3,622 steps, Infection 5,491 generated/5,488 killed/3 timed out/MSI 100%, Psalm and taint clean, Deptrac clean, PHPInsights thresholds green, OpenAPI/Spectral/Schemathesis green, K6 passkey scenarios green. Score 5/5, status PASS.

GitHub Completion Gate: comments, human approval state, requested changes, checks, score, status

Direct `gh` unavailable in child sandbox. Wrapper context generated 2026-06-08T16:01:38Z: PR #286 non-draft, head `e9acebc1b208f7613cd20e87c28948e05177582d` matches local, active unresolved threads 0, no requested-changes evidence, review decision `REVIEW_REQUIRED` allowed by prompt, merge state blocked only by approval policy. Score 5/5, status PASS.

CI Gate: required/applicable checks, status, conclusion, run URL, score, status

Required-check query reports no required checks on branch, so full visible rollup used excluding `BMAD FR/NFR Review Gate`. All visible checks SUCCESS: codecov patch/project, Cache Load Tests, GraphQL Inspector, Psalm, Infection, Memory leak tests, Cache Integration Tests, symfony-checks, Openapi-diff, PHPUnit, PHP Insights, test-and-report, Bats, K6, copilot setup, Deptrac, openapi-diff, Schemathesis, Spectral, Behat, lint, eslint, Code Review, CodeRabbit, qlty check/fmt, Snyk. Run URLs are in wrapper JSON under `/var/ai-review/bmad-final/github-corroboration-context.md`. Score 5/5, status PASS.

Required Fixes: file path, short description, expected fix

None.
Verification Output

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

BMAD FR/NFR Review Gate: FAIL

Review Output
=== Agent: codex ===
Gate validation failure: GitHub corroboration failed
STATUS: PASS
0 issues.
FR_NFR_SCORECARD: PASS
NFR_CATALOG_SCORECARD: PASS
EXPANDED_QUALITY_SCORECARD: PASS
SYSTEM_QUALITY_ATTRIBUTES_SCORECARD: PASS
WHOLE_CODEBASE_IMPACT: PASS
GRAPH_IMPACT_CONTEXT: PASS
TEST_CASE_MATRIX: PASS
AUTO_TEST_COVERAGE: PASS
FLAKY_TEST_RISK: PASS
MANUAL_TEST_EVIDENCE: PASS
QA_BEST_PRACTICES: PASS
GITHUB_COMPLETION_GATE: PASS
CI_GATE: PASS
FR_NFR_MIN_SCORE: 5/5
NFR_CATALOG_MIN_SCORE: 5/5
EXPANDED_QUALITY_MIN_SCORE: 5/5
SYSTEM_QUALITY_ATTRIBUTES_MIN_SCORE: 5/5
IMPACT_ANALYSIS_MIN_SCORE: 5/5
TEST_CASE_COVERAGE_MIN_SCORE: 5/5
AUTO_TEST_COVERAGE_MIN_SCORE: 5/5
FLAKY_TEST_RISK_MIN_SCORE: 5/5
GITHUB_COMPLETION_STATE: PASSING
GITHUB_HUMAN_APPROVAL_STATE: REVIEW_REQUIRED
CI_CHECK_ROLLUP: PASSING

Requirement Scorecard: source requirement, evidence, score, status

| Requirement | Evidence | Score | Status |
|---|---|---:|---|
| PRD FR-1 start passkey sign-up | `specs/passkey-authentication/prd.md`; REST `/api/passkeys/signup/options`, GraphQL `passkeySignUpOptionsUser`, DTO/YAML validation, existing-email 409 before challenge creation, RP/userVerification, TTL challenge; tests `PasskeyAuthEndpointsIntegrationTest`, `PasskeyGraphQLAuthOptionsIntegrationTest`; graph edges options processor -> command handler -> `PasskeyOptionsFactory` -> repository | 5/5 | PASS |
| PRD FR-2 complete passkey sign-up | `CompletePasskeySignUpCommandHandler`, `PasskeyCredentialValidator`, rollback tests, browser signup completion evidence, token/cookie response tests, replay rejection; graph claim -> validator -> credential repo -> session issuance | 5/5 | PASS |
| PRD FR-3 start authenticated registration | `security: ROLE_USER`, `PasskeyRegistrationOptionsProcessor`, `excludeCredentials`, user-bound challenge, no credential-record exposure; REST/GraphQL integration tests | 5/5 | PASS |
| PRD FR-4 complete authenticated registration | `CompletePasskeyRegistrationCommandHandler`, user-bound atomic claim, duplicate credential conflict, persisted credential metadata; wrong-user/duplicate tests | 5/5 | PASS |
| PRD FR-5 start passkey sign-in | `StartPasskeySignInCommandHandler`, known/unknown privacy-preserving response shape, empty `allowCredentials`, remember-me persisted in challenge; REST/GraphQL/manual evidence | 5/5 | PASS |
| PRD FR-6 complete passkey sign-in | Assertion verification, credential counter/record update, `IssuedSessionFactory`, `SignInPublisher`, 2FA pending-session parity; unit/integration/manual tests | 5/5 | PASS |
| PRD NFRs | Domain pure by scan, YAML/XML config, MongoDB TTL, env-backed RP/origins/timeouts, random single-use challenges, browser JSON compatibility, existing sign-in event path, WebAuthn adapter isolation; Deptrac/Psalm/Infection/CI green | 5/5 | PASS |
| Epic 1 planning/config | BMAD specs and docs/planning mirror committed; WebAuthn dependency and `PASSKEY_*` envs present; run-summary has BMALPH evidence | 5/5 | PASS |
| Epic 2 persistence | `PasskeyCredential`/`PasskeyChallenge`, XML mappings, unique/user/TTL indexes, repository tests, readiness command | 5/5 | PASS |
| Epic 3 registration ceremonies | Signup/enrollment positive, duplicate/rollback/replay negative, credential persistence | 5/5 | PASS |
| Epic 4 authentication ceremony | Sign-in options/completion, counter update, token/2FA paths | 5/5 | PASS |
| Epic 5 docs/tests | `docs/passkey-authentication.md`, OpenAPI/GraphQL specs, unit/integration/Behat/K6/manual evidence, GitHub checks | 5/5 | PASS |
| Implementation readiness watch items | Small collaborators, GraphQL `Iterable`, no frontend UI in backend repo with docs instead, skill/CI evidence in run summary | 5/5 | PASS |

NFR Catalog Scorecard: every pinned NFR category with checked subdimensions, evidence or not-applicable reason, source, score, status

| Category | Checked subdimensions and evidence | Source | Score | Status |
|---|---|---|---:|---|
| Performance | Response time, throughput, latency, concurrency, load/stress/spike, indexes, bottleneck and monitoring checked. K6 passkey option scenarios have 100% checks and p99 below thresholds; GitHub K6 and memory checks green; graph load-test workflow -> `make load-tests` -> passkey scenarios. | PRD, `nfr-catalog-evidence.md`, `passkey-load-run...md`, graph context, https://nonfunctionals.com/catalog.html | 5/5 | PASS |
| Usability | Task success, error recovery, learnability, accessibility/API ergonomics checked. Stable browser JSON, API errors, docs, OpenAPI/GraphQL descriptions, manual ceremonies. | PRD, docs, manual evidence, graph context, NonFunctionals catalog | 5/5 | PASS |
| Maintainability | Complexity, debt, coverage, docs, modularity, static analysis, dependency mapping checked. DDD boundaries, WebAuthn adapters, XML mappings, Psalm/Deptrac/PHPInsights/Infection green; no new suppressions. | Source scan, run summary, graph context, NonFunctionals catalog | 5/5 | PASS |
| Availability | Uptime relevance, fallback, recovery, TTL cleanup, readiness gate, runbook and alert guidance checked. Password/OAuth/TOTP fallback remains; prod passkey traffic returns 503 until flags and monitoring ready. | Docs, readiness tests, graph context, NonFunctionals catalog | 5/5 | PASS |
| Interoperability | REST/GraphQL/OpenAPI/WebAuthn/browser JSON/backward compatibility checked. Additive endpoints, WebAuthn library, API Platform YAML, Schemathesis/Spectral/OpenAPI diff/GraphQL Inspector green. | PRD, configs, CI rollup, https://nonfunctionals.com/catalog/interoperability.html | 5/5 | PASS |
| Security | Authn/authz, confidentiality, integrity, privacy, replay, rate limiting, dependency/vuln scanning checked. Atomic claims, ROLE_USER registration, privacy shape tests, Snyk/Psalm taint, GraphQL rate-limit tests. | Code/tests/CI/graph, https://nonfunctionals.com/catalog/security.html | 5/5 | PASS |
| Manageability | Monitoring, MTTD/MTTR, config, health/readiness, logs, runbooks, SLO evidence checked. `PASSKEY_*` flags, readiness command/listener, EMF endpoint metrics path, docs capacity/alerts. | Docs, Makefile, tests, graph context, NonFunctionals catalog | 5/5 | PASS |
| Automatability | Headless/deterministic checks, CI/CD fit, stable APIs, immutable evidence checked. PHPUnit, Behat, Schemathesis, K6, Infection, Bats, GitHub checks; manual only for authenticator boundary. | Run summary, CI wrapper, graph context, NonFunctionals catalog | 5/5 | PASS |
| Dependability | Reliability, integrity, correctness, rollback, replay, idempotency, edge/mutation evidence checked. Atomic claim, duplicate rejection, rollback tests, replay/expiry/manual evidence. | Tests, manual evidence, graph context, NonFunctionals catalog | 5/5 | PASS |

Expanded Quality Scorecard: every pinned expanded quality dimension with checked subdimensions, evidence or not-applicable reason, source, score, status

| Dimension | Checked subdimensions and evidence | Source | Score | Status |
|---|---|---|---:|---|
| Functional Suitability | Completeness/correctness/appropriateness covered by FR rows and REST/GraphQL/manual evidence. | PRD/tests/manual/graph | 5/5 | PASS |
| Performance Resource Sustainability | Time behavior, capacity, storage/TTL, memory and bounded growth covered by K6, memory tests, TTL indexes. | NFR evidence/CI/graph | 5/5 | PASS |
| Compatibility Coexistence | Additive APIs, existing auth fallback, OpenAPI diff and GraphQL Inspector green. | CI/config/graph | 5/5 | PASS |
| Interaction Capability Accessibility | Browser JSON, docs, problem responses, API ergonomics; no UI in backend scope. | Docs/manual/API tests | 5/5 | PASS |
| Reliability Resilience | Expiry, replay, rollback, 2FA, rate limits and readiness fallback tested. | Tests/manual/graph | 5/5 | PASS |
| Security Privacy Accountability | RP/origin/user verification, no token leaks, rate limits, sign-in event path, Snyk/taint. | Code/tests/CI | 5/5 | PASS |
| Maintainability Testability | Modular factories/resolvers/validators, static checks, mutation, edge coverage. | Source/run summary | 5/5 | PASS |
| Flexibility Portability | Env config, Docker/CI, RP/origin flexibility, WebAuthn standard. | Config/docs/CI | 5/5 | PASS |
| Safety Harm Prevention | No token on invalid or 2FA pending, single-use challenges, rollback safety. | Negative tests/manual | 5/5 | PASS |
| Data Quality Integrity | Credential uniqueness, user binding, counter update, TTL, atomic claim. | XML/repositories/tests | 5/5 | PASS |
| Operational Excellence Releaseability | Readiness command/listener, runbook, CI, rollback notes. | Makefile/docs/CI | 5/5 | PASS |
| Observability Diagnosability | EMF endpoint metrics path, rate-limit logs, readiness 503, runbook; no sensitive evidence. | Docs/code/graph | 5/5 | PASS |
| Supply-Chain Integrity | Composer lock, pinned workflow checkout, Snyk/security checks, CI trust boundaries. | `composer.lock`, workflows, CI | 5/5 | PASS |
| Compliance Governance | Privacy minimization, sanitized manual artifacts, audit/change evidence. | Manual/docs/tests | 5/5 | PASS |
| Sustainability Resource Impact | TTL cleanup, no sweep worker, bounded load/memory evidence. | Docs/tests/K6 | 5/5 | PASS |
| AI Automation Governance | BMAD loop, GitHub status/comment low-risk automation, Bats coverage, no high-risk autonomous write. | Scripts/tests/CI | 5/5 | PASS |

System Quality Attributes Scorecard: every pinned Wikipedia system quality attribute with checked meaning, evidence or concrete not-applicable reason, source, score, status, improvement recommendation

Source: current Wikipedia page includes the pinned attributes and was last edited 2026-04-21; source used: https://en.wikipedia.org/wiki/List_of_system_quality_attributes. Each row also uses PRD, graph context, tests, CI, and manual evidence.

| Attribute | Checked meaning in this PR | Evidence | Source | Score | Status | Improvement |
|---|---|---|---|---:|---|---|
| Accessibility | API/client recoverability and docs clarity | OpenAPI/GraphQL/docs/problem responses | Wiki+PRD+docs | 5/5 | PASS | none |
| Accountability | Traceable auth events and review evidence | SignInPublisher, GitHub checks, sanitized evidence | Wiki+code+CI | 5/5 | PASS | none |
| Accuracy | Correct WebAuthn and persisted data binding | validator/repository tests | Wiki+tests | 5/5 | PASS | none |
| Adaptability | Env-configurable RP/origin/timeouts | `PASSKEY_*` config tests | Wiki+config | 5/5 | PASS | none |
| Administrability | Operator commands and readiness controls | `make passkey-production-readiness`, docs | Wiki+Makefile | 5/5 | PASS | none |
| Affordability | Bounded infra/cost impact | no worker sweep, TTL, K6/memory | Wiki+NFR | 5/5 | PASS | none |
| Agility | Changeable modular DDD implementation | factories/resolvers/handlers, CI | Wiki+graph | 5/5 | PASS | none |
| Analyzability | Static/graph/test traceability | Graphify context, Psalm, Deptrac | Wiki+graph | 5/5 | PASS | none |
| Auditability | Review, security, event audit evidence | sign-in event, GitHub, docs | Wiki+CI | 5/5 | PASS | none |
| Autonomy | Safe deterministic automation | BMAD scripts/Bats/status safeguards | Wiki+tests | 5/5 | PASS | none |
| Availability | Service continuity and fallback | TTL, 503 readiness, existing auth fallback | Wiki+docs | 5/5 | PASS | none |
| Compatibility | Existing clients not broken | additive APIs, OpenAPI diff, GraphQL Inspector | Wiki+CI | 5/5 | PASS | none |
| Composability | Fits command bus and factories | processors -> commands -> handlers graph | Wiki+graph | 5/5 | PASS | none |
| Confidentiality | No credential/user/token leakage | privacy tests, sanitized artifacts | Wiki+tests | 5/5 | PASS | none |
| Configurability | Runtime flags and RP/origin values | `.env*`, `services.yaml`, config tests | Wiki+config | 5/5 | PASS | none |
| Convenience | Browser JSON and docs support integration | manual browser/docs/OpenAPI examples | Wiki+docs | 5/5 | PASS | none |
| Correctness | FR behavior matches spec | FR tests and manual evidence | Wiki+PRD | 5/5 | PASS | none |
| Credibility | Evidence is corroborated | CI, graph, manual, GitHub connector/wrapper | Wiki+evidence | 5/5 | PASS | none |
| Customizability | Per-env passkey settings | config DTO and envs | Wiki+code | 5/5 | PASS | none |
| Debuggability | Diagnosable errors/logging | RFC7807, rate-limit logs, runbook | Wiki+docs | 5/5 | PASS | none |
| Degradability | Safe fallback when passkeys unavailable | password/OAuth/TOTP fallback, prod 503 | Wiki+docs | 5/5 | PASS | none |
| Determinability | Deterministic repeatable behavior | unit/integration/K6/CI, fixed artifacts | Wiki+tests | 5/5 | PASS | none |
| Demonstrability | Behavior can be shown with artifacts | browser transcript, CI URLs, specs | Wiki+manual | 5/5 | PASS | none |
| Dependability | Trustworthy auth under failure | replay/rollback/duplicate/counter tests | Wiki+tests | 5/5 | PASS | none |
| Deployability | Deploy gates and Docker support | workflows, Docker env pass-through, readiness | Wiki+CI | 5/5 | PASS | none |
| Discoverability | API surface discoverable | OpenAPI/GraphQL descriptions/docs | Wiki+API | 5/5 | PASS | none |
| Distributability | Containerized service fit | Docker compose/envs/workflows | Wiki+config | 5/5 | PASS | none |
| Durability | Persistent credentials protected | MongoDB indexes, backup inherited | Wiki+persistence | 5/5 | PASS | none |
| Effectiveness | Meets user outcome | signup/enroll/sign-in/2FA evidence | Wiki+manual | 5/5 | PASS | none |
| Efficiency | Meets latency/resource thresholds | K6 p99, memory, indexes | Wiki+K6 | 5/5 | PASS | none |
| Elasticity | Handles profiles and capacity planning | smoke/average/stress/spike, capacity formula | Wiki+NFR | 5/5 | PASS | none |
| Evolvability | Adapter isolation and docs | WebAuthn factory/validator boundaries | Wiki+graph | 5/5 | PASS | none |
| Extensibility | Future WebAuthn changes isolated | interface/factory design | Wiki+code | 5/5 | PASS | none |
| Failure Transparency | Safe visible failures | 401/409/503/problem responses | Wiki+API | 5/5 | PASS | none |
| Familiarity | Uses established local patterns | API Platform/CQRS/AuthPayload | Wiki+code | 5/5 | PASS | none |
| Fault-Tolerance | Recovers by fresh challenges/fallback auth | replay/expiry tests, readiness gate | Wiki+tests | 5/5 | PASS | none |
| Fidelity | Browser ceremony matches real WebAuthn JSON | Chrome CTAP2 evidence | Wiki+manual | 5/5 | PASS | none |
| Flexibility | Config and API additive behavior | envs/OpenAPI diff | Wiki+config | 5/5 | PASS | none |
| Inspectability | Reviewable graph and specs | graph context, docs, generated specs | Wiki+graph | 5/5 | PASS | none |
| Installability | Dependency and setup documented | composer lock, Docker, docs | Wiki+docs | 5/5 | PASS | none |
| Integrity | Data and auth integrity | unique index, user binding, counter update | Wiki+tests | 5/5 | PASS | none |
| Interactivity | Browser ceremony operability | manual authenticator run | Wiki+manual | 5/5 | PASS | none |
| Interchangeability | Standard WebAuthn payloads | browser JSON, WebAuthn lib | Wiki+code | 5/5 | PASS | none |
| Interoperability | REST/GraphQL/WebAuthn standards | OpenAPI, GraphQL, Schemathesis | Wiki+CI | 5/5 | PASS | none |
| Intuitiveness | Predictable API sequence | docs/options-then-complete pattern | Wiki+docs | 5/5 | PASS | none |
| Learnability | Developer docs and examples | `docs/passkey-authentication.md` | Wiki+docs | 5/5 | PASS | none |
| Localizability | No new translated UI scope; API errors use existing patterns | backend-only docs and error contracts | Wiki+PRD | 5/5 | PASS | none |
| Maintainability | DDD/static/mutation quality | Deptrac/PHPInsights/Infection | Wiki+CI | 5/5 | PASS | none |
| Manageability | Runtime controls and runbook | readiness flags/command/docs | Wiki+ops | 5/5 | PASS | none |
| Mobility | Browser/device passkey standard | WebAuthn JSON/manual CTAP2 | Wiki+manual | 5/5 | PASS | none |
| Modifiability | Low coupling and clear collaborators | graph/factories/resolvers/tests | Wiki+graph | 5/5 | PASS | none |
| Modularity | Layered User bounded-context implementation | Domain/Application/Infrastructure | Wiki+code | 5/5 | PASS | none |
| Observability | Metrics/logs/runbook path | EMF endpoint metrics, rate-limit logs | Wiki+docs | 5/5 | PASS | none |
| Operability | Make targets and docs | readiness/load/CI commands | Wiki+Makefile | 5/5 | PASS | none |
| Orthogonality | Passkeys additive to existing auth | route/access tests and OpenAPI diff | Wiki+tests | 5/5 | PASS | none |
| Portability | Container/env/WebAuthn standard | Docker/envs/browser standards | Wiki+config | 5/5 | PASS | none |
| Precision | Schema and validator precision | YAML validation, OpenAPI schemas | Wiki+API | 5/5 | PASS | none |
| Predictability | Stable TTL/errors/limiting | tests for expiry/replay/rate limit | Wiki+tests | 5/5 | PASS | none |
| Process Capabilities | BMAD/CI process evidence | run summary, skills, AI review loop | Wiki+specs | 5/5 | PASS | none |
| Producibility | Reproducible generated artifacts | OpenAPI/GraphQL generation scripts | Wiki+scripts | 5/5 | PASS | none |
| Provability | Claims backed by graph/test/manual | graph context and CI rollup | Wiki+evidence | 5/5 | PASS | none |
| Recoverability | Fresh challenge/fallback/runbook | expiry/retry docs/tests | Wiki+docs | 5/5 | PASS | none |
| Redundancy | Alternative auth methods remain | password/OAuth/TOTP unaffected | Wiki+PRD | 5/5 | PASS | none |
| Relevance | Scope matches backend passkeys | PRD in/out of scope | Wiki+PRD | 5/5 | PASS | none |
| Reliability | Repeatable suites green | PHPUnit/Behat/K6/Infection/CI | Wiki+CI | 5/5 | PASS | none |
| Repairability | Modular source and runbook | specific handlers/resolvers/docs | Wiki+graph | 5/5 | PASS | none |
| Repeatability | Make/CI deterministic checks | run summary commands and GitHub checks | Wiki+CI | 5/5 | PASS | none |
| Reproducibility | Sanitized artifacts and commands | manual/load artifacts | Wiki+specs | 5/5 | PASS | none |
| Resilience | Replay/rollback/rate-limit handling | tests and manual evidence | Wiki+tests | 5/5 | PASS | none |
| Responsiveness | p99 targets met | K6 evidence | Wiki+K6 | 5/5 | PASS | none |
| Reusability | Existing session/rate-limit patterns reused | graph and source | Wiki+code | 5/5 | PASS | none |
| Robustness | Negative and edge coverage | invalid/replay/wrong-user/duplicate tests | Wiki+tests | 5/5 | PASS | none |

[truncated after 160 lines]
Verification Output

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

BMAD FR/NFR Review Gate: PASS

Review Output
STATUS: PASS
0 issues.

FR_NFR_SCORECARD: PASS
NFR_CATALOG_SCORECARD: PASS
EXPANDED_QUALITY_SCORECARD: PASS
SYSTEM_QUALITY_ATTRIBUTES_SCORECARD: PASS
WHOLE_CODEBASE_IMPACT: PASS
GRAPH_IMPACT_CONTEXT: PASS
TEST_CASE_MATRIX: PASS
AUTO_TEST_COVERAGE: PASS
FLAKY_TEST_RISK: PASS
MANUAL_TEST_EVIDENCE: PASS
QA_BEST_PRACTICES: PASS
GITHUB_COMPLETION_GATE: PASS
CI_GATE: PASS

FR_NFR_MIN_SCORE: 5/5
NFR_CATALOG_MIN_SCORE: 5/5
EXPANDED_QUALITY_MIN_SCORE: 5/5
SYSTEM_QUALITY_ATTRIBUTES_MIN_SCORE: 5/5
IMPACT_ANALYSIS_MIN_SCORE: 5/5
TEST_CASE_COVERAGE_MIN_SCORE: 5/5
AUTO_TEST_COVERAGE_MIN_SCORE: 5/5
FLAKY_TEST_RISK_MIN_SCORE: 5/5
GITHUB_COMPLETION_STATE: PASSING
GITHUB_HUMAN_APPROVAL_STATE: REVIEW_REQUIRED
CI_CHECK_ROLLUP: PASSING

Requirement Scorecard: source requirement, evidence, score, status

| Requirement | Evidence | Score | Status |
|---|---|---:|---|
| PRD FR-1 signup options | `StartPasskeySignUpCommandHandler`, `PasskeyOptionsFactory`, validation YAML, integration tests for browser-safe options and duplicate email | 5/5 | PASS |
| PRD FR-2 signup complete | `CompletePasskeySignUpCommandHandler`, `PasskeySignUpCompletionHandler`, attestation validator, rollback tests, manual browser signup | 5/5 | PASS |
| PRD FR-3 authenticated register options | `StartPasskeyRegistrationCommandHandler`, API security config, excludeCredentials tests, manual enrollment evidence | 5/5 | PASS |
| PRD FR-4 authenticated register complete | `CompletePasskeyRegistrationCommandHandler`, duplicate credential rejection, wrong-user tests, Mongo unique index evidence | 5/5 | PASS |
| PRD FR-5 signin options privacy | `StartPasskeySignInCommandHandler`, empty `allowCredentials`, known/unknown same-shape REST and GraphQL tests | 5/5 | PASS |
| PRD FR-6 signin complete | `CompletePasskeySignInCommandHandler`, `PasskeyCredentialResolver`, counter update tests, 2FA/manual evidence | 5/5 | PASS |
| PRD NFRs | Domain purity scan, XML/YAML config, TTL/readiness command, env config, CI/check rollup, manual evidence | 5/5 | PASS |
| Epics 1-5 | Planning, persistence, ceremonies, auth, docs/tests mapped in `specs/passkey-authentication/run-summary.md` | 5/5 | PASS |
| Implementation readiness | Existing API Platform/CQRS/ODM/session patterns reused; graph context inspected for related runtime and contract surfaces | 5/5 | PASS |

NFR Catalog Scorecard: every pinned NFR category with checked subdimensions, evidence or not-applicable reason, source, score, status

| Category | Checked subdimensions and evidence | Source | Score | Status |
|---|---|---|---:|---|
| Performance | Response time, throughput, concurrency, DB indexes, K6 smoke/average/stress/spike p99 thresholds; load evidence shows 100% checks and p99 below limits | PRD NFRs, `nfr-catalog-evidence.md`, NonFunctionals catalog | 5/5 | PASS |
| Usability | API ergonomics, clear RFC7807 errors, browser JSON shape, docs/examples, manual WebAuthn ceremony recovery | PRD, OpenAPI/GraphQL config, manual evidence | 5/5 | PASS |
| Maintainability | DDD layering, low coupling, tests, Psalm/Deptrac/PHP Insights/Infection, no new suppressions | PRD, architecture, CI rollup, graph context | 5/5 | PASS |
| Availability | TTL expiry, atomic claim, replay protection, prod readiness gate, 429/503 problem responses, health/CI evidence | PRD, repositories, readiness listener, tests | 5/5 | PASS |
| Interoperability | REST/GraphQL contracts, OpenAPI examples, WebAuthn library compatibility, schema checks, GraphQL Inspector/Schemathesis | PRD, API config, NonFunctionals Interoperability | 5/5 | PASS |
| Security | Authn/authz, random single-use challenges, replay resistance, privacy shape, credential uniqueness, Snyk/security scans | PRD, tests, CI, OWASP/NIST lens | 5/5 | PASS |
| Manageability | Env controls, production readiness command/listener, logs, CI evidence, documented operational gates | Architecture, run summary, graph context | 5/5 | PASS |
| Automatability | Deterministic fixtures, make/docker flows, CI, Schemathesis, K6, browser evidence recorded, no fragile manual server behavior | PRD, run summary, CI rollup | 5/5 | PASS |
| Dependability | Correctness, consistency, rollback, idempotent single-use challenges, regression/mutation/edge tests, graph-backed impact | PRD, tests, graph context | 5/5 | PASS |

Expanded Quality Scorecard: every pinned expanded quality dimension with checked subdimensions, evidence or not-applicable reason, source, score, status

| Dimension | Evidence | Score | Status |
|---|---|---:|---|
| Functional Suitability | FR-1..FR-6 implemented with REST/GraphQL, positive/negative/edge tests, manual browser ceremonies | 5/5 | PASS |
| Performance Resource Sustainability | K6 thresholds, index assertions, bounded TTL challenges, no unbounded polling/storage | 5/5 | PASS |
| Compatibility Coexistence | Backward-compatible OpenAPI diff, GraphQL Inspector, unchanged password/OAuth paths, graph impact reviewed | 5/5 | PASS |
| Interaction Capability Accessibility | Self-descriptive API errors, docs/examples, browser JSON compatibility, manual DevTools authenticator evidence | 5/5 | PASS |
| Reliability Resilience | Atomic claim, replay/expiry tests, rollback tests, prod readiness, CI pass | 5/5 | PASS |
| Security Privacy Accountability | No user enumeration beyond password parity, safe tokens, authz for enrollment, no sensitive output, audit/event reuse | 5/5 | PASS |
| Maintainability Testability | Unit/integration/Behat/Schemathesis/K6/Infection/static checks mapped; DDD boundaries enforced | 5/5 | PASS |
| Flexibility Portability | Env-driven RP/origin/timeouts, container/make flows, WebAuthn behind adapters | 5/5 | PASS |
| Safety Harm Prevention | Replay, wrong-user, duplicate, rollback, 2FA pending-session, no data-loss regressions | 5/5 | PASS |
| Data Quality Integrity | Mongo unique/lookup/TTL indexes, counter updates, user-handle consistency, race/retry coverage | 5/5 | PASS |
| Operational Excellence Releaseability | Readiness command/listener, CI green, docs/manual evidence, deployment flags | 5/5 | PASS |
| Observability Diagnosability | Problem responses, logs for limiter/readiness/session issues, CI artifacts and sanitized load logs | 5/5 | PASS |
| Supply-Chain Integrity | Lockfile changes scoped to WebAuthn stack, Snyk success, pinned actions/checks reviewed | 5/5 | PASS |
| Compliance Governance | Security/privacy checks, approval state reviewed, no unresolved threads, standards cross-checks | 5/5 | PASS |
| Sustainability Resource Impact | TTL cleanup, index-backed queries, bounded challenge lifetime and load profiles | 5/5 | PASS |
| AI Automation Governance | BMAD artifacts, graph context, review/check corroboration, low-risk status publication excluded from self-check | 5/5 | PASS |

System Quality Attributes Scorecard: every pinned Wikipedia system quality attribute with checked meaning, evidence or concrete not-applicable reason, source, score, status, improvement recommendation

Source baseline checked against the current Wikipedia notable quality-attribute list: https://en.wikipedia.org/wiki/List_of_system_quality_attributes. No pinned attribute was missing from that list.

| Attribute | Checked meaning, evidence, source | Score | Status | Improvement |
|---|---|---:|---|---|
| Accessibility | API/client accessibility via clear JSON schemas/errors; OpenAPI/manual evidence | 5/5 | PASS | none |
| Accountability | Auth/session issuance, publisher reuse, GitHub/CI traceability | 5/5 | PASS | none |
| Accuracy | WebAuthn verification, counter update, user binding tests | 5/5 | PASS | none |
| Adaptability | Env-driven RP/origins/timeouts and adapter isolation | 5/5 | PASS | none |
| Administrability | Readiness command/listener and make/CI workflows | 5/5 | PASS | none |
| Affordability | Bounded TTL/indexed queries/load profile avoid avoidable cost growth | 5/5 | PASS | none |
| Agility | DDD/CQRS small handlers, docs/spec traceability | 5/5 | PASS | none |
| Analyzability | Graph context, Psalm, Deptrac, PHP Insights, tests | 5/5 | PASS | none |
| Auditability | CI/check/manual evidence and session/event paths inspected | 5/5 | PASS | none |
| Autonomy | Automated CI/review gate behavior bounded and reviewable | 5/5 | PASS | none |
| Availability | TTL, 503 readiness, replay/expiry tests, K6 | 5/5 | PASS | none |
| Compatibility | REST/GraphQL/OpenAPI/WebAuthn compatibility checks | 5/5 | PASS | none |
| Composability | WebAuthn behind app/infra factories and resolvers | 5/5 | PASS | none |
| Confidentiality | No credential/user enumeration leakage, safe responses | 5/5 | PASS | none |
| Configurability | Passkey env vars and production flags verified | 5/5 | PASS | none |
| Convenience | Familiar API Platform patterns, docs/examples/manual flow | 5/5 | PASS | none |
| Correctness | FR tests and manual ceremonies cover success/failure | 5/5 | PASS | none |
| Credibility | CI, graph, manual, load, and GitHub evidence corroborated | 5/5 | PASS | none |
| Customizability | RP/origin/timeout config without domain changes | 5/5 | PASS | none |
| Debuggability | Problem responses, logs, CI artifacts, graph edges | 5/5 | PASS | none |
| Degradability | Readiness gate returns 503 before unsafe prod enablement | 5/5 | PASS | none |
| Determinability | Deterministic tests/fixtures and stable status evidence | 5/5 | PASS | none |
| Demonstrability | Manual browser evidence and run summary demonstrate ceremonies | 5/5 | PASS | none |
| Dependability | Atomic claims, rollback, mutation/regression evidence | 5/5 | PASS | none |
| Deployability | Env/config/docs/readiness/CI support release | 5/5 | PASS | none |
| Discoverability | OpenAPI/GraphQL docs expose endpoint contracts | 5/5 | PASS | none |
| Distributability | Container/make workflows and env isolation preserved | 5/5 | PASS | none |
| Durability | Mongo persistence/indexes and rollback tests verified | 5/5 | PASS | none |
| Effectiveness | User outcomes signup/enroll/signin achieved | 5/5 | PASS | none |
| Efficiency | K6 p99/checks and indexed DB access verified | 5/5 | PASS | none |
| Elasticity | Stateless request paths plus indexed Mongo under load | 5/5 | PASS | none |
| Evolvability | Layered adapters and docs support future WebAuthn changes | 5/5 | PASS | none |
| Extensibility | DTO/factory/resolver split supports ceremony expansion | 5/5 | PASS | none |
| Failure Transparency | RFC7807 401/409/422/429/503 behavior tested/documented | 5/5 | PASS | none |
| Familiarity | Existing API Platform/CQRS/session patterns reused | 5/5 | PASS | none |
| Fault-Tolerance | Retry/rollback/readiness/expiry failure paths covered | 5/5 | PASS | none |
| Fidelity | Browser WebAuthn JSON and virtual authenticator evidence | 5/5 | PASS | none |
| Flexibility | Configurable RP/origins/timeouts and adapters | 5/5 | PASS | none |
| Inspectability | Graph artifacts, specs, CI, manual evidence available | 5/5 | PASS | none |
| Installability | Composer/lock/docker/make evidence and CI pass | 5/5 | PASS | none |
| Integrity | Credential uniqueness, counter update, user binding | 5/5 | PASS | none |
| Interactivity | Browser ceremony request/response shapes manually verified | 5/5 | PASS | none |
| Interchangeability | WebAuthn library isolated behind validators/factories | 5/5 | PASS | none |
| Interoperability | REST/GraphQL/OpenAPI/WebAuthn contracts verified | 5/5 | PASS | none |
| Intuitiveness | API names/errors/examples match existing auth patterns | 5/5 | PASS | none |
| Learnability | Docs and OpenAPI examples cover integration | 5/5 | PASS | none |
| Localizability | No new user-facing localized message gap in scoped backend API | 5/5 | PASS | none |
| Maintainability | Static analysis, DDD boundaries, tests, docs | 5/5 | PASS | none |
| Manageability | Operational readiness flags/command/listener | 5/5 | PASS | none |
| Mobility | Browser/client portability through standard WebAuthn JSON | 5/5 | PASS | none |
| Modifiability | Handler/factory/repository separation and graph-reviewed coupling | 5/5 | PASS | none |
| Modularity | Domain/Application/Infrastructure split retained | 5/5 | PASS | none |
| Observability | Logs/problem responses/CI artifacts/load evidence | 5/5 | PASS | none |
| Operability | Make commands, readiness checks, docs, CI | 5/5 | PASS | none |
| Orthogonality | Passkey flow added without password/OAuth regression | 5/5 | PASS | none |
| Portability | Containerized execution and env config retained | 5/5 | PASS | none |
| Precision | Exact status codes/schemas/challenge matching tested | 5/5 | PASS | none |
| Predictability | Single-use/expiry semantics and deterministic CI evidence | 5/5 | PASS | none |
| Process Capabilities | BMAD/spec/skill sweep/CI process evidenced | 5/5 | PASS | none |
| Producibility | Repeatable make/docker/CI/test artifacts | 5/5 | PASS | none |
| Provability | Tests, manual evidence, graph context, GitHub checks prove behavior | 5/5 | PASS | none |
| Recoverability | Rollback and failure cleanup tests | 5/5 | PASS | none |
| Redundancy | Not introducing redundant service dependency; safe fallback gates reviewed | 5/5 | PASS | none |
| Relevance | Implementation matches scoped passkey PRD only | 5/5 | PASS | none |
| Reliability | Edge/replay/expiry/wrong-user tests and CI pass | 5/5 | PASS | none |
| Repairability | Clear module placement and diagnostics support fixes | 5/5 | PASS | none |
| Repeatability | Deterministic CI/manual rerun evidence | 5/5 | PASS | none |
| Reproducibility | Specs, commands, sanitized logs, artifacts recorded | 5/5 | PASS | none |
| Resilience | Atomic claim, TTL, rollback, production readiness | 5/5 | PASS | none |
| Responsiveness | K6 response-time thresholds satisfied | 5/5 | PASS | none |
| Reusability | Existing session/publisher/factory patterns reused | 5/5 | PASS | none |
| Robustness | Negative/edge/race/replay tests covered | 5/5 | PASS | none |
| Safety | No unsafe token issuance on invalid/replayed ceremonies | 5/5 | PASS | none |
| Scalability | Indexed lookups and load profiles verified | 5/5 | PASS | none |
| Seamlessness | REST/GraphQL auth flows integrate with existing auth payloads | 5/5 | PASS | none |
| Self-Sustainability | TTL cleanup and bounded resource use | 5/5 | PASS | none |

[truncated after 160 lines]
Verification Output

@Kravalg Kravalg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved current head e9acebc after resolving CI, OpenAPI/passkey security review findings, and BMAD FR/NFR gate status. Code Review and BMAD gates are PASS; all visible PR checks are green.

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review @cubic-dev-ai review

Current head is e9acebc1b208f7613cd20e87c28948e05177582d. All visible checks are green, Code Review and BMAD FR/NFR gates are PASS, and there are no active unresolved review threads. Please re-review the latest head.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

@Kravalg Understood — I’ll re-review the latest head e9acebc1b208f7613cd20e87c28948e05177582d for PR #286 now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 8, 2026

Copy link
Copy Markdown

@coderabbitai review @cubic-dev-ai review

Current head is e9acebc1b208f7613cd20e87c28948e05177582d. All visible checks are green, Code Review and BMAD FR/NFR gates are PASS, and there are no active unresolved review threads. Please re-review the latest head.

@Kravalg I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 0 files (changes from recent commits).

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Re-trigger cubic

@Kravalg

Kravalg commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Current head e9acebc1b208f7613cd20e87c28948e05177582d is ready for required human/code-owner review.

Status summary:

  • All visible PR checks are green.
  • Merge conflicts are resolved (mergeable=MERGEABLE).
  • Code Review status/comment: PASS.
  • BMAD FR/NFR Review Gate status/comment: PASS.
  • CodeRabbit re-review was triggered and finished for the current head.
  • Cubic re-review approved the current head with 0 issues.
  • Active unresolved review threads: 0.

Remaining branch-protection blocker: main requires 3 approvals, code-owner review, and last-push approval. Requested reviewers still pending: @Derane @pixelTM @KostiukVM @kukuruzvelt @GritsayAndriy @RudoiDmytro @bra1n2h @Vlas-Pravsha @vygaslav03.

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.

Feature: Add passkey-based authentication support for sign in and sign up

4 participants