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

Reset password #262

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions devspace.dev.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ version: v2beta1
name: stalker-vars-dev

vars:
STALKER_APP_BASE_URL: https://your-base-url
10 changes: 10 additions & 0 deletions devspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,16 @@ deployments:
value: ${JM_KAFKA_KEY_PASSWORD}
- name: STALKER_VERSION
value: ${STALKER_VERSION}
- name: EMAIL_SENDER
value: ${EMAIL_SENDER}
- name: MAILJET_API_KEY
value: ${JM_MAILJET_API_KEY}
- name: MAILJET_API_SECRET
value: ${JM_MAILJET_API_SECRET}
- name: EMAIL_RECIPIENTS_FILTER_LIST
value: ${JM_EMAIL_RECIPIENTS_FILTER_LIST}
- name: STALKER_APP_BASE_URL
value: ${STALKER_APP_BASE_URL}

image: jobs-manager
name: jobs-manager-container
Expand Down
1 change: 1 addition & 0 deletions packages/backend/jobs-manager/service/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ COPY --from=build /app/packages/backend/jobs-manager/service/ssl-certificate-cha
COPY --from=build /app/packages/backend/jobs-manager/service/src/modules/database/subscriptions/event-subscriptions/built-in/ /server/dist/src/modules/database/subscriptions/event-subscriptions/built-in/
COPY --from=build /app/packages/backend/jobs-manager/service/src/modules/database/subscriptions/cron-subscriptions/built-in/ /server/dist/src/modules/database/subscriptions/cron-subscriptions/built-in/
COPY --from=build /app/packages/backend/jobs-manager/service/src/modules/database/custom-jobs/built-in/ /server/dist/src/modules/database/custom-jobs/built-in/
COPY --from=build /app/packages/backend/jobs-manager/service/src/modules/notifications/emails/templates/ /server/dist/src/modules/notifications/emails/templates/


ENTRYPOINT ["node", "/server/dist/src/main"]
9 changes: 7 additions & 2 deletions packages/backend/jobs-manager/service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@
"class-validator": "^0.14.0",
"cron-validator": "^1.3.1",
"dot-object": "^2.1.4",
"handlebars": "^4.7.8",
"jwt-decode": "^3.1.2",
"kafkajs": "^2.2.3",
"mjml": "^4.15.3",
"mongodb-memory-server": "^8.11.4",
"mongoose": "^6.10.0",
"node-forge": "^1.3.1",
"node-mailjet": "^6.0.5",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-unique-token": "^3.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^4.1.2",
"rxjs": "^7.8.0",
Expand All @@ -63,10 +67,11 @@
"@nestjs/testing": "^9.2.1",
"@types/express": "^4.17.16",
"@types/jest": "^29.5.1",
"@types/mjml": "^4",
"@types/node": "^18.11.18",
"@types/passport-jwt": "^3.0.8",
"@types/passport-local": "^1.0.35",
"@types/supertest": "^2.0.12",
"@types/supertest": "~6.0.2",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"argon2": "^0.30.3",
Expand All @@ -77,7 +82,7 @@
"lint-staged": "^13.1.0",
"mongodb": "^4.13.0",
"prettier": "^2.8.3",
"supertest": "^6.3.3",
"supertest": "~7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import request from 'supertest';
import { AppModule } from './app.module';

