diff --git a/packages/core/src/lib/passcode.test.ts b/packages/core/src/lib/passcode.test.ts index 0e6ffb454..1b68ee454 100644 --- a/packages/core/src/lib/passcode.test.ts +++ b/packages/core/src/lib/passcode.test.ts @@ -5,8 +5,8 @@ import { ConnectorType } from '@/connectors/types'; import RequestError from '@/errors/RequestError'; import { deletePasscodesByIds, - findUnconsumedPasscodeBySessionIdAndType, - findUnconsumedPasscodesBySessionIdAndType, + findUnconsumedPasscodeByJtiAndType, + findUnconsumedPasscodesByJtiAndType, insertPasscode, updatePasscode, } from '@/queries/passcode'; @@ -23,13 +23,13 @@ import { jest.mock('@/queries/passcode'); jest.mock('@/connectors'); -const mockedFindUnconsumedPasscodesBySessionIdAndType = - findUnconsumedPasscodesBySessionIdAndType as jest.MockedFunction< - typeof findUnconsumedPasscodesBySessionIdAndType +const mockedFindUnconsumedPasscodesByJtiAndType = + findUnconsumedPasscodesByJtiAndType as jest.MockedFunction< + typeof findUnconsumedPasscodesByJtiAndType >; -const mockedFindUnconsumedPasscodeBySessionIdAndType = - findUnconsumedPasscodeBySessionIdAndType as jest.MockedFunction< - typeof findUnconsumedPasscodeBySessionIdAndType +const mockedFindUnconsumedPasscodeByJtiAndType = + findUnconsumedPasscodeByJtiAndType as jest.MockedFunction< + typeof findUnconsumedPasscodeByJtiAndType >; const mockedDeletePasscodesByIds = deletePasscodesByIds as jest.MockedFunction< typeof deletePasscodesByIds @@ -41,7 +41,7 @@ const mockedGetConnectorInstanceByType = getConnectorInstanceByType as jest.Mock const mockedUpdatePasscode = updatePasscode as jest.MockedFunction; beforeAll(() => { - mockedFindUnconsumedPasscodesBySessionIdAndType.mockResolvedValue([]); + mockedFindUnconsumedPasscodesByJtiAndType.mockResolvedValue([]); mockedInsertPasscode.mockImplementation(async (data) => ({ ...data, createdAt: Date.now(), @@ -53,7 +53,7 @@ beforeAll(() => { }); afterEach(() => { - mockedFindUnconsumedPasscodesBySessionIdAndType.mockClear(); + mockedFindUnconsumedPasscodesByJtiAndType.mockClear(); mockedDeletePasscodesByIds.mockClear(); mockedInsertPasscode.mockClear(); mockedGetConnectorInstanceByType.mockClear(); @@ -62,7 +62,7 @@ afterEach(() => { describe('createPasscode', () => { it('should generate `passcodeLength` digits code for phone and insert to database', async () => { const phone = '13000000000'; - const passcode = await createPasscode('sessionId', PasscodeType.SignIn, { + const passcode = await createPasscode('jti', PasscodeType.SignIn, { phone, }); expect(new RegExp(`^\\d{${passcodeLength}}$`).test(passcode.code)).toBeTruthy(); @@ -71,7 +71,7 @@ describe('createPasscode', () => { it('should generate `passcodeLength` digits code for email and insert to database', async () => { const email = 'jony@example.com'; - const passcode = await createPasscode('sessionId', PasscodeType.SignIn, { + const passcode = await createPasscode('jti', PasscodeType.SignIn, { email, }); expect(new RegExp(`^\\d{${passcodeLength}}$`).test(passcode.code)).toBeTruthy(); @@ -80,11 +80,11 @@ describe('createPasscode', () => { it('should disable existing passcode', async () => { const email = 'jony@example.com'; - const sessionId = 'sessonId'; - mockedFindUnconsumedPasscodesBySessionIdAndType.mockResolvedValue([ + const jti = 'jti'; + mockedFindUnconsumedPasscodesByJtiAndType.mockResolvedValue([ { id: 'id', - sessionId, + interactionJti: jti, code: '1234', type: PasscodeType.SignIn, createdAt: Date.now(), @@ -94,7 +94,7 @@ describe('createPasscode', () => { tryCount: 0, }, ]); - await createPasscode(sessionId, PasscodeType.SignIn, { + await createPasscode(jti, PasscodeType.SignIn, { email, }); expect(mockedDeletePasscodesByIds).toHaveBeenCalledWith(['id']); @@ -105,7 +105,7 @@ describe('sendPasscode', () => { it('should throw error when email and phone are both empty', async () => { const passcode: Passcode = { id: 'id', - sessionId: 'sessionId', + interactionJti: 'jti', phone: null, email: null, type: PasscodeType.SignIn, @@ -134,7 +134,7 @@ describe('sendPasscode', () => { }); const passcode: Passcode = { id: 'id', - sessionId: 'sessionId', + interactionJti: 'jti', phone: 'phone', email: null, type: PasscodeType.SignIn, @@ -153,7 +153,7 @@ describe('sendPasscode', () => { describe('verifyPasscode', () => { const passcode: Passcode = { id: 'id', - sessionId: 'sessionId', + interactionJti: 'jti', phone: 'phone', email: null, type: PasscodeType.SignIn, @@ -164,8 +164,8 @@ describe('verifyPasscode', () => { }; it('should mark as consumed on successful verification', async () => { - mockedFindUnconsumedPasscodeBySessionIdAndType.mockResolvedValue(passcode); - await verifyPasscode(passcode.sessionId, passcode.type, passcode.code, { phone: 'phone' }); + mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); + await verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' }); expect(mockedUpdatePasscode).toHaveBeenCalledWith( expect.objectContaining({ set: { consumed: true }, @@ -174,54 +174,58 @@ describe('verifyPasscode', () => { }); it('should fail when passcode not found', async () => { - mockedFindUnconsumedPasscodeBySessionIdAndType.mockResolvedValue(null); + mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(null); await expect( - verifyPasscode(passcode.sessionId, passcode.type, passcode.code, { phone: 'phone' }) + verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' }) ).rejects.toThrow(new RequestError('passcode.not_found')); }); it('should fail when phone mismatch', async () => { - mockedFindUnconsumedPasscodeBySessionIdAndType.mockResolvedValue(passcode); + mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); await expect( - verifyPasscode(passcode.sessionId, passcode.type, passcode.code, { phone: 'invalid_phone' }) + verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { + phone: 'invalid_phone', + }) ).rejects.toThrow(new RequestError('passcode.phone_mismatch')); }); it('should fail when email mismatch', async () => { - mockedFindUnconsumedPasscodeBySessionIdAndType.mockResolvedValue({ + mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({ ...passcode, phone: null, email: 'email', }); await expect( - verifyPasscode(passcode.sessionId, passcode.type, passcode.code, { email: 'invalid_email' }) + verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { + email: 'invalid_email', + }) ).rejects.toThrow(new RequestError('passcode.email_mismatch')); }); it('should fail when expired', async () => { - mockedFindUnconsumedPasscodeBySessionIdAndType.mockResolvedValue({ + mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({ ...passcode, createdAt: Date.now() - passcodeExpiration - 100, }); await expect( - verifyPasscode(passcode.sessionId, passcode.type, passcode.code, { phone: 'phone' }) + verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' }) ).rejects.toThrow(new RequestError('passcode.expired')); }); it('should fail when exceed max count', async () => { - mockedFindUnconsumedPasscodeBySessionIdAndType.mockResolvedValue({ + mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({ ...passcode, tryCount: passcodeMaxTryCount, }); await expect( - verifyPasscode(passcode.sessionId, passcode.type, passcode.code, { phone: 'phone' }) + verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' }) ).rejects.toThrow(new RequestError('passcode.exceed_max_try')); }); it('should fail when invalid code, and should increase try_count', async () => { - mockedFindUnconsumedPasscodeBySessionIdAndType.mockResolvedValue(passcode); + mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); await expect( - verifyPasscode(passcode.sessionId, passcode.type, 'invalid', { phone: 'phone' }) + verifyPasscode(passcode.interactionJti, passcode.type, 'invalid', { phone: 'phone' }) ).rejects.toThrow(new RequestError('passcode.code_mismatch')); expect(mockedUpdatePasscode).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/core/src/lib/passcode.ts b/packages/core/src/lib/passcode.ts index ec1e52cc5..00f2cf2a0 100644 --- a/packages/core/src/lib/passcode.ts +++ b/packages/core/src/lib/passcode.ts @@ -6,8 +6,8 @@ import { ConnectorType, EmailConector, SmsConnector } from '@/connectors/types'; import RequestError from '@/errors/RequestError'; import { deletePasscodesByIds, - findUnconsumedPasscodeBySessionIdAndType, - findUnconsumedPasscodesBySessionIdAndType, + findUnconsumedPasscodeByJtiAndType, + findUnconsumedPasscodesByJtiAndType, insertPasscode, updatePasscode, } from '@/queries/passcode'; @@ -16,12 +16,12 @@ export const passcodeLength = 6; const randomCode = customAlphabet('1234567890', passcodeLength); export const createPasscode = async ( - sessionId: string, + jti: string, type: PasscodeType, payload: { phone: string } | { email: string } ) => { // Disable existing passcodes. - const passcodes = await findUnconsumedPasscodesBySessionIdAndType(sessionId, type); + const passcodes = await findUnconsumedPasscodesByJtiAndType(jti, type); if (passcodes.length > 0) { await deletePasscodesByIds(passcodes.map(({ id }) => id)); @@ -29,7 +29,7 @@ export const createPasscode = async ( return insertPasscode({ id: nanoid(), - sessionId, + interactionJti: jti, type, code: randomCode(), ...payload, @@ -61,7 +61,7 @@ export const verifyPasscode = async ( code: string, payload: { phone: string } | { email: string } ): Promise => { - const passcode = await findUnconsumedPasscodeBySessionIdAndType(sessionId, type); + const passcode = await findUnconsumedPasscodeByJtiAndType(sessionId, type); if (!passcode) { throw new RequestError('passcode.not_found'); diff --git a/packages/core/src/queries/passcode.ts b/packages/core/src/queries/passcode.ts index 1e3a85a6e..4d7603e84 100644 --- a/packages/core/src/queries/passcode.ts +++ b/packages/core/src/queries/passcode.ts @@ -9,24 +9,18 @@ import { DeletionError } from '@/errors/SlonikError'; const { table, fields } = convertToIdentifiers(Passcodes); -export const findUnconsumedPasscodeBySessionIdAndType = async ( - sessionId: string, - type: PasscodeType -) => +export const findUnconsumedPasscodeByJtiAndType = async (jti: string, type: PasscodeType) => pool.maybeOne(sql` select ${sql.join(Object.values(fields), sql`, `)} from ${table} - where ${fields.sessionId}=${sessionId} and ${fields.type}=${type} and ${fields.consumed} = false + where ${fields.interactionJti}=${jti} and ${fields.type}=${type} and ${fields.consumed} = false `); -export const findUnconsumedPasscodesBySessionIdAndType = async ( - sessionId: string, - type: PasscodeType -) => +export const findUnconsumedPasscodesByJtiAndType = async (jti: string, type: PasscodeType) => pool.many(sql` select ${sql.join(Object.values(fields), sql`, `)} from ${table} - where ${fields.sessionId}=${sessionId} and ${fields.type}=${type} and ${fields.consumed} = false + where ${fields.interactionJti}=${jti} and ${fields.type}=${type} and ${fields.consumed} = false `); export const insertPasscode = buildInsertInto(pool, Passcodes, { diff --git a/packages/schemas/src/db-entries/passcode.ts b/packages/schemas/src/db-entries/passcode.ts index 75d11e9d4..1480cf4f4 100644 --- a/packages/schemas/src/db-entries/passcode.ts +++ b/packages/schemas/src/db-entries/passcode.ts @@ -7,7 +7,7 @@ import { PasscodeType } from './custom-types'; export type CreatePasscode = { id: string; - sessionId: string; + interactionJti: string; phone?: string | null; email?: string | null; type: PasscodeType; @@ -19,7 +19,7 @@ export type CreatePasscode = { export type Passcode = { id: string; - sessionId: string; + interactionJti: string; phone: string | null; email: string | null; type: PasscodeType; @@ -31,7 +31,7 @@ export type Passcode = { const createGuard: Guard = z.object({ id: z.string(), - sessionId: z.string(), + interactionJti: z.string(), phone: z.string().nullable().optional(), email: z.string().nullable().optional(), type: z.nativeEnum(PasscodeType), @@ -46,7 +46,7 @@ export const Passcodes: GeneratedSchema = Object.freeze({ tableSingular: 'passcode', fields: { id: 'id', - sessionId: 'session_id', + interactionJti: 'interaction_jti', phone: 'phone', email: 'email', type: 'type', @@ -57,7 +57,7 @@ export const Passcodes: GeneratedSchema = Object.freeze({ }, fieldKeys: [ 'id', - 'sessionId', + 'interactionJti', 'phone', 'email', 'type', diff --git a/packages/schemas/tables/passcodes.sql b/packages/schemas/tables/passcodes.sql index 03024a744..ba69a03d8 100644 --- a/packages/schemas/tables/passcodes.sql +++ b/packages/schemas/tables/passcodes.sql @@ -2,7 +2,7 @@ create type passcode_type as enum ('SignIn', 'Register', 'ForgotPassword'); create table passcodes ( id varchar(128) not null, - session_id varchar(128) not null, + interaction_jti varchar(128) not null, phone varchar(32), email varchar(128), type passcode_type not null,