Skip to content

[INNO] - Passkey Directory Endpoint#7317

Open
jrmccannon wants to merge 12 commits into
mainfrom
jmccannon/inno/passkey-directory-report
Open

[INNO] - Passkey Directory Endpoint#7317
jrmccannon wants to merge 12 commits into
mainfrom
jmccannon/inno/passkey-directory-report

Conversation

@jrmccannon

@jrmccannon jrmccannon commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

Associated PRs

📔 Objective

This PR adds a new Passkey Directory Report endpoint (GET /reports/passkey-directory) that fetches and returns a list of domains supporting passkeys from the https://passkeys-api.2fa.directory/v1/all.json. The query layer (GetPasskeyDirectoryQuery) retrieves the external directory data, parses each entry for passwordless login support, MFA support, and documentation links, and caches results for 24 hours using FusionCache. The endpoint is gated behind the PasskeyDirectoryReport feature flag. This data powers a client-side report that helps organization administrators identify which of their members' credentials could be upgraded to passkeys. Unit tests cover the controller endpoint and the query's parsing/filtering logic.

📸 Screenshots

@github-actions

github-actions Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Logo
Checkmarx One – Scan Summary & Details88717a62-924e-4ff5-a1f1-f3408dfdfd30


New Issues (2) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM CSRF src/Api/Vault/Controllers/CiphersController.cs: 1558
detailsMethod at line 1558 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
Attack Vector
2 MEDIUM CSRF src/Api/Vault/Controllers/CiphersController.cs: 1385
detailsMethod at line 1385 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
Attack Vector

@codecov

codecov Bot commented Mar 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.59459% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.38%. Comparing base (eeb1c26) to head (d6704a3).

Files with missing lines Patch % Lines
...Reports/ReportFeatures/GetPasskeyDirectoryQuery.cs 92.15% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7317      +/-   ##
==========================================
+ Coverage   62.36%   62.38%   +0.02%     
==========================================
  Files        2286     2289       +3     
  Lines       99673    99743      +70     
  Branches     9003     9010       +7     
==========================================
+ Hits        62157    62223      +66     
- Misses      35344    35346       +2     
- Partials     2172     2174       +2     

☔ 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.

@jrmccannon jrmccannon added the ai-review-vnext Request a Claude code review using the vNext workflow label Mar 30, 2026
@github-actions

github-actions Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the new Passkey Directory Report feature: the GET /reports/passkey-directory endpoint, the GetPasskeyDirectoryQuery (external 2FA Directory fetch, parse, and 24h FusionCache), DI registration, and unit tests. The change is well-scoped and cleanly implemented — the endpoint is feature-flagged, the removed IGetOrganizationReportQuery/IAddOrganizationReportCommand fields were dead (unused on main) so removing them is not a breaking change, the URL is a hardcoded constant (no SSRF), and logging exposes only counts (no PII/vault data, zero-knowledge preserved). Test coverage exercises the controller mapping and the query's filtering/parsing branches.

Code Review Details

No new blocking findings. The two substantive discussion points already have open review threads:

  • 🎨 : External-API resilience — the query uses a bare AddHttpClient (default 100s timeout, no resilience handler) and passes a plain FusionCacheEntryOptions that discards configured fail-safe/timeout defaults. Since data is cached 24h and non-sensitive this is low risk, but a stale-fallback / bounded timeout would harden the external call. Tracked in the existing thread on src/Core/Dirt/Reports/ReportFeatures/GetPasskeyDirectoryQuery.cs:31.
  • ❓ : Endpoint authorization — GetPasskeyDirectoryAsync relies on class-level [Authorize("Application")] with no AccessReports check. This is defensible because the response is public, non-org-specific directory data, but the intent is worth confirming. Tracked in the existing thread on src/Api/Dirt/Controllers/ReportsController.cs:214.

