mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(core,schemas): bind webauthn (#4626)
This commit is contained in:
parent
9428d37a5e
commit
af246ad863
36 changed files with 1052 additions and 101 deletions
|
@ -42,6 +42,7 @@
|
|||
"@logto/schemas": "workspace:^1.10.0",
|
||||
"@logto/shared": "workspace:^3.0.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"@simplewebauthn/server": "^8.2.0",
|
||||
"@withtyped/client": "^0.7.22",
|
||||
"chalk": "^5.0.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import { MfaFactor, type BindMfa } from '@logto/schemas';
|
||||
import { MfaFactor, type BindMfa, type BindWebAuthn } from '@logto/schemas';
|
||||
|
||||
export const mockTotpBind: BindMfa = {
|
||||
type: MfaFactor.TOTP,
|
||||
secret: 'secret',
|
||||
};
|
||||
|
||||
export const mockWebAuthnBind: BindWebAuthn = {
|
||||
type: MfaFactor.WebAuthn,
|
||||
credentialId: 'credentialId',
|
||||
publicKey: 'publicKey',
|
||||
counter: 0,
|
||||
agent: 'agent',
|
||||
transports: [],
|
||||
};
|
||||
|
|
20
packages/core/src/__mocks__/webauthn.ts
Normal file
20
packages/core/src/__mocks__/webauthn.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type WebAuthnRegistrationOptions } from '@logto/schemas';
|
||||
|
||||
export const mockWebAuthnCreationOptions: WebAuthnRegistrationOptions = {
|
||||
rp: {
|
||||
name: 'Logto',
|
||||
id: 'logto.io',
|
||||
},
|
||||
user: {
|
||||
id: 'id',
|
||||
name: 'test-user',
|
||||
displayName: 'Test User',
|
||||
},
|
||||
challenge: 'challenge',
|
||||
pubKeyCredParams: [
|
||||
{
|
||||
type: 'public-key',
|
||||
alg: -7,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,246 @@
|
|||
import { InteractionEvent, MfaFactor, adminTenantId } from '@logto/schemas';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
import type Provider from 'oidc-provider';
|
||||
|
||||
import { mockWebAuthnBind } from '#src/__mocks__/mfa-verification.js';
|
||||
import { createMockLogContext } from '#src/test-utils/koa-audit-log.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||
|
||||
import type {
|
||||
Identifier,
|
||||
VerifiedRegisterInteractionResult,
|
||||
VerifiedSignInInteractionResult,
|
||||
} from '../types/index.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const { mockEsm } = createMockUtils(jest);
|
||||
|
||||
const getLogtoConnectorById = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ metadata: { target: 'logto' }, dbEntry: { syncProfile: true } });
|
||||
|
||||
const { assignInteractionResults } = mockEsm('#src/libraries/session.js', () => ({
|
||||
assignInteractionResults: jest.fn(),
|
||||
}));
|
||||
|
||||
mockEsm('#src/libraries/user.js', () => ({
|
||||
encryptUserPassword: jest.fn().mockResolvedValue({
|
||||
passwordEncrypted: 'passwordEncrypted',
|
||||
passwordEncryptionMethod: 'plain',
|
||||
}),
|
||||
}));
|
||||
|
||||
mockEsm('@logto/shared', () => ({
|
||||
generateStandardId: jest.fn().mockReturnValue('uid'),
|
||||
}));
|
||||
|
||||
mockEsm('#src/utils/tenant.js', () => ({
|
||||
getTenantId: () => adminTenantId,
|
||||
}));
|
||||
|
||||
const userQueries = {
|
||||
findUserById: jest.fn().mockResolvedValue({
|
||||
identities: { google: { userId: 'googleId', details: {} } },
|
||||
mfaVerifications: [],
|
||||
}),
|
||||
updateUserById: jest.fn(),
|
||||
hasActiveUsers: jest.fn().mockResolvedValue(true),
|
||||
hasUserWithEmail: jest.fn().mockResolvedValue(false),
|
||||
hasUserWithPhone: jest.fn().mockResolvedValue(false),
|
||||
};
|
||||
|
||||
const { hasActiveUsers, updateUserById } = userQueries;
|
||||
|
||||
const userLibraries = { generateUserId: jest.fn().mockResolvedValue('uid'), insertUser: jest.fn() };
|
||||
const { generateUserId, insertUser } = userLibraries;
|
||||
|
||||
const submitInteraction = await pickDefault(import('./submit-interaction.js'));
|
||||
const now = Date.now();
|
||||
|
||||
jest.useFakeTimers().setSystemTime(now);
|
||||
|
||||
describe('submit action', () => {
|
||||
const tenant = new MockTenant(
|
||||
undefined,
|
||||
{ users: userQueries, signInExperiences: { updateDefaultSignInExperience: jest.fn() } },
|
||||
{ getLogtoConnectorById },
|
||||
{ users: userLibraries }
|
||||
);
|
||||
const ctx = {
|
||||
...createContextWithRouteParameters(),
|
||||
...createMockLogContext(),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
interactionDetails: { params: {} } as Awaited<ReturnType<Provider['interactionDetails']>>,
|
||||
assignInteractionHookResult: jest.fn(),
|
||||
};
|
||||
const profile = {
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
phone: '123456',
|
||||
email: 'email@logto.io',
|
||||
connectorId: 'logto',
|
||||
};
|
||||
|
||||
const userInfo = {
|
||||
id: 'foo',
|
||||
name: 'foo_social',
|
||||
avatar: 'avatar',
|
||||
email: 'email@socail.com',
|
||||
phone: '123123',
|
||||
};
|
||||
|
||||
const identifiers: Identifier[] = [
|
||||
{
|
||||
key: 'social',
|
||||
connectorId: 'logto',
|
||||
userInfo,
|
||||
},
|
||||
];
|
||||
|
||||
const upsertProfile = {
|
||||
username: 'username',
|
||||
primaryPhone: '123456',
|
||||
primaryEmail: 'email@logto.io',
|
||||
passwordEncrypted: 'passwordEncrypted',
|
||||
passwordEncryptionMethod: 'plain',
|
||||
identities: {
|
||||
logto: { userId: userInfo.id, details: userInfo },
|
||||
},
|
||||
name: userInfo.name,
|
||||
avatar: userInfo.avatar,
|
||||
lastSignInAt: now,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('register with bindMfa', () => {
|
||||
it('should handle totp', async () => {
|
||||
const interaction: VerifiedRegisterInteractionResult = {
|
||||
event: InteractionEvent.Register,
|
||||
profile,
|
||||
identifiers,
|
||||
bindMfa: { type: MfaFactor.TOTP, secret: 'secret' },
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
expect(generateUserId).toBeCalled();
|
||||
expect(hasActiveUsers).not.toBeCalled();
|
||||
|
||||
expect(insertUser).toBeCalledWith(
|
||||
{
|
||||
id: 'uid',
|
||||
mfaVerifications: [
|
||||
{
|
||||
type: MfaFactor.TOTP,
|
||||
key: 'secret',
|
||||
id: 'uid',
|
||||
createdAt: new Date(now).toISOString(),
|
||||
},
|
||||
],
|
||||
...upsertProfile,
|
||||
},
|
||||
['user']
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle webauthn', async () => {
|
||||
const interaction: VerifiedRegisterInteractionResult = {
|
||||
event: InteractionEvent.Register,
|
||||
profile,
|
||||
identifiers,
|
||||
bindMfa: mockWebAuthnBind,
|
||||
pendingAccountId: 'id',
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
expect(generateUserId).not.toBeCalled();
|
||||
expect(hasActiveUsers).not.toBeCalled();
|
||||
|
||||
expect(insertUser).toBeCalledWith(
|
||||
{
|
||||
id: 'id',
|
||||
mfaVerifications: [
|
||||
{
|
||||
...mockWebAuthnBind,
|
||||
id: 'uid',
|
||||
createdAt: new Date(now).toISOString(),
|
||||
},
|
||||
],
|
||||
...upsertProfile,
|
||||
},
|
||||
['user']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sign in with bindMfa', () => {
|
||||
it('should handle totp', async () => {
|
||||
getLogtoConnectorById.mockResolvedValueOnce({
|
||||
metadata: { target: 'logto' },
|
||||
dbEntry: { syncProfile: false },
|
||||
});
|
||||
const interaction: VerifiedSignInInteractionResult = {
|
||||
event: InteractionEvent.SignIn,
|
||||
accountId: 'foo',
|
||||
identifiers,
|
||||
bindMfa: {
|
||||
type: MfaFactor.TOTP,
|
||||
secret: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
|
||||
expect(getLogtoConnectorById).toBeCalledWith('logto');
|
||||
|
||||
expect(updateUserById).toBeCalledWith('foo', {
|
||||
mfaVerifications: [
|
||||
{
|
||||
type: MfaFactor.TOTP,
|
||||
key: 'secret',
|
||||
id: 'uid',
|
||||
createdAt: new Date(now).toISOString(),
|
||||
},
|
||||
],
|
||||
lastSignInAt: now,
|
||||
});
|
||||
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
|
||||
login: { accountId: 'foo' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle webauthn', async () => {
|
||||
getLogtoConnectorById.mockResolvedValueOnce({
|
||||
metadata: { target: 'logto' },
|
||||
dbEntry: { syncProfile: false },
|
||||
});
|
||||
const interaction: VerifiedSignInInteractionResult = {
|
||||
event: InteractionEvent.SignIn,
|
||||
accountId: 'foo',
|
||||
identifiers,
|
||||
bindMfa: mockWebAuthnBind,
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
|
||||
expect(getLogtoConnectorById).toBeCalledWith('logto');
|
||||
|
||||
expect(updateUserById).toBeCalledWith('foo', {
|
||||
mfaVerifications: [
|
||||
{
|
||||
id: 'uid',
|
||||
createdAt: new Date(now).toISOString(),
|
||||
...mockWebAuthnBind,
|
||||
},
|
||||
],
|
||||
lastSignInAt: now,
|
||||
});
|
||||
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
|
||||
login: { accountId: 'foo' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
InteractionEvent,
|
||||
MfaFactor,
|
||||
adminConsoleApplicationId,
|
||||
adminTenantId,
|
||||
} from '@logto/schemas';
|
||||
import { InteractionEvent, adminConsoleApplicationId, adminTenantId } from '@logto/schemas';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
import type Provider from 'oidc-provider';
|
||||
|
||||
|
@ -147,6 +142,33 @@ describe('submit action', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('register and use pendingAccountId', async () => {
|
||||
const interaction: VerifiedRegisterInteractionResult = {
|
||||
event: InteractionEvent.Register,
|
||||
profile,
|
||||
identifiers,
|
||||
pendingAccountId: 'pending-account-id',
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
|
||||
expect(generateUserId).not.toBeCalled();
|
||||
expect(hasActiveUsers).not.toBeCalled();
|
||||
expect(encryptUserPassword).toBeCalledWith('password');
|
||||
expect(getLogtoConnectorById).toBeCalledWith('logto');
|
||||
|
||||
expect(insertUser).toBeCalledWith(
|
||||
{
|
||||
id: 'pending-account-id',
|
||||
...upsertProfile,
|
||||
},
|
||||
['user']
|
||||
);
|
||||
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
|
||||
login: { accountId: 'pending-account-id' },
|
||||
});
|
||||
});
|
||||
|
||||
it('register new social user', async () => {
|
||||
const interaction: VerifiedRegisterInteractionResult = {
|
||||
event: InteractionEvent.Register,
|
||||
|
@ -210,35 +232,6 @@ describe('submit action', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('register with bindMfa', async () => {
|
||||
const interaction: VerifiedRegisterInteractionResult = {
|
||||
event: InteractionEvent.Register,
|
||||
profile,
|
||||
identifiers,
|
||||
bindMfa: { type: MfaFactor.TOTP, secret: 'secret' },
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
expect(generateUserId).toBeCalled();
|
||||
expect(hasActiveUsers).not.toBeCalled();
|
||||
|
||||
expect(insertUser).toBeCalledWith(
|
||||
{
|
||||
id: 'uid',
|
||||
mfaVerifications: [
|
||||
{
|
||||
type: MfaFactor.TOTP,
|
||||
key: 'secret',
|
||||
id: 'uid',
|
||||
createdAt: new Date(now).toISOString(),
|
||||
},
|
||||
],
|
||||
...upsertProfile,
|
||||
},
|
||||
['user']
|
||||
);
|
||||
});
|
||||
|
||||
it('admin user register', async () => {
|
||||
hasActiveUsers.mockResolvedValueOnce(false);
|
||||
const adminConsoleCtx = {
|
||||
|
@ -308,41 +301,6 @@ describe('submit action', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('sign-in with bindMfa', async () => {
|
||||
getLogtoConnectorById.mockResolvedValueOnce({
|
||||
metadata: { target: 'logto' },
|
||||
dbEntry: { syncProfile: false },
|
||||
});
|
||||
const interaction: VerifiedSignInInteractionResult = {
|
||||
event: InteractionEvent.SignIn,
|
||||
accountId: 'foo',
|
||||
identifiers,
|
||||
bindMfa: {
|
||||
type: MfaFactor.TOTP,
|
||||
secret: 'secret',
|
||||
},
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
|
||||
expect(getLogtoConnectorById).toBeCalledWith('logto');
|
||||
|
||||
expect(updateUserById).toBeCalledWith('foo', {
|
||||
mfaVerifications: [
|
||||
{
|
||||
type: MfaFactor.TOTP,
|
||||
key: 'secret',
|
||||
id: 'uid',
|
||||
createdAt: new Date(now).toISOString(),
|
||||
},
|
||||
],
|
||||
lastSignInAt: now,
|
||||
});
|
||||
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
|
||||
login: { accountId: 'foo' },
|
||||
});
|
||||
});
|
||||
|
||||
it('sign-in and sync new Social', async () => {
|
||||
getLogtoConnectorById.mockResolvedValueOnce({
|
||||
metadata: { target: 'logto' },
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
adminTenantId,
|
||||
InteractionEvent,
|
||||
adminConsoleApplicationId,
|
||||
MfaFactor,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { conditional, conditionalArray, trySafe } from '@silverhand/essentials';
|
||||
|
@ -41,12 +42,23 @@ const parseBindMfa = ({
|
|||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
type: bindMfa.type,
|
||||
key: bindMfa.secret,
|
||||
id: generateStandardId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
if (bindMfa.type === MfaFactor.TOTP) {
|
||||
return {
|
||||
type: MfaFactor.TOTP,
|
||||
key: bindMfa.secret,
|
||||
id: generateStandardId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (bindMfa.type === MfaFactor.WebAuthn) {
|
||||
return {
|
||||
...bindMfa,
|
||||
id: generateStandardId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialUserRoles = (
|
||||
|
@ -76,7 +88,8 @@ export default async function submitInteraction(
|
|||
const { event, profile } = interaction;
|
||||
|
||||
if (event === InteractionEvent.Register) {
|
||||
const id = await generateUserId();
|
||||
const { pendingAccountId } = interaction;
|
||||
const id = pendingAccountId ?? (await generateUserId());
|
||||
const userProfile = await parseUserProfile(tenantContext, interaction);
|
||||
const mfaVerification = parseBindMfa(interaction);
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { demoAppApplicationId, InteractionEvent } from '@logto/schemas';
|
||||
import { demoAppApplicationId, InteractionEvent, MfaFactor } from '@logto/schemas';
|
||||
import { createMockUtils } from '@logto/shared/esm';
|
||||
|
||||
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
|
||||
import { mockUser } from '#src/__mocks__/user.js';
|
||||
import { mockWebAuthnCreationOptions } from '#src/__mocks__/webauthn.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type koaAuditLog from '#src/middleware/koa-audit-log.js';
|
||||
import { createMockLogContext } from '#src/test-utils/koa-audit-log.js';
|
||||
|
@ -52,6 +54,21 @@ const { sendVerificationCodeToIdentifier } = await mockEsmWithActual(
|
|||
})
|
||||
);
|
||||
|
||||
const { generateWebAuthnRegistrationOptions } = await mockEsmWithActual(
|
||||
'./utils/webauthn.js',
|
||||
() => ({
|
||||
generateWebAuthnRegistrationOptions: jest.fn().mockResolvedValue(mockWebAuthnCreationOptions),
|
||||
})
|
||||
);
|
||||
|
||||
const { verifyIdentifier, verifyProfile } = await mockEsmWithActual(
|
||||
'./verifications/index.js',
|
||||
() => ({
|
||||
verifyIdentifier: jest.fn().mockResolvedValue({}),
|
||||
verifyProfile: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const { createLog, prependAllLogEntries } = createMockLogContext();
|
||||
|
||||
await mockEsmWithActual(
|
||||
|
@ -79,6 +96,9 @@ const tenantContext = new MockTenant(
|
|||
signInExperiences: {
|
||||
findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||
},
|
||||
users: {
|
||||
findUserById: jest.fn().mockResolvedValue(mockUser),
|
||||
},
|
||||
},
|
||||
{
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
|
@ -94,6 +114,11 @@ const tenantContext = new MockTenant(
|
|||
// @ts-expect-error
|
||||
return connector as LogtoConnector;
|
||||
},
|
||||
},
|
||||
{
|
||||
users: {
|
||||
generateUserId: jest.fn().mockReturnValue('generated-id'),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -197,4 +222,67 @@ describe('interaction routes', () => {
|
|||
expect(response.body).toHaveProperty('secretQrCode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /verification/webauthn-registration', () => {
|
||||
const path = `${interactionPrefix}/${verificationPath}/webauthn-registration`;
|
||||
|
||||
afterEach(() => {
|
||||
getInteractionStorage.mockClear();
|
||||
});
|
||||
|
||||
it('should return WebAuthn options for new user', async () => {
|
||||
getInteractionStorage.mockReturnValue({
|
||||
event: InteractionEvent.Register,
|
||||
});
|
||||
verifyIdentifier.mockResolvedValueOnce({
|
||||
event: InteractionEvent.Register,
|
||||
});
|
||||
verifyProfile.mockResolvedValueOnce({
|
||||
event: InteractionEvent.Register,
|
||||
});
|
||||
const response = await sessionRequest.post(path).send();
|
||||
expect(generateWebAuthnRegistrationOptions).toBeCalled();
|
||||
expect(storeInteractionResult).toBeCalledWith(
|
||||
{
|
||||
pendingMfa: {
|
||||
type: MfaFactor.WebAuthn,
|
||||
challenge: mockWebAuthnCreationOptions.challenge,
|
||||
},
|
||||
pendingAccountId: 'generated-id',
|
||||
},
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toMatchObject(mockWebAuthnCreationOptions);
|
||||
});
|
||||
|
||||
it('should return WebAuthn options for existing user', async () => {
|
||||
getInteractionStorage.mockReturnValue({
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
verifyIdentifier.mockResolvedValueOnce({
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
verifyProfile.mockResolvedValueOnce({
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
const response = await sessionRequest.post(path).send();
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(generateWebAuthnRegistrationOptions).toBeCalled();
|
||||
expect(storeInteractionResult).toBeCalledWith(
|
||||
{
|
||||
pendingMfa: {
|
||||
type: MfaFactor.WebAuthn,
|
||||
challenge: mockWebAuthnCreationOptions.challenge,
|
||||
},
|
||||
},
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
expect(response.body).toMatchObject(mockWebAuthnCreationOptions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,25 +1,52 @@
|
|||
import { MfaFactor, requestVerificationCodePayloadGuard } from '@logto/schemas';
|
||||
import {
|
||||
InteractionEvent,
|
||||
MfaFactor,
|
||||
requestVerificationCodePayloadGuard,
|
||||
webAuthnRegistrationOptionsGuard,
|
||||
} from '@logto/schemas';
|
||||
import type Router from 'koa-router';
|
||||
import { type IRouterParamContext } from 'koa-router';
|
||||
import qrcode from 'qrcode';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { parseUserProfile } from './actions/helpers.js';
|
||||
import { interactionPrefix, verificationPath } from './const.js';
|
||||
import type { WithInteractionDetailsContext } from './middleware/koa-interaction-details.js';
|
||||
import { socialAuthorizationUrlPayloadGuard } from './types/guard.js';
|
||||
import { getInteractionStorage, storeInteractionResult } from './utils/interaction.js';
|
||||
import {
|
||||
getInteractionStorage,
|
||||
isRegisterInteractionResult,
|
||||
isSignInInteractionResult,
|
||||
storeInteractionResult,
|
||||
} from './utils/interaction.js';
|
||||
import { createSocialAuthorizationUrl } from './utils/social-verification.js';
|
||||
import { generateTotpSecret } from './utils/totp-validation.js';
|
||||
import { sendVerificationCodeToIdentifier } from './utils/verification-code-validation.js';
|
||||
import { generateWebAuthnRegistrationOptions } from './utils/webauthn.js';
|
||||
import { verifyIdentifier } from './verifications/index.js';
|
||||
import verifyProfile from './verifications/profile-verification.js';
|
||||
|
||||
export default function additionalRoutes<T extends IRouterParamContext>(
|
||||
router: Router<unknown, WithInteractionDetailsContext<WithLogContext<T>>>,
|
||||
tenant: TenantContext
|
||||
) {
|
||||
const {
|
||||
provider,
|
||||
libraries: {
|
||||
users: { generateUserId },
|
||||
passcodes,
|
||||
},
|
||||
queries: {
|
||||
users: { findUserById },
|
||||
},
|
||||
} = tenant;
|
||||
|
||||
// Create social authorization url interaction verification
|
||||
router.post(
|
||||
`${interactionPrefix}/${verificationPath}/social-authorization-uri`,
|
||||
|
@ -63,7 +90,7 @@ export default function additionalRoutes<T extends IRouterParamContext>(
|
|||
{ event, ...guard.body },
|
||||
interactionDetails.jti,
|
||||
createLog,
|
||||
tenant.libraries.passcodes
|
||||
passcodes
|
||||
);
|
||||
|
||||
ctx.status = 204;
|
||||
|
@ -92,7 +119,7 @@ export default function additionalRoutes<T extends IRouterParamContext>(
|
|||
await storeInteractionResult(
|
||||
{ pendingMfa: { type: MfaFactor.TOTP, secret } },
|
||||
ctx,
|
||||
tenant.provider,
|
||||
provider,
|
||||
true
|
||||
);
|
||||
|
||||
|
@ -104,4 +131,86 @@ export default function additionalRoutes<T extends IRouterParamContext>(
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${interactionPrefix}/${verificationPath}/webauthn-registration`,
|
||||
koaGuard({
|
||||
status: [200],
|
||||
response: webAuthnRegistrationOptionsGuard,
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { interactionDetails, createLog } = ctx;
|
||||
// Check interaction exists
|
||||
const interaction = getInteractionStorage(interactionDetails.result);
|
||||
const { event } = interaction;
|
||||
assertThat(
|
||||
event !== InteractionEvent.ForgotPassword,
|
||||
'session.not_supported_for_forgot_password'
|
||||
);
|
||||
createLog(`Interaction.${event}.BindMfa.WebAuthn.Create`);
|
||||
|
||||
// WebAuthn requires user id and name, so we need to verify and get profile first
|
||||
const accountVerifiedInteraction = await verifyIdentifier(ctx, tenant, interaction);
|
||||
const profileVerifiedInteraction = await verifyProfile(tenant, accountVerifiedInteraction);
|
||||
|
||||
if (isRegisterInteractionResult(profileVerifiedInteraction)) {
|
||||
const newAccountId = await generateUserId();
|
||||
const newUserProfile = await parseUserProfile(tenant, profileVerifiedInteraction);
|
||||
const options = await generateWebAuthnRegistrationOptions({
|
||||
rpId: EnvSet.values.endpoint.hostname,
|
||||
user: {
|
||||
id: newAccountId,
|
||||
username: newUserProfile.username ?? newAccountId,
|
||||
primaryEmail: newUserProfile.primaryEmail ?? null,
|
||||
primaryPhone: newUserProfile.primaryPhone ?? null,
|
||||
mfaVerifications: [],
|
||||
},
|
||||
});
|
||||
|
||||
await storeInteractionResult(
|
||||
{
|
||||
pendingMfa: { type: MfaFactor.WebAuthn, challenge: options.challenge },
|
||||
pendingAccountId: newAccountId,
|
||||
},
|
||||
ctx,
|
||||
provider,
|
||||
true
|
||||
);
|
||||
|
||||
ctx.body = options;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
if (isSignInInteractionResult(profileVerifiedInteraction)) {
|
||||
const { accountId } = profileVerifiedInteraction;
|
||||
const { id, username, primaryEmail, primaryPhone, mfaVerifications } = await findUserById(
|
||||
accountId
|
||||
);
|
||||
const options = await generateWebAuthnRegistrationOptions({
|
||||
rpId: EnvSet.values.endpoint.hostname,
|
||||
user: {
|
||||
id,
|
||||
username,
|
||||
primaryEmail,
|
||||
primaryPhone,
|
||||
mfaVerifications,
|
||||
},
|
||||
});
|
||||
|
||||
await storeInteractionResult(
|
||||
{
|
||||
pendingMfa: { type: MfaFactor.WebAuthn, challenge: options.challenge },
|
||||
},
|
||||
ctx,
|
||||
provider,
|
||||
true
|
||||
);
|
||||
|
||||
ctx.body = options;
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { InteractionEvent, bindMfaPayloadGuard, verifyMfaPayloadGuard } from '@l
|
|||
import type Router from 'koa-router';
|
||||
import { type IRouterParamContext } from 'koa-router';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
@ -35,7 +36,14 @@ export default function mfaRoutes<T extends IRouterParamContext>(
|
|||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
const bindMfaPayload = ctx.guard.body;
|
||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||
const {
|
||||
signInExperience,
|
||||
interactionDetails,
|
||||
createLog,
|
||||
request: {
|
||||
headers: { 'user-agent': userAgent = '' },
|
||||
},
|
||||
} = ctx;
|
||||
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
||||
|
||||
const log = createLog(`Interaction.${interactionStorage.event}.BindMfa.Totp.Submit`);
|
||||
|
@ -44,7 +52,12 @@ export default function mfaRoutes<T extends IRouterParamContext>(
|
|||
verifyMfaSettings(bindMfaPayload.type, signInExperience);
|
||||
}
|
||||
|
||||
const bindMfa = await bindMfaPayloadVerification(ctx, bindMfaPayload, interactionStorage);
|
||||
const { hostname, origin } = EnvSet.values.endpoint;
|
||||
const bindMfa = await bindMfaPayloadVerification(ctx, bindMfaPayload, interactionStorage, {
|
||||
rpId: hostname,
|
||||
userAgent,
|
||||
origin,
|
||||
});
|
||||
|
||||
log.append({ bindMfa, interactionStorage });
|
||||
|
||||
|
|
|
@ -56,6 +56,9 @@ export const anonymousInteractionResultGuard = z.object({
|
|||
pendingMfa: pendingMfaGuard.optional(),
|
||||
// The verified mfa
|
||||
verifiedMfa: verifyMfaResultGuard.optional(),
|
||||
// The user id to be used for register, if not provided, a new one will be generated
|
||||
// WebAuthn requires a user id to be provided, so we have to generate and know it before submit interaction
|
||||
pendingAccountId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const forgotPasswordProfileGuard = z.object({
|
||||
|
|
|
@ -79,6 +79,7 @@ export type VerifiedRegisterInteractionResult = {
|
|||
profile?: Profile;
|
||||
identifiers?: Identifier[];
|
||||
bindMfa?: BindMfa;
|
||||
pendingAccountId?: string;
|
||||
};
|
||||
|
||||
export type VerifiedSignInInteractionResult = {
|
||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
|||
VerifiedInteractionResult,
|
||||
RegisterInteractionResult,
|
||||
AccountVerifiedInteractionResult,
|
||||
VerifiedRegisterInteractionResult,
|
||||
} from '../types/index.js';
|
||||
|
||||
const isProfileIdentifier = (identifier: Identifier, profile?: Profile) => {
|
||||
|
@ -88,6 +89,11 @@ export const isForgotPasswordInteractionResult = (
|
|||
): interaction is VerifiedForgotPasswordInteractionResult =>
|
||||
interaction.event === InteractionEvent.ForgotPassword;
|
||||
|
||||
export const isRegisterInteractionResult = (
|
||||
interaction: VerifiedInteractionResult
|
||||
): interaction is VerifiedRegisterInteractionResult =>
|
||||
interaction.event === InteractionEvent.Register;
|
||||
|
||||
export const isSignInInteractionResult = (
|
||||
interaction: RegisterInteractionResult | AccountVerifiedInteractionResult
|
||||
): interaction is AccountVerifiedInteractionResult => interaction.event === InteractionEvent.SignIn;
|
||||
|
|
68
packages/core/src/routes/interaction/utils/webauthn.ts
Normal file
68
packages/core/src/routes/interaction/utils/webauthn.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import {
|
||||
type BindWebAuthnPayload,
|
||||
MfaFactor,
|
||||
type MfaVerificationWebAuthn,
|
||||
type User,
|
||||
type WebAuthnRegistrationOptions,
|
||||
} from '@logto/schemas';
|
||||
import {
|
||||
type GenerateRegistrationOptionsOpts,
|
||||
generateRegistrationOptions,
|
||||
verifyRegistrationResponse,
|
||||
type VerifyRegistrationResponseOpts,
|
||||
} from '@simplewebauthn/server';
|
||||
|
||||
type GenerateWebAuthnRegistrationOptionsParameters = {
|
||||
rpId: string;
|
||||
user: Pick<User, 'id' | 'username' | 'primaryEmail' | 'primaryPhone' | 'mfaVerifications'>;
|
||||
};
|
||||
|
||||
export const generateWebAuthnRegistrationOptions = async ({
|
||||
rpId,
|
||||
user,
|
||||
}: GenerateWebAuthnRegistrationOptionsParameters): Promise<WebAuthnRegistrationOptions> => {
|
||||
const options: GenerateRegistrationOptionsOpts = {
|
||||
rpName: rpId,
|
||||
rpID: rpId,
|
||||
userID: user.id,
|
||||
userName: user.username ?? user.primaryEmail ?? user.primaryPhone ?? user.id,
|
||||
timeout: 60_000,
|
||||
attestationType: 'none',
|
||||
excludeCredentials: user.mfaVerifications
|
||||
.filter(
|
||||
(verification): verification is MfaVerificationWebAuthn =>
|
||||
verification.type === MfaFactor.WebAuthn
|
||||
)
|
||||
.map(({ credentialId, transports }) => ({
|
||||
id: Uint8Array.from(Buffer.from(credentialId, 'base64')),
|
||||
type: 'public-key',
|
||||
transports,
|
||||
})),
|
||||
authenticatorSelection: {
|
||||
residentKey: 'discouraged',
|
||||
},
|
||||
// Values for COSEALG.ES256, COSEALG.RS256, Node.js don't have those enums
|
||||
supportedAlgorithmIDs: [-7, -257],
|
||||
};
|
||||
|
||||
return generateRegistrationOptions(options);
|
||||
};
|
||||
|
||||
export const verifyWebAuthnRegistration = async (
|
||||
payload: Omit<BindWebAuthnPayload, 'type'>,
|
||||
challenge: string,
|
||||
rpId: string,
|
||||
origin: string
|
||||
) => {
|
||||
const options: VerifyRegistrationResponseOpts = {
|
||||
response: {
|
||||
...payload,
|
||||
type: 'public-key',
|
||||
},
|
||||
expectedChallenge: challenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: rpId,
|
||||
requireUserVerification: true,
|
||||
};
|
||||
return verifyRegistrationResponse(options);
|
||||
};
|
|
@ -28,6 +28,12 @@ const { bindMfaPayloadVerification, verifyMfaPayloadVerification } = await impor
|
|||
'./mfa-payload-verification.js'
|
||||
);
|
||||
|
||||
const additionalParameters = {
|
||||
rpId: 'logto.io',
|
||||
userAgent: 'userAgent',
|
||||
origin: 'https://logto.io',
|
||||
};
|
||||
|
||||
describe('bindMfaPayloadVerification', () => {
|
||||
const baseCtx = {
|
||||
...createContextWithRouteParameters(),
|
||||
|
@ -54,7 +60,8 @@ describe('bindMfaPayloadVerification', () => {
|
|||
type: MfaFactor.TOTP,
|
||||
secret: 'secret',
|
||||
},
|
||||
}
|
||||
},
|
||||
additionalParameters
|
||||
)
|
||||
).resolves.toMatchObject({
|
||||
type: MfaFactor.TOTP,
|
||||
|
@ -66,7 +73,12 @@ describe('bindMfaPayloadVerification', () => {
|
|||
|
||||
it('should reject when pendingMfa is missing', async () => {
|
||||
await expect(
|
||||
bindMfaPayloadVerification(baseCtx, { type: MfaFactor.TOTP, code: '123456' }, interaction)
|
||||
bindMfaPayloadVerification(
|
||||
baseCtx,
|
||||
{ type: MfaFactor.TOTP, code: '123456' },
|
||||
interaction,
|
||||
additionalParameters
|
||||
)
|
||||
).rejects.toEqual(new RequestError('session.mfa.pending_info_not_found'));
|
||||
});
|
||||
|
||||
|
@ -83,7 +95,8 @@ describe('bindMfaPayloadVerification', () => {
|
|||
type: MfaFactor.TOTP,
|
||||
secret: 'secret',
|
||||
},
|
||||
}
|
||||
},
|
||||
additionalParameters
|
||||
)
|
||||
).rejects.toEqual(new RequestError('session.mfa.invalid_totp_code'));
|
||||
});
|
||||
|
|
|
@ -9,7 +9,10 @@ import {
|
|||
type MfaVerificationTotp,
|
||||
type VerifyMfaPayload,
|
||||
type VerifyMfaResult,
|
||||
type BindWebAuthn,
|
||||
type BindWebAuthnPayload,
|
||||
} from '@logto/schemas';
|
||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
|
||||
import type { WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
@ -17,6 +20,7 @@ import assertThat from '#src/utils/assert-that.js';
|
|||
|
||||
import type { AnonymousInteractionResult } from '../types/index.js';
|
||||
import { validateTotpToken } from '../utils/totp-validation.js';
|
||||
import { verifyWebAuthnRegistration } from '../utils/webauthn.js';
|
||||
|
||||
const verifyBindTotp = async (
|
||||
interactionStorage: AnonymousInteractionResult,
|
||||
|
@ -27,8 +31,6 @@ const verifyBindTotp = async (
|
|||
ctx.createLog(`Interaction.${event}.BindMfa.Totp.Submit`);
|
||||
|
||||
assertThat(pendingMfa, 'session.mfa.pending_info_not_found');
|
||||
// Will add more type, disable the rule for now, this can be a reminder when adding new type
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
assertThat(pendingMfa.type === MfaFactor.TOTP, 'session.mfa.pending_info_not_found');
|
||||
|
||||
const { code, type } = payload;
|
||||
|
@ -61,12 +63,69 @@ const verifyTotp = async (
|
|||
return { type, id };
|
||||
};
|
||||
|
||||
const verifyBindWebAuthn = async (
|
||||
interactionStorage: AnonymousInteractionResult,
|
||||
payload: BindWebAuthnPayload,
|
||||
ctx: WithLogContext,
|
||||
{
|
||||
rpId,
|
||||
userAgent,
|
||||
origin,
|
||||
}: {
|
||||
rpId: string;
|
||||
userAgent: string;
|
||||
origin: string;
|
||||
}
|
||||
): Promise<BindWebAuthn> => {
|
||||
const { event, pendingMfa } = interactionStorage;
|
||||
ctx.createLog(`Interaction.${event}.BindMfa.Totp.Submit`);
|
||||
|
||||
assertThat(pendingMfa, 'session.mfa.pending_info_not_found');
|
||||
assertThat(pendingMfa.type === MfaFactor.WebAuthn, 'session.mfa.pending_info_not_found');
|
||||
|
||||
const { type, ...rest } = payload;
|
||||
const { challenge } = pendingMfa;
|
||||
const { verified, registrationInfo } = await verifyWebAuthnRegistration(
|
||||
rest,
|
||||
challenge,
|
||||
rpId,
|
||||
origin
|
||||
);
|
||||
|
||||
assertThat(verified, 'session.mfa.webauthn_verification_failed');
|
||||
assertThat(registrationInfo, 'session.mfa.webauthn_verification_failed');
|
||||
|
||||
const { credentialID, credentialPublicKey, counter } = registrationInfo;
|
||||
|
||||
return {
|
||||
type,
|
||||
credentialId: isoBase64URL.fromBuffer(credentialID),
|
||||
publicKey: isoBase64URL.fromBuffer(credentialPublicKey),
|
||||
counter,
|
||||
agent: userAgent,
|
||||
transports: payload.response.transports ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
export async function bindMfaPayloadVerification(
|
||||
ctx: WithLogContext,
|
||||
bindMfaPayload: BindMfaPayload,
|
||||
interactionStorage: AnonymousInteractionResult
|
||||
interactionStorage: AnonymousInteractionResult,
|
||||
{
|
||||
rpId,
|
||||
userAgent,
|
||||
origin,
|
||||
}: {
|
||||
rpId: string;
|
||||
userAgent: string;
|
||||
origin: string;
|
||||
}
|
||||
): Promise<BindMfa> {
|
||||
return verifyBindTotp(interactionStorage, bindMfaPayload, ctx);
|
||||
if (bindMfaPayload.type === MfaFactor.TOTP) {
|
||||
return verifyBindTotp(interactionStorage, bindMfaPayload, ctx);
|
||||
}
|
||||
|
||||
return verifyBindWebAuthn(interactionStorage, bindMfaPayload, ctx, { rpId, userAgent, origin });
|
||||
}
|
||||
|
||||
export async function verifyMfaPayloadVerification(
|
||||
|
|
|
@ -25,8 +25,6 @@ export const verifyBindMfa = async (
|
|||
|
||||
const { type } = bindMfa;
|
||||
|
||||
// There will be more types later
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (type === MfaFactor.TOTP) {
|
||||
const { accountId } = interaction;
|
||||
const { mfaVerifications } = await tenant.queries.users.findUserById(accountId);
|
||||
|
|
|
@ -24,6 +24,8 @@ const session = {
|
|||
'Benutzerkennung nicht gefunden. Bitte gehen Sie zurück und melden Sie sich erneut an.',
|
||||
interaction_not_found:
|
||||
'Interaktionssitzung nicht gefunden. Bitte gehen Sie zurück und starten Sie die Sitzung erneut.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -33,6 +35,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -21,11 +21,13 @@ const session = {
|
|||
identifier_not_found: 'User identifier not found. Please go back and sign in again.',
|
||||
interaction_not_found:
|
||||
'Interaction session not found. Please go back and start the session again.',
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
mfa_sign_in_only: 'Mfa is only available for sign-in interaction.',
|
||||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ const session = {
|
|||
'Identificador de usuario no encontrado. Vuelva atrás e inicie sesión nuevamente.',
|
||||
interaction_not_found:
|
||||
'No se encuentra la sesión de interacción. Vuelva atrás y vuelva a iniciar la sesión.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -34,6 +36,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ const session = {
|
|||
'Identifiant utilisateur introuvable. Veuillez retourner en arrière et vous connecter à nouveau.',
|
||||
interaction_not_found:
|
||||
"Session d'interaction introuvable. Veuillez retourner en arrière et recommencer la session.",
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -35,6 +37,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ const session = {
|
|||
identifier_not_found: 'Identificativo utente non trovato. Torna indietro e accedi nuovamente.',
|
||||
interaction_not_found:
|
||||
'Sessione di interazione non trovata. Torna indietro e avvia la sessione nuovamente.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -33,6 +35,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ const session = {
|
|||
identifier_not_found: 'ユーザーIDが見つかりません。戻って再度サインインしてください。',
|
||||
interaction_not_found:
|
||||
'インタラクションセッションが見つかりません。戻ってセッションを開始してください。',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -31,6 +33,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ const session = {
|
|||
connector_validation_session_not_found: '연동 세션 유효성 검증을 위한 토큰을 찾을 수 없어요.',
|
||||
identifier_not_found: '사용자 식별자를 찾을 수 없어요. 처음부터 다시 로그인을 시도해 주세요.',
|
||||
interaction_not_found: '인터렉션 세션을 찾을 수 없어요. 처음부터 다시 세션을 시작해 주세요.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -30,6 +32,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ const session = {
|
|||
'Nie znaleziono identyfikatora użytkownika. Proszę wróć i zaloguj się ponownie.',
|
||||
interaction_not_found:
|
||||
'Nie znaleziono sesji interakcji. Proszę wróć i rozpocznij sesję ponownie.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -32,6 +34,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ const session = {
|
|||
'Identificador de usuário não encontrado. Por favor, volte e faça o login novamente.',
|
||||
interaction_not_found:
|
||||
'Sessão de interação não encontrada. Por favor, volte e inicie a sessão novamente.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -33,6 +35,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ const session = {
|
|||
'Identificador do usuário não encontrado. Por favor, volte e faça login novamente.',
|
||||
interaction_not_found:
|
||||
'Sessão de interação não encontrada. Por favor, volte e inicie a sessão novamente.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -35,6 +37,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ const session = {
|
|||
identifier_not_found:
|
||||
'Идентификатор пользователя не найден. Вернитесь и войдите в систему снова.',
|
||||
interaction_not_found: 'Сессия взаимодействия не найдена. Вернитесь и начните сессию заново.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -31,6 +33,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ const session = {
|
|||
identifier_not_found: 'Kullanıcı kimliği bulunamadı. Lütfen geri gidin ve yeniden giriş yapın.',
|
||||
interaction_not_found:
|
||||
'Etkileşim oturumu bulunamadı. Lütfen geri gidin ve oturumu yeniden başlatın.',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -32,6 +34,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ const session = {
|
|||
connector_validation_session_not_found: '找不到连接器用于验证 token 的信息。',
|
||||
identifier_not_found: '找不到用户标识符。请返回并重新登录。',
|
||||
interaction_not_found: '找不到交互会话。请返回并重新开始会话。',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -27,6 +29,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ const session = {
|
|||
connector_validation_session_not_found: '找不到連接器用於驗證 token 的信息。',
|
||||
identifier_not_found: '找不到用戶標識符。請返回並重新登錄。',
|
||||
interaction_not_found: '找不到互動會話。請返回並重新開始會話。',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -27,6 +29,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ const session = {
|
|||
connector_validation_session_not_found: '找不到連接器用於驗證 token 的資訊。',
|
||||
identifier_not_found: '找不到使用者標識符。請返回並重新登錄。',
|
||||
interaction_not_found: '找不到交互會話。請返回並重新開始會話。',
|
||||
/** UNTRANSLATED */
|
||||
not_supported_for_forgot_password: 'This operation is not supported for forgot password.',
|
||||
mfa: {
|
||||
/** UNTRANSLATED */
|
||||
require_mfa_verification: 'Mfa verification is required to sign in.',
|
||||
|
@ -27,6 +29,8 @@ const session = {
|
|||
pending_info_not_found: 'Pending MFA info not found, please initiate MFA first.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_totp_code: 'Invalid TOTP code.',
|
||||
/** UNTRANSLATED */
|
||||
webauthn_verification_failed: 'WebAuthn verification failed.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -26,11 +26,22 @@ export const mfaVerificationTotp = z.object({
|
|||
|
||||
export type MfaVerificationTotp = z.infer<typeof mfaVerificationTotp>;
|
||||
|
||||
export const webAuthnTransportGuard = z.enum([
|
||||
'usb',
|
||||
'nfc',
|
||||
'ble',
|
||||
'internal',
|
||||
'cable',
|
||||
'hybrid',
|
||||
'smart-card',
|
||||
]);
|
||||
|
||||
export const mfaVerificationWebAuthn = z.object({
|
||||
type: z.literal(MfaFactor.WebAuthn),
|
||||
...baseMfaVerification,
|
||||
credentialId: z.string(),
|
||||
publicKey: z.string(),
|
||||
transports: webAuthnTransportGuard.array().optional(),
|
||||
counter: z.number(),
|
||||
agent: z.string(),
|
||||
});
|
||||
|
|
|
@ -19,3 +19,4 @@ export * from './cookie.js';
|
|||
export * from './dashboard.js';
|
||||
export * from './domain.js';
|
||||
export * from './sentinel.js';
|
||||
export * from './mfa.js';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { MfaFactor, jsonObjectGuard } from '../foundations/index.js';
|
||||
import { MfaFactor, jsonObjectGuard, webAuthnTransportGuard } from '../foundations/index.js';
|
||||
|
||||
import type {
|
||||
EmailVerificationCodePayload,
|
||||
|
@ -110,7 +110,36 @@ export const bindTotpPayloadGuard = z.object({
|
|||
|
||||
export type BindTotpPayload = z.infer<typeof bindTotpPayloadGuard>;
|
||||
|
||||
export const bindMfaPayloadGuard = bindTotpPayloadGuard;
|
||||
export const bindWebAuthnPayloadGuard = z.object({
|
||||
type: z.literal(MfaFactor.WebAuthn),
|
||||
id: z.string(),
|
||||
rawId: z.string(),
|
||||
response: z.object({
|
||||
clientDataJSON: z.string(),
|
||||
attestationObject: z.string(),
|
||||
authenticatorData: z.string().optional(),
|
||||
transports: webAuthnTransportGuard.array().optional(),
|
||||
publicKeyAlgorithm: z.number().optional(),
|
||||
publicKey: z.string().optional(),
|
||||
}),
|
||||
authenticatorAttachment: z.enum(['cross-platform', 'platform']).optional(),
|
||||
clientExtensionResults: z.object({
|
||||
appid: z.boolean().optional(),
|
||||
crepProps: z
|
||||
.object({
|
||||
rk: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
hmacCreateSecret: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type BindWebAuthnPayload = z.infer<typeof bindWebAuthnPayloadGuard>;
|
||||
|
||||
export const bindMfaPayloadGuard = z.discriminatedUnion('type', [
|
||||
bindTotpPayloadGuard,
|
||||
bindWebAuthnPayloadGuard,
|
||||
]);
|
||||
|
||||
export type BindMfaPayload = z.infer<typeof bindMfaPayloadGuard>;
|
||||
|
||||
|
@ -129,9 +158,19 @@ export const pendingTotpGuard = z.object({
|
|||
|
||||
export type PendingTotp = z.infer<typeof pendingTotpGuard>;
|
||||
|
||||
export const pendingWebAuthnGuard = z.object({
|
||||
type: z.literal(MfaFactor.WebAuthn),
|
||||
challenge: z.string(),
|
||||
});
|
||||
|
||||
export type PendingWebAuthn = z.infer<typeof pendingWebAuthnGuard>;
|
||||
|
||||
// Some information like TOTP secret should be generated in the backend
|
||||
// and stored in the interaction temporarily.
|
||||
export const pendingMfaGuard = pendingTotpGuard;
|
||||
export const pendingMfaGuard = z.discriminatedUnion('type', [
|
||||
pendingTotpGuard,
|
||||
pendingWebAuthnGuard,
|
||||
]);
|
||||
|
||||
export type PendingMfa = z.infer<typeof pendingMfaGuard>;
|
||||
|
||||
|
@ -139,8 +178,19 @@ export const bindTotpGuard = pendingTotpGuard;
|
|||
|
||||
export type BindTotp = z.infer<typeof bindTotpGuard>;
|
||||
|
||||
export const bindWebAuthnGuard = z.object({
|
||||
type: z.literal(MfaFactor.WebAuthn),
|
||||
credentialId: z.string(),
|
||||
publicKey: z.string(),
|
||||
transports: webAuthnTransportGuard.array(),
|
||||
counter: z.number(),
|
||||
agent: z.string(),
|
||||
});
|
||||
|
||||
export type BindWebAuthn = z.infer<typeof bindWebAuthnGuard>;
|
||||
|
||||
// The type for binding new mfa verification to a user, not always equals to the pending type.
|
||||
export const bindMfaGuard = bindTotpGuard;
|
||||
export const bindMfaGuard = z.discriminatedUnion('type', [bindTotpGuard, bindWebAuthnGuard]);
|
||||
|
||||
export type BindMfa = z.infer<typeof bindMfaGuard>;
|
||||
|
||||
|
|
50
packages/schemas/src/types/mfa.ts
Normal file
50
packages/schemas/src/types/mfa.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { webAuthnTransportGuard } from '../foundations/jsonb-types/index.js';
|
||||
|
||||
export const webAuthnRegistrationOptionsGuard = z.object({
|
||||
rp: z.object({
|
||||
name: z.string(),
|
||||
id: z.string().optional(),
|
||||
}),
|
||||
user: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
displayName: z.string(),
|
||||
}),
|
||||
challenge: z.string(),
|
||||
pubKeyCredParams: z.array(
|
||||
z.object({
|
||||
type: z.literal('public-key'),
|
||||
alg: z.number(),
|
||||
})
|
||||
),
|
||||
timeout: z.number().optional(),
|
||||
excludeCredentials: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.literal('public-key'),
|
||||
id: z.string(),
|
||||
transports: webAuthnTransportGuard.array().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
authenticatorSelection: z
|
||||
.object({
|
||||
authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(),
|
||||
requireResidentKey: z.boolean().optional(),
|
||||
residentKey: z.enum(['discouraged', 'preferred', 'required']).optional(),
|
||||
userVerification: z.enum(['required', 'preferred', 'discouraged']).optional(),
|
||||
})
|
||||
.optional(),
|
||||
attestation: z.enum(['none', 'indirect', 'direct', 'enterprise']).optional(),
|
||||
extensions: z
|
||||
.object({
|
||||
appid: z.string().optional(),
|
||||
credProps: z.boolean().optional(),
|
||||
hmacCreateSecret: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type WebAuthnRegistrationOptions = z.infer<typeof webAuthnRegistrationOptionsGuard>;
|
182
pnpm-lock.yaml
generated
182
pnpm-lock.yaml
generated
|
@ -3172,6 +3172,9 @@ importers:
|
|||
'@silverhand/essentials':
|
||||
specifier: ^2.8.4
|
||||
version: 2.8.4
|
||||
'@simplewebauthn/server':
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
'@withtyped/client':
|
||||
specifier: ^0.7.22
|
||||
version: 0.7.22(zod@3.22.3)
|
||||
|
@ -6356,6 +6359,54 @@ packages:
|
|||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||
dev: true
|
||||
|
||||
/@cbor-extract/cbor-extract-darwin-arm64@2.1.1:
|
||||
resolution: {integrity: sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@cbor-extract/cbor-extract-darwin-x64@2.1.1:
|
||||
resolution: {integrity: sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@cbor-extract/cbor-extract-linux-arm64@2.1.1:
|
||||
resolution: {integrity: sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@cbor-extract/cbor-extract-linux-arm@2.1.1:
|
||||
resolution: {integrity: sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@cbor-extract/cbor-extract-linux-x64@2.1.1:
|
||||
resolution: {integrity: sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@cbor-extract/cbor-extract-win32-x64@2.1.1:
|
||||
resolution: {integrity: sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@changesets/apply-release-plan@6.1.4:
|
||||
resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==}
|
||||
dependencies:
|
||||
|
@ -6843,6 +6894,10 @@ packages:
|
|||
'@hapi/hoek': 9.3.0
|
||||
dev: true
|
||||
|
||||
/@hexagon/base64@1.1.28:
|
||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.10:
|
||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
|
@ -8564,6 +8619,50 @@ packages:
|
|||
nullthrows: 1.1.1
|
||||
dev: true
|
||||
|
||||
/@peculiar/asn1-android@2.3.6:
|
||||
resolution: {integrity: sha512-zkYh4DsiRhiNfg6tWaUuRc+huwlb9XJbmeZLrjTz9v76UK1Ehq3EnfJFED6P3sdznW/nqWe46LoM9JrqxcD58g==}
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.6
|
||||
asn1js: 3.0.5
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/@peculiar/asn1-ecc@2.3.6:
|
||||
resolution: {integrity: sha512-Hu1xzMJQWv8/GvzOiinaE6XiD1/kEhq2C/V89UEoWeZ2fLUcGNIvMxOr/pMyL0OmpRWj/mhCTXOZp4PP+a0aTg==}
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.6
|
||||
'@peculiar/asn1-x509': 2.3.6
|
||||
asn1js: 3.0.5
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/@peculiar/asn1-rsa@2.3.6:
|
||||
resolution: {integrity: sha512-DswjJyAXZnvESuImGNTvbNKvh1XApBVqU+r3UmrFFTAI23gv62byl0f5OFKWTNhCf66WQrd3sklpsCZc/4+jwA==}
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.6
|
||||
'@peculiar/asn1-x509': 2.3.6
|
||||
asn1js: 3.0.5
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/@peculiar/asn1-schema@2.3.6:
|
||||
resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==}
|
||||
dependencies:
|
||||
asn1js: 3.0.5
|
||||
pvtsutils: 1.3.5
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/@peculiar/asn1-x509@2.3.6:
|
||||
resolution: {integrity: sha512-dRwX31R1lcbIdzbztiMvLNTDoGptxdV7HocNx87LfKU0fEWh7fTWJjx4oV+glETSy6heF/hJHB2J4RGB3vVSYg==}
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.6
|
||||
asn1js: 3.0.5
|
||||
ipaddr.js: 2.1.0
|
||||
pvtsutils: 1.3.5
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/@pkgr/utils@2.3.1:
|
||||
resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
@ -9033,6 +9132,27 @@ packages:
|
|||
typescript: 5.0.2
|
||||
dev: true
|
||||
|
||||
/@simplewebauthn/server@8.2.0:
|
||||
resolution: {integrity: sha512-nknf7kCa5V61Kk2zn1vTuKeAlyut9aWduIcbHNQWpMCEJqH/m8cXpb+9UV42MEQRIk8JVC1GSNeEx56QVTfJHw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
dependencies:
|
||||
'@hexagon/base64': 1.1.28
|
||||
'@peculiar/asn1-android': 2.3.6
|
||||
'@peculiar/asn1-ecc': 2.3.6
|
||||
'@peculiar/asn1-rsa': 2.3.6
|
||||
'@peculiar/asn1-schema': 2.3.6
|
||||
'@peculiar/asn1-x509': 2.3.6
|
||||
'@simplewebauthn/typescript-types': 8.0.0
|
||||
cbor-x: 1.5.4
|
||||
cross-fetch: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/@simplewebauthn/typescript-types@8.0.0:
|
||||
resolution: {integrity: sha512-d7Izb2H+LZJteXMkS8DmpAarD6mZdpIOu/av/yH4/u/3Pd6DKFLyBM3j8BMmUvUqpzvJvHARNrRfQYto58mtTQ==}
|
||||
dev: false
|
||||
|
||||
/@sinclair/typebox@0.24.46:
|
||||
resolution: {integrity: sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==}
|
||||
dev: true
|
||||
|
@ -10402,6 +10522,15 @@ packages:
|
|||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/asn1js@3.0.5:
|
||||
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dependencies:
|
||||
pvtsutils: 1.3.5
|
||||
pvutils: 1.1.3
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/ast-types-flow@0.0.7:
|
||||
resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
|
||||
dev: true
|
||||
|
@ -10823,6 +10952,28 @@ packages:
|
|||
resolution: {integrity: sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==}
|
||||
dev: true
|
||||
|
||||
/cbor-extract@2.1.1:
|
||||
resolution: {integrity: sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
node-gyp-build-optional-packages: 5.0.3
|
||||
optionalDependencies:
|
||||
'@cbor-extract/cbor-extract-darwin-arm64': 2.1.1
|
||||
'@cbor-extract/cbor-extract-darwin-x64': 2.1.1
|
||||
'@cbor-extract/cbor-extract-linux-arm': 2.1.1
|
||||
'@cbor-extract/cbor-extract-linux-arm64': 2.1.1
|
||||
'@cbor-extract/cbor-extract-linux-x64': 2.1.1
|
||||
'@cbor-extract/cbor-extract-win32-x64': 2.1.1
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/cbor-x@1.5.4:
|
||||
resolution: {integrity: sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw==}
|
||||
optionalDependencies:
|
||||
cbor-extract: 2.1.1
|
||||
dev: false
|
||||
|
||||
/ccount@1.1.0:
|
||||
resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
|
||||
dev: true
|
||||
|
@ -11332,7 +11483,6 @@ packages:
|
|||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/cross-spawn@5.1.0:
|
||||
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
|
||||
|
@ -13884,6 +14034,11 @@ packages:
|
|||
resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==}
|
||||
dev: true
|
||||
|
||||
/ipaddr.js@2.1.0:
|
||||
resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==}
|
||||
engines: {node: '>= 10'}
|
||||
dev: false
|
||||
|
||||
/is-alphabetical@1.0.4:
|
||||
resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
|
||||
dev: true
|
||||
|
@ -16349,7 +16504,6 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/node-fetch@3.3.0:
|
||||
resolution: {integrity: sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==}
|
||||
|
@ -16365,6 +16519,13 @@ packages:
|
|||
engines: {node: '>= 6.13.0'}
|
||||
dev: false
|
||||
|
||||
/node-gyp-build-optional-packages@5.0.3:
|
||||
resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/node-gyp-build-optional-packages@5.0.6:
|
||||
resolution: {integrity: sha512-2ZJErHG4du9G3/8IWl/l9Bp5BBFy63rno5GVmjQijvTuUZKsl6g8RB4KH/x3NLcV5ZBb4GsXmAuTYr6dRml3Gw==}
|
||||
hasBin: true
|
||||
|
@ -17562,6 +17723,17 @@ packages:
|
|||
resolution: {integrity: sha512-rLSBxJjP+4DQOgcJAx6RZHT2he2pkhQdSnofG5VWyVl6GRq/K02ISOuOLcsMOrtKDIJb8JN2zm3FFzWNbezdPw==}
|
||||
dev: true
|
||||
|
||||
/pvtsutils@1.3.5:
|
||||
resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/pvutils@1.1.3:
|
||||
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: false
|
||||
|
||||
/q@1.5.1:
|
||||
resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
|
||||
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||
|
@ -18478,7 +18650,7 @@ packages:
|
|||
/rxjs@7.8.0:
|
||||
resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
|
||||
dependencies:
|
||||
tslib: 2.4.1
|
||||
tslib: 2.5.0
|
||||
|
||||
/safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
|
@ -19828,6 +20000,10 @@ packages:
|
|||
/tslib@2.5.0:
|
||||
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
|
||||
|
||||
/tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
dev: false
|
||||
|
||||
/tsscmp@1.0.6:
|
||||
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
|
||||
engines: {node: '>=0.6.x'}
|
||||
|
|
Loading…
Add table
Reference in a new issue