Skip to content

Commit 1e77967

Browse files
authored
fix(core): bind WebAuthn rpId to request domain for account api (#7764)
* fix(core): bind WebAuthn rpId to request domain for account api * chore: update chageset description
1 parent 9c731fa commit 1e77967

File tree

3 files changed

+28
-16
lines changed

3 files changed

+28
-16
lines changed

.changeset/thin-walls-tap.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@logto/integration-tests": patch
3+
"@logto/core": patch
4+
---
5+
6+
fix(core): bind WebAuthn `rpId` to request domain for account api
7+
8+
- Before: WebAuthn registration via the account API always bound passkeys to the Logto default domain.
9+
- After: The `rpId` now matches the domain you use to access the API (including custom domains), consistent with the sign-in experience.

packages/core/src/routes/verification/index.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { z } from 'zod';
1414

1515
import koaGuard from '#src/middleware/koa-guard.js';
1616

17-
import { EnvSet, getTenantEndpoint } from '../../env-set/index.js';
1817
import {
1918
buildVerificationRecordByIdAndType,
2019
insertVerificationRecord,
@@ -251,6 +250,13 @@ export default function verificationRoutes<T extends UserRouter>(
251250
}
252251
);
253252

253+
/**
254+
* WebAuthn registration (passkey binding)
255+
*
256+
* The rpId must be exactly the domain from which this API is accessed.
257+
* This keeps behavior aligned with the experience flow.
258+
*
259+
*/
254260
router.post(
255261
`${verificationApiPrefix}/web-authn/registration`,
256262
koaGuard({
@@ -262,21 +268,16 @@ export default function verificationRoutes<T extends UserRouter>(
262268
status: [200],
263269
}),
264270
async (ctx, next) => {
265-
const { id: userId } = ctx.auth;
266-
267-
// If custom domain is enabled, use the custom domain as the RP ID.
268-
// Otherwise, use the default tenant hostname as the RP ID.
269-
// The background is that a passkey must be registered with a specific RP ID, which is a domain.
270-
// In the future, we will support specifying the RP ID.
271-
const domain = await queries.domains.findActiveDomain(tenantContext.id);
272-
const rpId = domain
273-
? domain.domain
274-
: getTenantEndpoint(tenantContext.id, EnvSet.values).hostname;
271+
const {
272+
auth: { id: userId },
273+
URL: { hostname },
274+
} = ctx;
275275

276276
const webAuthnVerification = WebAuthnVerification.create(libraries, queries, userId);
277277

278-
const registrationOptions =
279-
await webAuthnVerification.generateWebAuthnRegistrationOptions(rpId);
278+
const registrationOptions = await webAuthnVerification.generateWebAuthnRegistrationOptions(
279+
hostname // RP ID: Use the domain of the current API request (custom domain supported)
280+
);
280281

281282
const { expiresAt } = await insertVerificationRecord(webAuthnVerification, queries, userId);
282283

packages/integration-tests/src/tests/api/account/mfa-webauthn.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createWebAuthnRegistrationOptions,
77
verifyWebAuthnRegistration,
88
} from '#src/api/verification-record.js';
9+
import { logtoUrl } from '#src/constants.js';
910
import { expectRejects } from '#src/helpers/index.js';
1011
import {
1112
createDefaultTenantUserWithPassword,
@@ -40,7 +41,7 @@ describe('my-account (mfa - WebAuthn)', () => {
4041
await createWebAuthnRegistrationOptions(api);
4142

4243
expect(verificationRecordId).toBeTruthy();
43-
expect(registrationOptions.rp.name).toBe('localhost');
44+
expect(registrationOptions.rp.name).toBe(new URL(logtoUrl).hostname);
4445
expect(registrationOptions.user.displayName).toBe(user.username);
4546

4647
await deleteDefaultTenantUser(user.id);
@@ -60,7 +61,8 @@ describe('my-account (mfa - WebAuthn)', () => {
6061
},
6162
} = await createWebAuthnRegistrationOptions(api);
6263

63-
const rawId = Buffer.from(rpId ?? 'localhost')
64+
const expectedHost = new URL(logtoUrl).hostname;
65+
const rawId = Buffer.from(rpId ?? expectedHost)
6466
.toString('base64')
6567
.replaceAll('+', '-')
6668
.replaceAll('/', '_')
@@ -78,7 +80,7 @@ describe('my-account (mfa - WebAuthn)', () => {
7880
JSON.stringify({
7981
type: 'webauthn.create',
8082
challenge,
81-
origin: 'http://localhost:3001',
83+
origin: logtoUrl,
8284
crossOrigin: false,
8385
})
8486
).toString('base64url'),

0 commit comments

Comments
 (0)