0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

refactor(core): sign-in and register flow share verification session (#2109)

This commit is contained in:
Darcy Ye 2022-10-11 14:49:33 +08:00 committed by GitHub
parent 9dba4b14a0
commit 2c0c49e1fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 68 deletions

View file

@ -1,2 +1 @@
export const forgotPasswordVerificationTimeout = 10 * 60; // 10 mins.
export const verificationTimeout = 10 * 60; // 10 mins.

View file

@ -337,7 +337,7 @@ describe('session -> passwordlessRoutes', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
it('should call interactionResult (with flow `sign-in`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
@ -358,6 +358,27 @@ describe('session -> passwordlessRoutes', () => {
expect.anything()
);
});
it('should call interactionResult (with flow `register`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000000',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(200);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
login: { accountId: 'id' },
}),
expect.anything()
);
});
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
@ -370,12 +391,12 @@ describe('session -> passwordlessRoutes', () => {
const response = await sessionRequest.post(`${signInRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it('throw when flow is not `sign-in`', async () => {
it('throw when flow is not `sign-in` and `register`', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000000',
flow: PasscodeType.Register,
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
@ -440,7 +461,7 @@ describe('session -> passwordlessRoutes', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
it('should call interactionResult (with flow `sign-in`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
@ -461,6 +482,27 @@ describe('session -> passwordlessRoutes', () => {
expect.anything()
);
});
it('should call interactionResult (with flow `register`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'a@a.com',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${signInRoute}/email`);
expect(response.statusCode).toEqual(200);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
login: { accountId: 'id' },
}),
expect.anything()
);
});
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
@ -473,12 +515,12 @@ describe('session -> passwordlessRoutes', () => {
const response = await sessionRequest.post(`${signInRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it('throw when flow is not `sign-in`', async () => {
it('throw when flow is not `sign-in` and `register`', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'a@a.com',
flow: PasscodeType.Register,
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
@ -517,7 +559,7 @@ describe('session -> passwordlessRoutes', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
it('should call interactionResult (with flow `register`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
@ -538,6 +580,27 @@ describe('session -> passwordlessRoutes', () => {
expect.anything()
);
});
it('should call interactionResult (with flow `sign-in`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000001',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/sms`);
expect(response.statusCode).toEqual(200);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
login: { accountId: 'user1' },
}),
expect.anything()
);
});
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
@ -550,12 +613,12 @@ describe('session -> passwordlessRoutes', () => {
const response = await sessionRequest.post(`${registerRoute}/sms`);
expect(response.statusCode).toEqual(404);
});
it('throw when flow is not `register`', async () => {
it('throw when flow is not `register` and `sign-in`', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
phone: '13000000001',
flow: PasscodeType.SignIn,
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
@ -594,7 +657,7 @@ describe('session -> passwordlessRoutes', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call interactionResult', async () => {
it('should call interactionResult (with flow `register`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
@ -615,6 +678,27 @@ describe('session -> passwordlessRoutes', () => {
expect.anything()
);
});
it('should call interactionResult (with flow `sign-in`)', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'b@a.com',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},
});
const response = await sessionRequest.post(`${registerRoute}/email`);
expect(response.statusCode).toEqual(200);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
login: { accountId: 'user1' },
}),
expect.anything()
);
});
it('throw when verification session invalid', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
@ -627,12 +711,12 @@ describe('session -> passwordlessRoutes', () => {
const response = await sessionRequest.post(`${registerRoute}/email`);
expect(response.statusCode).toEqual(404);
});
it('throw when flow is not `register`', async () => {
it('throw when flow is not `register` and `sign-in`', async () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
email: 'b@a.com',
flow: PasscodeType.SignIn,
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
},
},

View file

@ -16,11 +16,9 @@ import {
updateUserById,
} from '@/queries/user';
import {
emailRegisterSessionResultGuard,
emailSignInSessionResultGuard,
emailSessionResultGuard,
passcodeTypeGuard,
smsRegisterSessionResultGuard,
smsSignInSessionResultGuard,
smsSessionResultGuard,
} from '@/routes/session/types';
import assertThat from '@/utils/assert-that';
@ -178,7 +176,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
smsSignInSessionResultGuard
smsSessionResultGuard
);
const type = getPasswordlessRelatedLogType(PasscodeType.SignIn, 'sms');
@ -205,7 +203,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
emailSignInSessionResultGuard
emailSessionResultGuard
);
const type = getPasswordlessRelatedLogType(PasscodeType.SignIn, 'email');
@ -232,7 +230,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
smsRegisterSessionResultGuard
smsSessionResultGuard
);
const type = getPasswordlessRelatedLogType(PasscodeType.Register, 'sms');
@ -259,7 +257,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
const verificationStorage = await getVerificationStorageFromInteraction(
ctx,
provider,
emailRegisterSessionResultGuard
emailSessionResultGuard
);
const type = getPasswordlessRelatedLogType(PasscodeType.Register, 'email');

