mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
fix(core): set totp qrcode service and user name (#4703)
This commit is contained in:
parent
5cfb478715
commit
2c5db0d66a
5 changed files with 81 additions and 5 deletions
|
@ -221,6 +221,16 @@ describe('interaction routes', () => {
|
|||
const path = `${interactionPrefix}/${verificationPath}/totp`;
|
||||
|
||||
it('should return the generated secret', async () => {
|
||||
getInteractionStorage.mockReturnValue({
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
verifyIdentifier.mockResolvedValueOnce({
|
||||
event: InteractionEvent.Register,
|
||||
});
|
||||
verifyProfile.mockResolvedValueOnce({
|
||||
event: InteractionEvent.Register,
|
||||
});
|
||||
findUserById.mockResolvedValueOnce(mockUser);
|
||||
const response = await sessionRequest.post(path).send();
|
||||
expect(getInteractionStorage).toBeCalled();
|
||||
expect(storeInteractionResult).toBeCalled();
|
||||
|
|
|
@ -5,8 +5,10 @@ import {
|
|||
webAuthnRegistrationOptionsGuard,
|
||||
webAuthnAuthenticationOptionsGuard,
|
||||
} from '@logto/schemas';
|
||||
import { getUserDisplayName } from '@logto/shared';
|
||||
import type Router from 'koa-router';
|
||||
import { type IRouterParamContext } from 'koa-router';
|
||||
import { authenticator } from 'otplib';
|
||||
import qrcode from 'qrcode';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -116,9 +118,18 @@ export default function additionalRoutes<T extends IRouterParamContext>(
|
|||
async (ctx, next) => {
|
||||
const { interactionDetails, createLog } = ctx;
|
||||
// Check interaction exists
|
||||
const { event } = getInteractionStorage(interactionDetails.result);
|
||||
const interaction = getInteractionStorage(interactionDetails.result);
|
||||
const { event } = interaction;
|
||||
assertThat(
|
||||
event !== InteractionEvent.ForgotPassword,
|
||||
'session.not_supported_for_forgot_password'
|
||||
);
|
||||
createLog(`Interaction.${event}.BindMfa.Totp.Create`);
|
||||
|
||||
const service = ctx.URL.hostname;
|
||||
const accountVerifiedInteraction = await verifyIdentifier(ctx, tenant, interaction);
|
||||
const profileVerifiedInteraction = await verifyProfile(tenant, accountVerifiedInteraction);
|
||||
|
||||
const secret = generateTotpSecret();
|
||||
await storeInteractionResult(
|
||||
{ pendingMfa: { type: MfaFactor.TOTP, secret } },
|
||||
|
@ -127,13 +138,38 @@ export default function additionalRoutes<T extends IRouterParamContext>(
|
|||
true
|
||||
);
|
||||
|
||||
if (isRegisterInteractionResult(profileVerifiedInteraction)) {
|
||||
const {
|
||||
username = null,
|
||||
primaryEmail = null,
|
||||
primaryPhone = null,
|
||||
name = null,
|
||||
} = await parseUserProfile(tenant, profileVerifiedInteraction);
|
||||
const user = getUserDisplayName({ username, primaryEmail, primaryPhone, name });
|
||||
const keyUri = authenticator.keyuri(user, service, secret);
|
||||
|
||||
ctx.body = {
|
||||
secret,
|
||||
secretQrCode: await qrcode.toDataURL(`otpauth://totp/?secret=${secret}`),
|
||||
secretQrCode: await qrcode.toDataURL(keyUri),
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
if (isSignInInteractionResult(profileVerifiedInteraction)) {
|
||||
const { accountId } = profileVerifiedInteraction;
|
||||
const { username, primaryEmail, primaryPhone, name } = await findUserById(accountId);
|
||||
const user = getUserDisplayName({ username, primaryEmail, primaryPhone, name });
|
||||
const keyUri = authenticator.keyuri(user, service, secret);
|
||||
|
||||
ctx.body = {
|
||||
secret,
|
||||
secretQrCode: await qrcode.toDataURL(keyUri),
|
||||
};
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './object.js';
|
||||
export * from './ttl-cache.js';
|
||||
export * from './id.js';
|
||||
export * from './user-display-name.js';
|
||||
|
|
13
packages/shared/src/utils/user-display-name.test.ts
Normal file
13
packages/shared/src/utils/user-display-name.test.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { getUserDisplayName } from './user-display-name.js';
|
||||
|
||||
describe('getUserDisplayName', () => {
|
||||
it('should get user display name from name', () => {
|
||||
const user = {
|
||||
name: 'test',
|
||||
username: 'test',
|
||||
primaryEmail: 'test',
|
||||
primaryPhone: 'test',
|
||||
};
|
||||
expect(getUserDisplayName(user)).toEqual('test');
|
||||
});
|
||||
});
|
16
packages/shared/src/utils/user-display-name.ts
Normal file
16
packages/shared/src/utils/user-display-name.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Get user display name from multiple fields
|
||||
*/
|
||||
export const getUserDisplayName = ({
|
||||
name,
|
||||
username,
|
||||
primaryEmail,
|
||||
primaryPhone,
|
||||
}: {
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
primaryEmail: string | null;
|
||||
primaryPhone: string | null;
|
||||
}): string => {
|
||||
return name ?? username ?? primaryEmail ?? primaryPhone ?? 'Unnamed User';
|
||||
};
|
Loading…
Reference in a new issue