0
Fork 0
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:
wangsijie 2023-10-24 15:13:36 +08:00 committed by GitHub
parent 5cfb478715
commit 2c5db0d66a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 81 additions and 5 deletions

View file

@ -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();

View file

@ -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(

View file

@ -1,3 +1,4 @@
export * from './object.js';
export * from './ttl-cache.js';
export * from './id.js';
export * from './user-display-name.js';

View 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');
});
});

View 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';
};