Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Use JWT as reset password token #6714

Merged
merged 18 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions packages/cli/src/controllers/passwordReset.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,12 @@ export class PasswordResetController {
const baseUrl = getInstanceBaseUrl();
const { id, firstName, lastName } = user;

const jwtTokenSecret = this.config.getEnv('userManagement.jwtSecret');

const resetPasswordToken = this.jwtService.sign({ sub: id }, jwtTokenSecret, {
expiresIn: '1d',
});
const resetPasswordToken = this.jwtService.signData(
{ sub: id },
{
expiresIn: '1d',
},
);

const url = await UserService.generatePasswordResetUrl(baseUrl, resetPasswordToken);

Expand Down Expand Up @@ -297,13 +298,9 @@ export class PasswordResetController {
}

private verifyResetPasswordToken(resetPasswordToken: string) {
const jwtSecret = this.config.getEnv('userManagement.jwtSecret');

let decodedToken: JwtPayload;
try {
decodedToken = this.jwtService.verify(resetPasswordToken, jwtSecret, {
ignoreExpiration: false,
}) as JwtPayload;
decodedToken = this.jwtService.verifyToken(resetPasswordToken);
return decodedToken;
} catch (e) {
if (e instanceof TokenExpiredError) {
Expand Down

This file was deleted.

4 changes: 2 additions & 2 deletions packages/cli/src/databases/migrations/mysqldb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { MigrateIntegerKeysToString1690000000001 } from './1690000000001-Migrate
import { SeparateExecutionData1690000000030 } from './1690000000030-SeparateExecutionData';
import { FixExecutionDataType1690000000031 } from './1690000000031-FixExecutionDataType';
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
import { RemoveResetPasswordColumns1690000000032 } from './1690000000032-RemoveResetPasswordColumns';
import { RemoveResetPasswordColumns1690000000030 } from '../common/1690000000030-RemoveResetPasswordColumns';

export const mysqlMigrations: Migration[] = [
InitialMigration1588157391238,
Expand Down Expand Up @@ -88,5 +88,5 @@ export const mysqlMigrations: Migration[] = [
SeparateExecutionData1690000000030,
FixExecutionDataType1690000000031,
RemoveSkipOwnerSetup1681134145997,
RemoveResetPasswordColumns1690000000032,
RemoveResetPasswordColumns1690000000030,
];
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserAc
import { MigrateIntegerKeysToString1690000000000 } from './1690000000000-MigrateIntegerKeysToString';
import { SeparateExecutionData1690000000020 } from './1690000000020-SeparateExecutionData';
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
import { RemoveResetPasswordColumns1690000000030 } from './1690000000030-RemoveResetPasswordColumns';
import { RemoveResetPasswordColumns1690000000030 } from '../common/1690000000030-RemoveResetPasswordColumns';

export const postgresMigrations: Migration[] = [
InitialMigration1587669153312,
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion packages/cli/src/databases/migrations/sqlite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { MigrateIntegerKeysToString1690000000002 } from './1690000000002-Migrate
import { SeparateExecutionData1690000000010 } from './1690000000010-SeparateExecutionData';
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
import { FixMissingIndicesFromStringIdMigration1690000000020 } from './1690000000020-FixMissingIndicesFromStringIdMigration';
import { RemoveResetPasswordColumns1690000000030 } from './1690000000030-RemoveResetPasswordColumns';
import { RemoveResetPasswordColumns1690000000030 } from '../common/1690000000030-RemoveResetPasswordColumns';

const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/services/jwt-base.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Service } from 'typedi';
import * as jwt from 'jsonwebtoken';

@Service()
export class JwtBaseService {
RicardoE105 marked this conversation as resolved.
Show resolved Hide resolved
protected sign(payload: object, secret: string, options: jwt.SignOptions = {}): string {
return jwt.sign(payload, secret, options);
}

protected verify(token: string, secret: string, options: jwt.VerifyOptions = {}) {
return jwt.verify(token, secret, options) as jwt.JwtPayload;
}
}

export type JwtPayload = jwt.JwtPayload;

export type SignInOptions = jwt.SignOptions;

export type VerifyOptions = jwt.VerifyOptions;
19 changes: 11 additions & 8 deletions packages/cli/src/services/jwt.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Service } from 'typedi';
import * as jwt from 'jsonwebtoken';
import config from '@/config';
import { JwtBaseService } from '@/services/jwt-base.service';
import type { SignInOptions, VerifyOptions } from '@/services/jwt-base.service';
export type * from '@/services/jwt-base.service';

@Service()
export class JwtService {
public sign(payload: object, secret: string, options: jwt.SignOptions = {}): string {
return jwt.sign(payload, secret, options);
export class JwtService extends JwtBaseService {
private readonly userManagementSecret = config.getEnv('userManagement.jwtSecret');

public signData(payload: object, options: SignInOptions = {}): string {
return this.sign(payload, this.userManagementSecret, options);
}

public verify(token: string, secret: string, options: jwt.VerifyOptions = {}) {
return jwt.verify(token, secret, options);
public verifyToken(token: string, options: VerifyOptions = {}) {
return this.verify(token, this.userManagementSecret, options);
}
}

export type JwtPayload = jwt.JwtPayload;
31 changes: 7 additions & 24 deletions packages/cli/test/integration/passwordReset.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { JwtService } from '@/services/jwt.service';
import { Container } from 'typedi';

jest.mock('@/UserManagement/email/NodeMailer');
config.set('userManagement.jwtSecret', randomString(5, 10));

let globalOwnerRole: Role;
let globalMemberRole: Role;
Expand All @@ -32,7 +33,6 @@ const jwtService = Container.get(JwtService);
beforeAll(async () => {
globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole();
config.set('userManagement.jwtSecret', randomString(5, 10));
});

beforeEach(async () => {
Expand Down Expand Up @@ -133,10 +133,7 @@ describe('GET /resolve-password-token', () => {
});

test('should succeed with valid inputs', async () => {
const resetPasswordToken = jwtService.sign(
{ sub: owner.id },
config.getEnv('userManagement.jwtSecret'),
);
const resetPasswordToken = jwtService.signData({ sub: owner.id });

const response = await testServer.authlessAgent
.get('/resolve-password-token')
Expand All @@ -160,7 +157,7 @@ describe('GET /resolve-password-token', () => {
});

test('should fail if user is not found', async () => {
const token = jwtService.sign({ sub: 'test' }, config.getEnv('userManagement.jwtSecret'));
const token = jwtService.signData({ sub: 'test' });

const response = await testServer.authlessAgent
.get('/resolve-password-token')
Expand All @@ -170,11 +167,7 @@ describe('GET /resolve-password-token', () => {
});

test('should fail if token is expired', async () => {
const resetPasswordToken = jwtService.sign(
{ sub: owner.id },
config.getEnv('userManagement.jwtSecret'),
{ expiresIn: '-1h' },
);
const resetPasswordToken = jwtService.signData({ sub: owner.id }, { expiresIn: '-1h' });

const response = await testServer.authlessAgent
.get('/resolve-password-token')
Expand All @@ -188,10 +181,7 @@ describe('POST /change-password', () => {
const passwordToStore = randomValidPassword();

test('should succeed with valid inputs', async () => {
const resetPasswordToken = jwtService.sign(
{ sub: owner.id },
config.getEnv('userManagement.jwtSecret'),
);
const resetPasswordToken = jwtService.signData({ sub: owner.id });
const response = await testServer.authlessAgent.post('/change-password').send({
token: resetPasswordToken,
userId: owner.id,
Expand All @@ -218,10 +208,7 @@ describe('POST /change-password', () => {
});

test('should fail with invalid inputs', async () => {
const resetPasswordToken = jwtService.sign(
{ sub: owner.id },
config.getEnv('userManagement.jwtSecret'),
);
const resetPasswordToken = jwtService.signData({ sub: owner.id });

const invalidPayloads = [
{ token: uuid() },
Expand Down Expand Up @@ -254,11 +241,7 @@ describe('POST /change-password', () => {
});

test('should fail when token has expired', async () => {
const resetPasswordToken = jwtService.sign(
{ sub: owner.id },
config.getEnv('userManagement.jwtSecret'),
{ expiresIn: '-1h' },
);
const resetPasswordToken = jwtService.signData({ sub: owner.id }, { expiresIn: '-1h' });

const response = await testServer.authlessAgent.post('/change-password').send({
token: resetPasswordToken,
Expand Down
Loading