Skip to content

Fix passkey auth failures#552

Merged
ascorbic merged 6 commits intoemdash-cms:mainfrom
masonjames:codex/fix-passkey-auth-failures
Apr 14, 2026
Merged

Fix passkey auth failures#552
ascorbic merged 6 commits intoemdash-cms:mainfrom
masonjames:codex/fix-passkey-auth-failures

Conversation

@masonjames
Copy link
Copy Markdown
Contributor

What does this PR do?

Passkey verification currently reports expected authentication failures as 500s. For example, if a browser submits a passkey assertion for a credential that is not registered in the local EmDash database, the route throws Credential not found and the generic catch block turns it into PASSKEY_VERIFY_ERROR.

This maps expected WebAuthn assertion failures to a normal 401 response while preserving the existing 500 path for unexpected server/configuration errors. It also adds a regression test for the unregistered credential case.

Closes: N/A

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation and pnpm locale:extract has been run (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

No screenshots; API behavior only.

Validated with:

  • pnpm typecheck
  • pnpm vitest run tests/unit/auth/passkey-verify-route.test.ts
  • pnpm --silent lint:quick
  • pnpm format

Full lint note: pnpm --silent lint:json | jq '.diagnostics | length' currently reports 12 pre-existing warnings in unrelated files; this PR does not add diagnostics in the changed files.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 14, 2026

🦋 Changeset detected

Latest commit: 1fd50db

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@emdash-cms/auth Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/admin Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 14, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@552

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@552

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@552

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@552

emdash

npm i https://pkg.pr.new/emdash@552

create-emdash

npm i https://pkg.pr.new/create-emdash@552

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@552

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@552

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@552

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@552

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@552

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@552

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@552

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@552

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@552

commit: 1fd50db

@masonjames masonjames changed the title [codex] Fix passkey auth failures Fix passkey auth failures Apr 14, 2026
@masonjames masonjames marked this pull request as ready for review April 14, 2026 04:00
Copilot AI review requested due to automatic review settings April 14, 2026 04:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adjusts passkey verification error handling so expected WebAuthn/authentication failures return a normal 401 response instead of being converted into a 500 (PASSKEY_VERIFY_ERROR), and adds a regression test for the “unregistered credential” case.

Changes:

  • Map a set of known passkey authentication failure errors to 401 UNAUTHORIZED.
  • Add a unit test asserting unregistered credentials return 401 with the standard error shape.
  • Add a changeset documenting the patch release.

Reviewed changes

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

File Description
packages/core/src/astro/routes/api/auth/passkey/verify.ts Adds detection of expected passkey auth failures and returns apiError(..., 401) for those cases.
packages/core/tests/unit/auth/passkey-verify-route.test.ts Adds regression coverage for unregistered credential assertions returning 401 instead of 500.
.changeset/smart-swans-camp.md Records a patch-level release note for the passkey auth failure handling change.

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

Comment on lines +22 to +38
const EXPECTED_AUTH_FAILURE_MESSAGES = [
"Credential not found",
"Challenge not found or expired",
"Invalid challenge type",
"Challenge expired",
"Invalid client data type",
"Invalid origin:",
"Invalid RP ID hash",
"User presence not verified",
"Invalid signature counter",
"Invalid signature",
];

function isExpectedPasskeyAuthFailure(error: unknown): boolean {
return (
error instanceof Error &&
EXPECTED_AUTH_FAILURE_MESSAGES.some((message) => error.message.startsWith(message))
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The 401 mapping relies on matching error.message prefixes. This duplicates the literal throw new Error("...") strings in @emdash-cms/auth (see packages/auth/src/passkey/authenticate.ts) and will silently regress back to 500s if those messages change. Consider switching to a typed error (e.g., a dedicated PasskeyAuthError class or code field) from the auth package, or at least centralizing these prefixes in a shared export so core and auth stay in sync.

Suggested change
const EXPECTED_AUTH_FAILURE_MESSAGES = [
"Credential not found",
"Challenge not found or expired",
"Invalid challenge type",
"Challenge expired",
"Invalid client data type",
"Invalid origin:",
"Invalid RP ID hash",
"User presence not verified",
"Invalid signature counter",
"Invalid signature",
];
function isExpectedPasskeyAuthFailure(error: unknown): boolean {
return (
error instanceof Error &&
EXPECTED_AUTH_FAILURE_MESSAGES.some((message) => error.message.startsWith(message))
const EXPECTED_AUTH_FAILURE_MESSAGES = new Set([
"Credential not found",
"Challenge not found or expired",
"Invalid challenge type",
"Challenge expired",
"Invalid client data type",
"Invalid RP ID hash",
"User presence not verified",
"Invalid signature counter",
"Invalid signature",
]);
const INVALID_ORIGIN_AUTH_FAILURE_PATTERN = /^Invalid origin:\s.+$/;
function isExpectedPasskeyAuthFailure(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
return (
EXPECTED_AUTH_FAILURE_MESSAGES.has(error.message) ||
INVALID_ORIGIN_AUTH_FAILURE_PATTERN.test(error.message)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These are good points

Comment on lines +64 to +72
let user;
try {
user = await authenticateWithPasskey(passkeyConfig, adapter, body.credential, challengeStore);
} catch (error) {
if (isExpectedPasskeyAuthFailure(error)) {
return apiError("UNAUTHORIZED", "Authentication failed", 401);
}
throw error;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

let user; leaves user untyped here and requires mutation across the try/catch. Restructuring to keep const user = … within a single try block (and returning the success response from there) would be clearer and avoids accidentally widening user to any/unknown as this code evolves.

Copilot uses AI. Check for mistakes.
@masonjames
Copy link
Copy Markdown
Contributor Author

Updated to use typed passkey auth errors and added regression coverage.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missed one!

throw new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);
throw new PasskeyAuthenticationError(
"invalid_origin",
`Invalid origin: expected ${config.origin}, got ${clientData.origin}`,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
`Invalid origin: expected ${config.origin}, got ${clientData.origin}`,
"Origin mismatch",

I realise this was pre-existing, but it's probably best to keep this generic and not risk leaking details

Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

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

Thanks!

@ascorbic ascorbic merged commit f52154d into emdash-cms:main Apr 14, 2026
26 checks passed
@emdashbot emdashbot bot mentioned this pull request Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants