diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2dad2515989d9b64f9f0a80c6becd3d313c776f5..e7225378e2908aa2d7b598966c5e77525b70766d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ variables: LEGACY_AUTH_SERVICE_URL: https://download.data.grandlyon.com/auth ADMIN_USERNAME: data-beta-grandlyon-com USER_SUPPORT_MAILBOX: alpha-test@erasme.org + ACCESS_TOKEN_COOKIE_KEY: access_token build_development: stage: build diff --git a/docker-compose.yml b/docker-compose.yml index d5f18171e2e8a43dbee9593ff322347606d872cf..516ae78387a122b0707db8de644b69de32dc4036 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,10 +14,11 @@ services: - USER_SUPPORT_MAILBOX=${USER_SUPPORT_MAILBOX} - FRONT_END_URL=${FRONT_END_URL} - API_KEY=${API_KEY} + - ACCESS_TOKEN_COOKIE_KEY=${ACCESS_TOKEN_COOKIE_KEY} restart: unless-stopped depends_on: - redis - + redis: container_name: middleware-legacy-aut-redis-${TAG} image: redis:5.0.0-alpine diff --git a/package-lock.json b/package-lock.json index 20f830589443d37b8ecda461091636241c0a7745..f28c6aa0737c98bb687c92210510f1cbf4ca0fbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "middleware-legacy-auth", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2064,6 +2064,15 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/package.json b/package.json index 3e6fd24cc6092944270f69f8e08c189ed6ef0140..f986fbdfae748b48cfee2fabaa70655761a4c495 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "middleware-legacy-auth", - "version": "1.0.0", + "version": "2.0.0", "description": "description", "author": "", "license": "MIT", @@ -26,6 +26,7 @@ "bluebird": "^3.5.3", "class-transformer": "^0.2.0", "class-validator": "^0.9.1", + "cookie-parser": "^1.4.4", "dotenv": "^6.1.0", "jsonwebtoken": "^8.4.0", "moment": "^2.24.0", diff --git a/src/app.module.ts b/src/app.module.ts index 9cd9d0bd911c6345fc17feb81def946021b94097..1c2198fe2ad7f3d25c3b017643fc1f660d208b2c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,7 @@ import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { LegacyModule } from './legacy/legacy.module'; import { ConfigModule } from './configuration/config.module'; -import { DecodeJWTPayloadMiddleware } from './middlewares/decode-jwt-payload.middleware'; +import { VerifyXsrfTokenAndDecodeJWTPayloadMiddleware } from './middlewares/decode-jwt-payload.middleware'; @Module({ imports: [ConfigModule, LegacyModule], @@ -10,7 +10,7 @@ export class AppModule { configure(consumer: MiddlewareConsumer) { // Applying the middleware that takes the Authorization header jwt payload and put it in the request headers consumer - .apply(DecodeJWTPayloadMiddleware).forRoutes( + .apply(VerifyXsrfTokenAndDecodeJWTPayloadMiddleware).forRoutes( { path: 'user', method: RequestMethod.GET }, { path: 'user', method: RequestMethod.DELETE }, { path: 'user/resources', method: RequestMethod.GET }, diff --git a/src/configuration/config.service.ts b/src/configuration/config.service.ts index 7fc9a826b9016b05746da771a011029dc6cb6897..a0a45717da7d8369b6bfecf6eee3db94dcbfd862 100644 --- a/src/configuration/config.service.ts +++ b/src/configuration/config.service.ts @@ -25,6 +25,7 @@ export class ConfigService { this._config.userSupportMailbox = process.env.USER_SUPPORT_MAILBOX; this._config.frontEnd.url = process.env.FRONT_END_URL; this._config.apiKey = process.env.API_KEY; + this._config.accessTokenCookieKey = process.env.ACCESS_TOKEN_COOKIE_KEY; this.initilizePublicPrivateKeys(); } diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 9dbef1dcd287820576fe27e4fd1a42ba869aede1..513e46ad51beba90b1bb07511b701a428f960994 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -19,4 +19,5 @@ export const Config = { }, imageHost: 'https://highway-to-data.alpha.grandlyon.com/email-template-assets', apiKey: '', + accessTokenCookieKey: '', }; \ No newline at end of file diff --git a/src/legacy/legacy.controller.ts b/src/legacy/legacy.controller.ts index 9a5ec8c418c5c4913a7bfd39d02fc2de86ea55ff..cdd7cb9aa2f613cd1d033f3619509bd520efb47f 100644 --- a/src/legacy/legacy.controller.ts +++ b/src/legacy/legacy.controller.ts @@ -17,7 +17,11 @@ export class LegacyController { @Get('user') @ApiOperation({ title: 'Get user info.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiResponse({ status: 200, description: 'User is existing, returning its info', type: UserInfo }) @ApiResponse({ status: 400, description: 'Bad Request' }) @ApiResponse({ status: 500, description: 'Internal error' }) @@ -100,7 +104,11 @@ export class LegacyController { @Delete('user') @ApiOperation({ title: 'Delete user account.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiResponse({ status: 204, description: 'Account deleted' }) @ApiResponse({ status: 400, description: 'Bad Request (Invalid user credentials.)' }) @ApiResponse({ status: 500, description: 'Internal error' }) @@ -123,7 +131,11 @@ export class LegacyController { @Put('user/updatePassword') @ApiOperation({ title: 'Check if the user exist, if the old password is correct and then update with the new password.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiResponse({ status: 200, description: 'Success' }) @ApiResponse({ status: 400, description: 'Bad Request (user not found, password incorrect)' }) @ApiResponse({ status: 500, description: 'Internal error' }) @@ -145,7 +157,11 @@ export class LegacyController { @Put('user/update') @ApiOperation({ title: 'Check if the user exist, update with the new info.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiResponse({ status: 200, description: 'Success', type: UserInfo }) @ApiResponse({ status: 400, description: 'Bad Request (user not found, password incorrect)' }) @ApiResponse({ status: 500, description: 'Internal error' }) @@ -214,7 +230,11 @@ export class LegacyController { @Get('user/resources') @ApiOperation({ title: 'Get the list of accessible resources by the specified user.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiResponse({ status: 200, description: 'Success, returns accessible resources', type: [Resource] }) @ApiResponse({ status: 401, description: 'Authorization is not set, not a valid token, or user credentials are not correct'}) @ApiResponse({ status: 500, description: 'Internal error' }) @@ -237,7 +257,11 @@ export class LegacyController { @Post('user/resources/add') @ApiOperation({ title: 'Request access to a restricted access dataset for an existing user.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiImplicitBody({ name: 'body', type: AccessRequest, isArray: true }) @ApiResponse({ status: 201, description: 'Request created', type: AccessRequestResponse }) @ApiResponse({ status: 400, description: 'Bad Request (Mode does not exist)' }) @@ -261,7 +285,11 @@ export class LegacyController { @Post('user/resources/renew') @ApiOperation({ title: 'Renew access to a restricted access dataset for an existing user.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiImplicitBody({ name: 'body', type: AccessRequest }) @ApiResponse({ status: 201, description: 'Request created', type: AccessRenewalResponse }) @ApiResponse({ status: 400, description: 'Bad Request (Mode does not exist)' }) @@ -285,7 +313,11 @@ export class LegacyController { @Delete('user/resources/delete') @ApiOperation({ title: 'Delete access to a service.' }) - @ApiImplicitHeader({ name: 'Authorization', description: 'Bearer token'}) + @ApiImplicitHeader({ + name: 'Cookie', + description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)', + }) + @ApiImplicitHeader({ name: 'x-xsrf-token', description: 'Xsrf Token'}) @ApiImplicitBody({ name: 'body', type: AccessRequest, isArray: true }) @ApiResponse({ status: 200, description: 'Access removed', type: AccessDeletionResponse }) @ApiResponse({ status: 500, description: 'Internal error' }) diff --git a/src/main.ts b/src/main.ts index d237ae314cd5028d62a271b7eafb414702a83bbb..8439bddee86a1039b345badbb1f9d579cb12d969 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,10 +2,12 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; +import * as cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ credentials: true, origin: true }); + app.use(cookieParser()); const options = new DocumentBuilder() .setBasePath('') diff --git a/src/middlewares/decode-jwt-payload.middleware.ts b/src/middlewares/decode-jwt-payload.middleware.ts index 80ada259fc5e29a1d017850741056f45ebf417ff..8b7e055396bbaef091128f04e6b06a2c7886adf4 100644 --- a/src/middlewares/decode-jwt-payload.middleware.ts +++ b/src/middlewares/decode-jwt-payload.middleware.ts @@ -1,23 +1,38 @@ import { UnauthorizedException, Logger, NestMiddleware, MiddlewareFunction, Injectable } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; +import { ConfigService } from '../configuration/config.service'; @Injectable() -export class DecodeJWTPayloadMiddleware implements NestMiddleware { +export class VerifyXsrfTokenAndDecodeJWTPayloadMiddleware implements NestMiddleware { + + constructor( + private _configService: ConfigService, + ) {} + resolve(): MiddlewareFunction { return (req, res, next) => { - Logger.log('[-] DecodeJWTPayloadMiddleware'); - if (req.headers['x-anonymous-consumer'] !== 'true' && req.headers.authorization) { - const arr = req.headers.authorization.split(' '); - const token = jwt.decode(arr[1]); + Logger.log('[-] VerifyXsrfTokenAndDecodeJWTPayloadMiddleware'); + // Verifying that all the authentications part are set + // tslint:disable-next-line:max-line-length + if (req.headers['x-anonymous-consumer'] !== 'true' && req.cookies[this._configService.config.accessTokenCookieKey] && req.headers['x-xsrf-token']) { + let token = req.cookies[this._configService.config.accessTokenCookieKey]; + const xsrfToken = req.headers['x-xsrf-token']; + token = jwt.decode(token); + // Make sure the token has correctly been decrypted if (token) { - req.headers.token = token; - next(); + // Verify the wsrfToken from the header with the value of the corresponding in the JWT payload + if (token.xsrfToken === xsrfToken) { + req.headers.token = token; + next(); + } else { + throw new UnauthorizedException('Could\'t verify xsrf token.'); + } } else { throw new UnauthorizedException('Invalid token provided.'); } } else { - throw new UnauthorizedException('No authorization header provided.'); + throw new UnauthorizedException('Missing credential information.'); } }; } -} \ No newline at end of file +}