Skip to content
Merged

#65 #67

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
4 changes: 4 additions & 0 deletions config/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ database:
migrations: false
cli:
"migrationsDir": "test/e2e/migrations"
jwt:
secret: 'secret_code'
expiresIn: 3600
ignoreExpiration: false
security:
headerTokenToLogin: null
whitelistedIp: null
Expand Down
7 changes: 7 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,11 @@ export class AuthController {
public getProfile(@Request() req): AuthModelInterface {
return req.user;
}

@UseGuards(JwtAuthGuard)
@Get('refreshToken')
@ApiBearerAuth()
public refreshToken(@Request() req): Promise<AccessToken> {
return this.loginService.login(req.user);
}
}
15 changes: 6 additions & 9 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import {Module} from '@nestjs/common';
import {ConfigModule, ConfigService} from '@nestjs/config';
import {APP_GUARD} from '@nestjs/core';
import {JwtModule, JwtService} from '@nestjs/jwt';
import {JwtModule} from '@nestjs/jwt';
import {JwtModuleOptions} from '@nestjs/jwt/dist/interfaces/jwt-module-options.interface';
import {PassportModule} from '@nestjs/passport';
import {Request} from 'express';
import {CLS_REQ, ClsModule} from 'nestjs-cls';
import {ExtractJwt} from 'passport-jwt';

import {AuthController} from '@warp-core/auth/auth.controller';
import {GqlAuthGuard} from '@warp-core/auth/guard/gql-auth.guard';
import {PayloadInterface} from '@warp-core/auth/interface/payload.interface';
import {JwtService} from '@warp-core/auth/jwt.service';
import {LoginByHabitatService} from '@warp-core/auth/login/login-by-habitat.service';
import {AuthorizedHabitatModel} from '@warp-core/auth/payload/model/habitat.model';
import {RegisterService} from '@warp-core/auth/register/register.service';
Expand Down Expand Up @@ -49,6 +48,7 @@ const jwtFactory = {
},
LocalStrategy,
JwtStrategy,
JwtService,
{
provide: APP_GUARD,
useClass: GqlAuthGuard,
Expand All @@ -63,17 +63,14 @@ const jwtFactory = {
ClsModule.forFeatureAsync({
global: true,
provide: AuthorizedHabitatModel,
imports: [DatabaseModule, JwtModule],
imports: [DatabaseModule, AuthModule],
inject: [CLS_REQ, HabitatRepository, JwtService],
useFactory: async (
req: Request,
habitatRepository: HabitatRepository,
jwtService: JwtService,
) => {
const extractJwt = ExtractJwt.fromAuthHeaderAsBearerToken();
const payload = jwtService.decode(
extractJwt(req) ?? '',
) as PayloadInterface;
const payload = jwtService.decode(req);

if (!payload) {
return null;
Expand All @@ -87,6 +84,6 @@ const jwtFactory = {
}),
],
controllers: [AuthController],
exports: [ClsModule],
exports: [ClsModule, JwtService],
})
export class AuthModule {}
34 changes: 34 additions & 0 deletions src/auth/jwt.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Injectable} from '@nestjs/common';
import {ConfigService} from '@nestjs/config';
import {JwtService as NestJwtService} from '@nestjs/jwt';
import {Request} from 'express';
import {ExtractJwt} from 'passport-jwt';

import {PayloadInterface} from '@warp-core/auth/interface/payload.interface';
import {AccessToken} from '@warp-core/auth/login/access-token.model';

@Injectable()
export class JwtService {
constructor(
private readonly nestJwtService: NestJwtService,
private readonly configService: ConfigService,
) {}

public verifyAsync(token: string): Promise<unknown> {
return this.nestJwtService.verifyAsync(token, {
secret: this.configService.get('jwt.secret'),
});
}

public sign(payload: PayloadInterface): AccessToken {
return {
access_token: this.nestJwtService.sign(payload),
};
}

public decode(request: Request): PayloadInterface | undefined {
const extractJwt = ExtractJwt.fromAuthHeaderAsBearerToken();

return this.nestJwtService.decode(extractJwt(request) ?? '');
}
}
6 changes: 2 additions & 4 deletions src/auth/login/login-by-habitat.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Injectable} from '@nestjs/common';
import {JwtService} from '@nestjs/jwt';

import {AccessToken} from './access-token.model';
import {LoginInterface} from './login.interface';

import {PayloadInterface} from '@warp-core/auth/interface/payload.interface';
import {JwtService} from '@warp-core/auth/jwt.service';
import {AuthorizedHabitatModel} from '@warp-core/auth/payload/model/habitat.model';

@Injectable()
Expand All @@ -18,8 +18,6 @@ export class LoginByHabitatService implements LoginInterface {
currentHabitatId: habitat.id,
} as PayloadInterface;