View file

@ -14,58 +14,32 @@ export type Operation = z.infer<typeof operationGuard>;
export type VerifiedIdentity = { email: string } | { phone: string } | { id: string };
export type VerificationStorage =
| SmsSignInSessionStorage
| EmailSignInSessionStorage
| SmsRegisterSessionStorage
| EmailRegisterSessionStorage
| SmsSessionStorage
| EmailSessionStorage
| ForgotPasswordSessionStorage;
export type VerificationResult<T = VerificationStorage> = { verification: T };
const smsSignInSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.SignIn),
const smsSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.SignIn).or(z.literal(PasscodeType.Register)),
expiresAt: z.string(),
phone: z.string(),
});
export type SmsSignInSessionStorage = z.infer<typeof smsSignInSessionStorageGuard>;
export type SmsSessionStorage = z.infer<typeof smsSessionStorageGuard>;
export const smsSignInSessionResultGuard = z.object({ verification: smsSignInSessionStorageGuard });
export const smsSessionResultGuard = z.object({ verification: smsSessionStorageGuard });
const emailSignInSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.SignIn),
const emailSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.SignIn).or(z.literal(PasscodeType.Register)),
expiresAt: z.string(),
email: z.string(),
});
export type EmailSignInSessionStorage = z.infer<typeof emailSignInSessionStorageGuard>;
export type EmailSessionStorage = z.infer<typeof emailSessionStorageGuard>;
export const emailSignInSessionResultGuard = z.object({
verification: emailSignInSessionStorageGuard,
});
const smsRegisterSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.Register),
expiresAt: z.string(),
phone: z.string(),
});
export type SmsRegisterSessionStorage = z.infer<typeof smsRegisterSessionStorageGuard>;
export const smsRegisterSessionResultGuard = z.object({
verification: smsRegisterSessionStorageGuard,
});
const emailRegisterSessionStorageGuard = z.object({
flow: z.literal(PasscodeType.Register),
expiresAt: z.string(),
email: z.string(),
});
export type EmailRegisterSessionStorage = z.infer<typeof emailRegisterSessionStorageGuard>;
export const emailRegisterSessionResultGuard = z.object({
verification: emailRegisterSessionStorageGuard,
export const emailSessionResultGuard = z.object({
verification: emailSessionStorageGuard,
});
const forgotPasswordSessionStorageGuard = z.object({

View file

@ -10,14 +10,13 @@ import assertThat from '@/utils/assert-that';
import { verificationTimeout } from './consts';
import {
emailRegisterSessionResultGuard,
emailSignInSessionResultGuard,
emailSessionResultGuard,
smsSessionResultGuard,
forgotPasswordSessionResultGuard,
Method,
Operation,
smsRegisterSessionResultGuard,
smsSignInSessionResultGuard,
VerificationResult,
VerificationStorage,
VerifiedIdentity,
} from './types';
@ -45,7 +44,7 @@ export const getPasswordlessRelatedLogType = (
return result.data;
};
const parseVerificationStorage = <T = unknown>(
const parseVerificationStorage = <T = VerificationStorage>(
data: unknown,
resultGuard: ZodType<VerificationResult<T>>
): T => {
@ -64,7 +63,7 @@ const parseVerificationStorage = <T = unknown>(
return verificationResult.data.verification;
};
export const getVerificationStorageFromInteraction = async <T = unknown>(
export const getVerificationStorageFromInteraction = async <T = VerificationStorage>(
ctx: Context,
provider: Provider,
resultGuard: ZodType<VerificationResult<T>>
@ -96,10 +95,8 @@ export const assignVerificationResult = async (
};
assertThat(
smsSignInSessionResultGuard.safeParse(verificationResult).success ||
emailSignInSessionResultGuard.safeParse(verificationResult).success ||
smsRegisterSessionResultGuard.safeParse(verificationResult).success ||
emailRegisterSessionResultGuard.safeParse(verificationResult).success ||
emailSessionResultGuard.safeParse(verificationResult).success ||
smsSessionResultGuard.safeParse(verificationResult).success ||
forgotPasswordSessionResultGuard.safeParse(verificationResult).success,
new RequestError({ code: 'session.invalid_verification' })
);