describe('AppController (e2e)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { DatabaseModule } from './database/database.module';
import { FindingsModule } from './findings/findings.module';
import './notifications/emails/email.service';
import { NotificationsModule } from './notifications/notifications.module';

@Module({
imports: [DatabaseModule, AuthModule, FindingsModule],
imports: [DatabaseModule, AuthModule, FindingsModule, NotificationsModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import JwtRefreshGuard from './guards/jwt-refresh.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { MagicLinkAuthGuard } from './guards/magic-link.guard';

@Controller('/auth')
export class AuthController {
Expand All @@ -21,18 +22,13 @@ export class AuthController {
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
const accessToken = await this.authService.createAccessToken({
email: req.user.email,
id: req.user._id,
role: req.user.role,
});
const refreshToken: string = await this.authService.createRefreshToken(
req.user._id,
);
return {
access_token: accessToken,
refresh_token: refreshToken,
};
return await this.loginCore(req);
}

@UseGuards(MagicLinkAuthGuard)
@Post('login-magic-link')
async loginMagicLink(@Request() req) {
return await this.loginCore(req);
}

@UseGuards(JwtRefreshGuard)
Expand All @@ -59,10 +55,24 @@ export class AuthController {
/**
* This function is left without authorizations on purpose
* It is used to anonymously know if the platform was properly initialized
* @returns
*/
@Get('setup')
async getIsSetup(): Promise<any> {
return { isSetup: await this.authService.isAuthenticationSetup() };
}

private async loginCore(req: any) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the type here is something like ExpressRequest or something

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { Request } from 'express';
// [...]

private async loginCore(req: Request) {

const accessToken = await this.authService.createAccessToken({
email: req.user.email,
id: req.user._id,
role: req.user.role,
});
const refreshToken: string = await this.authService.createRefreshToken(
req.user._id,
);
return {
access_token: accessToken,
refresh_token: refreshToken,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { HttpStatus, INestApplication } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { randomUUID } from 'crypto';
import jwt_decode from 'jwt-decode';
import * as request from 'supertest';
import { Model, Types } from 'mongoose';
import request from 'supertest';
import {
admin,
createUser,
deleteReq,
login,
loginMagicLinkToken,
putReq,
} from '../../test/e2e.utils';
import { AppModule } from '../app.module';
import { MagicLinkToken } from '../database/users/magic-link-token.model';
import { Role } from './constants';

describe('Auth Controller (e2e)', () => {
Expand Down Expand Up @@ -162,6 +166,46 @@ describe('Auth Controller (e2e)', () => {
expect(r.statusCode).toBe(HttpStatus.UNAUTHORIZED);
});

it('Should connect as the magic link user (POST /auth/login-magic-link)', async () => {
// Arrange
const magicLinkToken = app.get<Model<MagicLinkToken>>(
getModelToken('magicLinkTokens'),
);

await magicLinkToken.create({
expirationDate: new Date().getTime() + 100000,
token: '1234',
userId: new Types.ObjectId(testAdmin.id),
});

// Act
const r = await loginMagicLinkToken(app, '1234');

// Assert
expect(r.statusCode).toBe(HttpStatus.CREATED);
expect(r.body.access_token).toBeTruthy();
expect(r.body.refresh_token).toBeTruthy();
const decodedToken: any = jwt_decode(r.body.access_token);
const decodedRefresh: any = jwt_decode(r.body.refresh_token);
expect(decodedToken.id).toBeTruthy();
expect(decodedToken.email).toBe(testAdmin.email);
expect(decodedToken.role).toBe(Role.UserResetPassword);
expect(decodedRefresh.id).toBeTruthy();
expect(decodedToken.exp < decodedRefresh.exp).toBeTruthy();

token = r.body.access_token;
refresh = r.body.refresh_token;
});

it('Should connect as the magic link user (POST /auth/login-magic-link)', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('Should connect as the magic link user (POST /auth/login-magic-link)', async () => {
it('Should not connect as the magic link user (POST /auth/login-magic-link)', async () => {

// Arrange
// Act
const r = await loginMagicLinkToken(app, 'iamnotvalid');

// Assert
expect(r.statusCode).toBe(HttpStatus.UNAUTHORIZED);
});

afterAll(async () => {
await deleteReq(app, token, `/users/${inactiveUser.id}`);
await deleteReq(app, token, `/users/${testAdmin.id}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { jwtConstants } from './constants';
import { JwtSocketioStrategy } from './strategies/jwt-socketio.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { MagicLinkStrategy } from './strategies/magic-link.strategy';
import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';

@Module({
Expand All @@ -23,6 +24,7 @@ import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';
providers: [
AuthService,
LocalStrategy,
MagicLinkStrategy,
JwtStrategy,
RefreshTokenStrategy,
JwtSocketioStrategy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export const jwtConstants = {
expirationTime: '300s',
};

export const resetPasswordConstants = {
expirationTimeSeconds: 3600,
};

export const rtConstants = {
secret: process.env['JM_REFRESH_SECRET'],
expirationTime: '25200s',
Expand All @@ -23,19 +27,38 @@ export const orchestratorConstants = {

export enum Role {
User = 'user',
UserResetPassword = 'user-reset-password',
Admin = 'admin',
ReadOnly = 'read-only',
}

export function roleIsAuthorized(role: string, requiredRole: string): boolean {
if (role === Role.Admin) return true;
if (
role === Role.User &&
(requiredRole === Role.ReadOnly || requiredRole === Role.User)
)
return true;
export function roleIsAuthorized(
userRole: string,
requiredRole: string,
): boolean {
const allowedRoles = expandRole(userRole);

if (role !== requiredRole) throw new UnauthorizedException();
if (!allowedRoles.includes(requiredRole)) throw new UnauthorizedException();

return true;
}

/** Returns all the roles allowed by the given role. */
function expandRole(role: string): string[] {
switch (role) {
case Role.Admin:
return [Role.Admin, Role.User, Role.ReadOnly, Role.UserResetPassword];

case Role.User:
return [Role.User, Role.ReadOnly, Role.UserResetPassword];

case Role.ReadOnly:
return [Role.ReadOnly, Role.UserResetPassword];

case Role.UserResetPassword:
return [Role.UserResetPassword];

default:
return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class MagicLinkAuthGuard extends AuthGuard('magic-link') {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { UniqueTokenStrategy } from 'passport-unique-token';
import { UsersService } from '../../database/users/users.service';

@Injectable()
export class MagicLinkStrategy extends PassportStrategy(
UniqueTokenStrategy,
'magic-link',
) {
constructor(private readonly usersService: UsersService) {
super();
}

public async validate(token: string) {
const user = await this.usersService.validateIdentityUsingUniqueToken(
token,
);

if (!user) {
throw new UnauthorizedException();
}

return user;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpStatus, INestApplication, ValidationPipe } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { useContainer } from 'class-validator';
import * as request from 'supertest';
import request from 'supertest';
import {
TestingData,
checkAuthorizations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('Cron Subscriptions Service', () => {
moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();

projectService = moduleFixture.get(ProjectService);
subscriptionsService = moduleFixture.get(CronSubscriptionsService);
domainsService = moduleFixture.get(DomainsService);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type MagicLinkTokenDocument = MagicLinkToken & Document;

@Schema()
export class MagicLinkToken {
@Prop({ unique: true })
public token: string;

@Prop()
public userId: string;

@Prop()
public expirationDate: number;
}

export const MagicLinkTokenSchema =
SchemaFactory.createForClass(MagicLinkToken);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsEmail, IsNotEmpty } from 'class-validator';

export class ResetPasswordRequestDto {
@IsNotEmpty()
@IsEmail()
email: string;
}
Loading
Loading