@jrmccannon
jrmccannon marked this pull request as ready for review April 15, 2026 18:40
@jrmccannon
jrmccannon requested a review from a team as a code owner April 15, 2026 18:40
/// <returns>List of domains with passkey support details</returns>
[HttpGet("passkey-directory")]
[RequireFeature(FeatureFlagKeys.PasskeyDirectoryReport)]
public async Task<IEnumerable<PasskeyDirectoryResponseModel>> GetPasskeyDirectoryAsync()

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.

QUESTION: No AccessReports authorization check on this endpoint

Details

Every other endpoint in ReportsController gates access with _currentContext.AccessReports(orgId), but GetPasskeyDirectoryAsync only requires basic authentication (class-level [Authorize("Application")]). This means any authenticated Bitwarden user can call this endpoint, not just organization admins with report access.

The returned data originates from a public API, so there may be no confidentiality concern. However, the PR description states this powers a report for "organization administrators," and placing the endpoint in this controller without the same authorization pattern could be an intentional choice or an oversight.

Was the decision to skip AccessReports intentional given the data is publicly available, or should this endpoint require an orgId parameter with the standard permission check to stay consistent with the controller's authorization model?

@sonarqubecloud

Copy link
Copy Markdown

lastbestdev
lastbestdev previously approved these changes Jul 2, 2026

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

LGTM, no blocking comments

/// Gets the list of domains that support passkeys from the 2FA Directory
/// </summary>
/// <returns>List of domains with passkey support details</returns>
[HttpGet("passkey-directory")]

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.

Not critical: instead of placing the endpoint under /reports/, I'm wondering if we want to consolidate endpoints that call third party APIs elsewhere. They could share a controller with a base route like /external or similar.

Alternatively, we could follow what was done for HIBP and just introduce a new controller for 2FA directory API proxy requests, like /2fa-directory/.

It is worth noting that the Inactive 2FA report in the web client has direct calls to the 2FA directory API still, which should also be refactored to make requests through our API like this.

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.

Not critical: Where should this README live/is it appropriate to include here?

Comment thread test/Core.Test/Dirt/ReportFeatures/GetPasskeyDirectoryQueryTests.cs Dismissed
Comment thread test/Core.Test/Dirt/ReportFeatures/GetPasskeyDirectoryQueryTests.cs Dismissed
Comment thread test/Core.Test/Dirt/ReportFeatures/GetPasskeyDirectoryQueryTests.cs Dismissed
Comment thread test/Core.Test/Dirt/ReportFeatures/GetPasskeyDirectoryQueryTests.cs Dismissed
var entries = await cache.GetOrSetAsync(
key: _cacheKey,
factory: async _ => await FetchPasskeyDirectoryAsync(),
options: new FusionCacheEntryOptions(duration: _cacheDuration)

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.

🎨 SUGGESTED: Passing a bare FusionCacheEntryOptions discards the configured fail-safe/timeout defaults for this external-API call.

Details and fix

AddReportingServices registers this cache via AddExtendedCache, which configures resilience defaults through WithDefaultEntryOptions — notably IsFailSafeEnabled = true, FactorySoftTimeout, and FactoryHardTimeout (see GlobalSettings.ExtendedCacheSettings).

Constructing new FusionCacheEntryOptions(duration: _cacheDuration) replaces those defaults for this call rather than merging, so fail-safe is disabled here. Because the factory (FetchPasskeyDirectoryAsync) calls a third-party API and does EnsureSuccessStatusCode(), a transient outage on a cold/expired cache now surfaces as a 500 to the caller, whereas fail-safe would let a slightly-stale cached copy be served.

The repo's CACHING.md documents the duration-only-override pattern, which starts from a duplicate of the default entry options:

var entries = await cache.GetOrSetAsync(
    key: _cacheKey,
    factory: async _ => await FetchPasskeyDirectoryAsync(),
    options => options.SetDuration(_cacheDuration)
);

This keeps the 24h duration while preserving fail-safe and factory timeouts. Reference: src/Core/Utilities/CACHING.md.

@lastbestdev lastbestdev added the t:feature Change Type - Feature Development label Jul 17, 2026
@lastbestdev
lastbestdev enabled auto-merge (squash) July 17, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review-vnext Request a Claude code review using the vNext workflow t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants