Skip to content

Commit c551284

Browse files
authored
feat(core): allow origin list for webauthn register and verify (#7424)
1 parent fe9b4b8 commit c551284

File tree

6 files changed

+25
-23
lines changed

6 files changed

+25
-23
lines changed

packages/core/src/routes/experience/classes/verifications/web-authn-verification.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,12 @@ export class WebAuthnVerification implements MfaVerificationRecord<VerificationT
117117
* Keep it as the single source of truth for generating the WebAuthn registration options.
118118
* TODO: Consider relocating the function under a shared folder
119119
*/
120-
async generateWebAuthnRegistrationOptions(
121-
ctx: WithLogContext
122-
): Promise<WebAuthnRegistrationOptions> {
123-
const { hostname } = ctx.URL;
120+
async generateWebAuthnRegistrationOptions(rpId: string): Promise<WebAuthnRegistrationOptions> {
124121
const user = await this.findUser();
125122

126123
const registrationOptions = await generateWebAuthnRegistrationOptions({
127124
user,
128-
rpId: hostname,
125+
rpId,
129126
});
130127

131128
this.registrationChallenge = registrationOptions.challenge;
@@ -146,20 +143,20 @@ export class WebAuthnVerification implements MfaVerificationRecord<VerificationT
146143
ctx: WithLogContext,
147144
payload: Omit<BindWebAuthnPayload, 'type'>
148145
) {
149-
const { hostname, origin } = ctx.URL;
150146
const {
151147
request: {
152148
headers: { 'user-agent': userAgent = '' },
153149
},
150+
origin,
154151
} = ctx;
152+
const { webauthnRelatedOrigins } = await this.queries.accountCenters.findDefaultAccountCenter();
155153

156154
assertThat(this.registrationChallenge, 'session.mfa.pending_info_not_found');
157155

158156
const { verified, registrationInfo } = await verifyWebAuthnRegistration(
159157
payload,
160158
this.registrationChallenge,
161-
hostname,
162-
origin
159+
[origin, ...webauthnRelatedOrigins]
163160
);
164161

165162
assertThat(verified, 'session.mfa.webauthn_verification_failed');

packages/core/src/routes/experience/verification-routes/web-authn-verification.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
5555
experienceInteraction.identifiedUserId
5656
);
5757

58-
const registrationOptions =
59-
await webAuthnVerification.generateWebAuthnRegistrationOptions(ctx);
58+
const registrationOptions = await webAuthnVerification.generateWebAuthnRegistrationOptions(
59+
ctx.URL.hostname
60+
);
6061

6162
experienceInteraction.setVerificationRecord(webAuthnVerification);
6263

packages/core/src/routes/interaction/utils/webauthn.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe('generateWebAuthnRegistrationOptions', () => {
5454
describe('verifyWebAuthnRegistration', () => {
5555
it('should verify registration response', async () => {
5656
await expect(
57-
verifyWebAuthnRegistration(mockBindWebAuthnPayload, 'challenge', rpId, origin)
57+
verifyWebAuthnRegistration(mockBindWebAuthnPayload, 'challenge', [origin])
5858
).resolves.toHaveProperty('verified', true);
5959
expect(verifyRegistrationResponse).toHaveBeenCalled();
6060
});

packages/core/src/routes/interaction/utils/webauthn.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,15 @@ export const generateWebAuthnRegistrationOptions = async ({
6464
export const verifyWebAuthnRegistration = async (
6565
payload: Omit<BindWebAuthnPayload, 'type'>,
6666
challenge: string,
67-
rpId: string,
68-
origin: string
67+
origins: string[]
6968
) => {
7069
const options: VerifyRegistrationResponseOpts = {
7170
response: {
7271
...payload,
7372
type: 'public-key',
7473
},
7574
expectedChallenge: challenge,
76-
expectedOrigin: origin,
77-
expectedRPID: rpId,
75+
expectedOrigin: origins,
7876
requireUserVerification: false,
7977
};
8078
return verifyRegistrationResponse(options);

packages/core/src/routes/interaction/verifications/mfa-payload-verification.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,9 @@ const verifyBindWebAuthn = async (
9292

9393
const { type, ...rest } = payload;
9494
const { challenge } = pendingMfa;
95-
const { verified, registrationInfo } = await verifyWebAuthnRegistration(
96-
rest,
97-
challenge,
98-
rpId,
99-
origin
100-
);
95+
const { verified, registrationInfo } = await verifyWebAuthnRegistration(rest, challenge, [
96+
origin,
97+
]);
10198

10299
assertThat(verified, 'session.mfa.webauthn_verification_failed');
103100
assertThat(registrationInfo, 'session.mfa.webauthn_verification_failed');

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

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

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

17-
import { EnvSet } from '../../env-set/index.js';
17+
import { EnvSet, getTenantEndpoint } from '../../env-set/index.js';
1818
import {
1919
buildVerificationRecordByIdAndType,
2020
insertVerificationRecord,
@@ -267,10 +267,19 @@ export default function verificationRoutes<T extends UserRouter>(
267267
async (ctx, next) => {
268268
const { id: userId } = ctx.auth;
269269

270+
// If custom domain is enabled, use the custom domain as the RP ID.
271+
// Otherwise, use the default tenant hostname as the RP ID.
272+
// The background is that a passkey must be registered with a specific RP ID, which is a domain.
273+
// In the future, we will support specifying the RP ID.
274+
const domain = await queries.domains.findActiveDomain(tenantContext.id);
275+
const rpId = domain
276+
? domain.domain
277+
: getTenantEndpoint(tenantContext.id, EnvSet.values).hostname;
278+
270279
const webAuthnVerification = WebAuthnVerification.create(libraries, queries, userId);
271280

272281
const registrationOptions =
273-
await webAuthnVerification.generateWebAuthnRegistrationOptions(ctx);
282+
await webAuthnVerification.generateWebAuthnRegistrationOptions(rpId);
274283

275284
const { expiresAt } = await insertVerificationRecord(webAuthnVerification, queries, userId);
276285

0 commit comments

Comments
 (0)