return {
access_token: `${this.jwtService.sign(payload)}`,
} as AccessToken;
return this.jwtService.sign(payload);
}
}
12 changes: 10 additions & 2 deletions src/auth/strategy/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
});
}

public validate(payload: PayloadInterface): AuthModelInterface {
return payload.dbModel;
public validate(
payload: PayloadInterface & {
iat: number;
exp: number;
},
): AuthModelInterface & {
iat: number;
exp: number;
} {
return {...payload.dbModel, iat: payload.iat, exp: payload.exp};
}
}
86 changes: 86 additions & 0 deletions test/e2e/auth/token.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {HttpStatus, INestApplication} from '@nestjs/common';
import * as request from 'supertest';

import {LoginParameters} from '@warp-core/auth/login/login-parameters.model';
import {requestGraphQL} from '@warp-core/test/e2e/utils/graphql-request-test';
import {GraphqlRequestTest} from '@warp-core/test/e2e/utils/graphql-request-test/graphql-request-test';
import {createNestApplicationE2E} from '@warp-core/test/e2e/utils/setup-tests';

const startTimestamp = 1760083810;
const tokenRegex = /(^[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$)/;

describe('Token', () => {
let requestTest: GraphqlRequestTest;
let app: INestApplication;
const userId = 10;
let loginParameters: LoginParameters;
let loginToken: string;
const tokenExpirationInSeconds = 3600;

beforeEach(async () => {
app = await createNestApplicationE2E();
requestTest = requestGraphQL(app.getHttpServer());
jest.useFakeTimers().setSystemTime(new Date(startTimestamp * 1000));
loginParameters = await requestTest.register(userId);
loginToken = await requestTest.authenticate(loginParameters);
});

afterEach(() => {
jest.useRealTimers();
});

it('should login token be created', () => {
expect(loginToken).toMatch(tokenRegex);
});

it('should return profile info', async () => {
const profileResponse = await request(app.getHttpServer())
.get(`/auth/profile`)
.set('Authorization', requestTest.prepareAuthHeader())
.expect(HttpStatus.OK);

const profileData = profileResponse.body;

expect(profileData.id).toEqual(loginParameters.habitatId);
expect(profileData.userId).toEqual(loginParameters.userId);
expect(profileData.iat).toEqual(startTimestamp);
expect(profileData.exp).toEqual(startTimestamp + tokenExpirationInSeconds);
});

it('should refresh token with new iat and exp date', async () => {
const prolongedTimestamp = startTimestamp + 3000;
jest.useFakeTimers().setSystemTime(new Date(prolongedTimestamp * 1000));

const refreshedTokenResponse = await request(app.getHttpServer())
.get(`/auth/refreshToken`)
.set('Authorization', requestTest.prepareAuthHeader())
.expect(HttpStatus.OK);
const refreshedToken = refreshedTokenResponse.body.access_token;

expect(refreshedToken).toMatch(tokenRegex);

const profileResponse = await request(app.getHttpServer())
.get(`/auth/profile`)
.set('Authorization', `Bearer ${refreshedToken}`)
.expect(HttpStatus.OK);

const profileData = profileResponse.body;

expect(profileData.id).toEqual(loginParameters.habitatId);
expect(profileData.userId).toEqual(loginParameters.userId);
expect(profileData.iat).toEqual(prolongedTimestamp);
expect(profileData.exp).toEqual(
prolongedTimestamp + tokenExpirationInSeconds,
);
});

it('should not refresh token', () => {
const prolongedTimestamp = startTimestamp + 9999;
jest.useFakeTimers().setSystemTime(new Date(prolongedTimestamp * 1000));

return request(app.getHttpServer())
.get(`/auth/refreshToken`)
.set('Authorization', requestTest.prepareAuthHeader())
.expect(HttpStatus.UNAUTHORIZED);
});
});
6 changes: 3 additions & 3 deletions test/e2e/utils/graphql-request-test/graphql-request-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export class GraphqlRequestTest {
return registerResponse.body;
}

public async authenticate(loginParameters: LoginParameters): Promise<this> {
public async authenticate(loginParameters: LoginParameters): Promise<string> {
const loginResponse = await this.supertest
.post('/auth/login')
.send(loginParameters)
.expect(HttpStatus.CREATED);

this.loginToken = loginResponse.body.access_token;

return this;
return this.loginToken;
}

public async registerAndAuthenticate(userId: number): Promise<this> {
Expand Down Expand Up @@ -78,7 +78,7 @@ export class GraphqlRequestTest {
return generateGraphQLQuery(this.operationName, this.queryParameters);
}

private prepareAuthHeader(): string {
public prepareAuthHeader(): string {
if (this.loginToken) {
return `Bearer ${this.loginToken}`;
}
Expand Down
Loading