Repository navigation
feat(admin): add an authentication coverage report - #357
Merged
Merged
Conversation
Adds the phishing_resistant_only config key, which accepts passkeys only for every account apart from the one session that verifies a new account's address. The passkey fallback rule is now enforced on each continuation endpoint rather than only in the /login method list, and decoys mirror both rules. Closes #177.
Bccorb
force-pushed
the
feat/phishing-resistant-only
branch
from
October 7, 2026 00:39
8369444 to
4e9460e
Compare
GET /admin/reports/authentication-coverage reports, for a period, how many active users hold a passkey overall, per organization and per month or week, next to the login and authenticator policy enforced now, the authenticator mix by AAGUID and completed sign-ins by method. format=csv returns the same report for pasting into an assessment or insurance response. Code sign-ins now record metadata.channel on verify_otp_success so the report can tell email codes from phone codes. Closes #178.
Bccorb
marked this pull request as ready for review
October 7, 2026 00:39
Bccorb
force-pushed
the
feat/authentication-coverage-report
branch
from
October 7, 2026 00:39
ccf9bb0 to
71b7c40
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #178. Stacked on #355 (
feat/phishing-resistant-only), because the report states the enforced policy, includingphishingResistantOnly.What it adds
GET /admin/reports/authentication-coverage(auth: 'access',requireAdmin('read')).from,toYYYY-MM-DD), both included. Default: the 90 days ending today. At most 1827 days.organizationId404if it does not exist.bucketmonth(default) orweek.formatjson(default) orcsv(text/csv,Content-Disposition: attachment).The response carries:
policy: what is enforced now.phishingResistantOnly,loginMethodsandpasskeyFallbackEnabledfromgetLoginPolicy(), and theauthenticator_policyrules (attestation, user verification, attachment, synced passkeys, require-known, allow and deny lists).coverage: active (non-revoked) users as of the end ofto, how many hold at least one WebAuthn credential, and the percentage.byOrganization: the same per organization, plus a row for users in no organization. A user in two organizations counts in both.trend: per month or week (Monday start, UTC), clipped to the period, coverage as of the end of each bucket. The last bucket equalscoverage.authenticatorMix: credentials by AAGUID, with backup-eligible and backed-up counts. Missing and all-zero AAGUIDs are grouped asnull. Names come from a short table of well-known passkey providers, then the FIDO Metadata Service when it is loaded.signInMix: completed sign-ins in the period by method (passkey,email_otp,phone_otp,otp,magic_link,totp,oauth), folded by attempt the way/internal/metrics/sign-insis, plus the phishing-resistant share.The CSV has a header block, then policy, coverage by organization, trend, authenticator mix and sign-in mix sections, each with its own header row. Rows are in a fixed order, and cells starting with
=,+,-or@are prefixed with'so a spreadsheet does not run an organization name as a formula.Small supporting change:
verify_otp_successnow recordsmetadata.channel(emailorsms, matching whatotp_successalready records), so the report can tell email codes from phone codes. Rows written before this release land inotp.Limits (also in docs/admin-operations.md)
policyis the configuration at generation time, not a history.system_config_updatedevents hold the history.policyblock says whether attestation and an allow list are enforced.Testing
verify_otp_successfolding.npm run typecheck,npm run lint,npm run format:check,npm run buildandnpm run coverage(125 files, 1662 tests) pass.openapi.jsonandsrc/generated/api.tsare regenerated.Ripple
New route, so this is contract-affecting for the adapter. Nothing is changed outside this repo.
seamless-auth-server has no catch-all proxy, so the route needs passthroughs. Following #179 (enrollment) as the template:
packages/core/src/ensureCookies.ts(route entry)packages/express/src/createServer.ts(proxyWithIdentity("admin/reports/authentication-coverage", "access", "GET"))packages/fastify/src/routes/proxyRoutes.tspackages/nextjs/src/routes/proxyRoutes.tspackages/express/tests/queryForwarding.test.js,packages/fastify/tests/queryForwarding.test.js(the query string has to be forwarded)packages/fastify/tests/parity.test.js,packages/nextjs/tests/parity.test.jsformat=csvneeds more than a passthrough entry.packages/core/src/proxyRequest.tsalways returns{ status, body: await upstream.json() }, andauthFetch's tolerantjson()turns a text body into{ message: "<csv>" }, droppingContent-TypeandContent-Disposition. The adapter needs a raw mode that forwards the body and those two headers unchanged, in core and all three framework packages. Until then the adapter can serve the JSON report and a client can build its own CSV from it.seamless-auth-admin-dashboard is the natural place to surface it, next to the enrollment page: a
useCoverageReporthook (modeled onsrc/hooks/useEnrollment.ts), a page undersrc/pages/, a route insrc/App.tsx, asrc/components/Sidebar.tsxentry, their tests and an e2e mock ine2e/mockApi.ts.The response schemas live in
src/schemas/coverageReport.tsfor now. Moving them into@seamless-auth/typeswould let the adapter and dashboard share them, and is worth doing when the dashboard work starts.