Prechádzať zdrojové kódy

Adding fingerprint cookie to authentication

bodicsek 3 rokov pred
rodič
commit
e076440ad2

+ 58 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "occ-fw-backend",
-  "version": "0.0.2",
+  "version": "0.0.8",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "occ-fw-backend",
-      "version": "0.0.2",
+      "version": "0.0.8",
       "license": "UNLICENSED",
       "dependencies": {
         "@nestjs/axios": "^0.1.0",
@@ -15,6 +15,7 @@
         "@nestjs/mapped-types": "*",
         "@nestjs/passport": "^9.0.0",
         "@nestjs/platform-express": "^9.0.0",
+        "cookie-parser": "^1.4.6",
         "passport": "^0.6.0",
         "passport-jwt": "^4.0.0",
         "reflect-metadata": "^0.1.13",
@@ -25,6 +26,7 @@
         "@nestjs/cli": "^9.0.0",
         "@nestjs/schematics": "^9.0.0",
         "@nestjs/testing": "^9.0.0",
+        "@types/cookie-parser": "^1.4.3",
         "@types/express": "^4.17.13",
         "@types/jest": "28.1.8",
         "@types/node": "^16.0.0",
@@ -1909,6 +1911,15 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/cookie-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
+      "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
+      "dev": true,
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
     "node_modules/@types/cookiejar": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -3321,6 +3332,26 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/cookie-parser": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+      "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+      "dependencies": {
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/cookie-parser/node_modules/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/cookie-signature": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -9972,6 +10003,15 @@
         "@types/node": "*"
       }
     },
+    "@types/cookie-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
+      "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
+      "dev": true,
+      "requires": {
+        "@types/express": "*"
+      }
+    },
     "@types/cookiejar": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -11065,6 +11105,22 @@
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
       "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
     },
+    "cookie-parser": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+      "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+      "requires": {
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+          "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+        }
+      }
+    },
     "cookie-signature": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",

+ 2 - 0
package.json

@@ -35,6 +35,7 @@
     "@nestjs/mapped-types": "*",
     "@nestjs/passport": "^9.0.0",
     "@nestjs/platform-express": "^9.0.0",
+    "cookie-parser": "^1.4.6",
     "passport": "^0.6.0",
     "passport-jwt": "^4.0.0",
     "reflect-metadata": "^0.1.13",
@@ -45,6 +46,7 @@
     "@nestjs/cli": "^9.0.0",
     "@nestjs/schematics": "^9.0.0",
     "@nestjs/testing": "^9.0.0",
+    "@types/cookie-parser": "^1.4.3",
     "@types/express": "^4.17.13",
     "@types/jest": "28.1.8",
     "@types/node": "^16.0.0",

+ 22 - 4
src/auth/auth.controller.ts

@@ -1,13 +1,31 @@
-import { Body, Controller, Post } from '@nestjs/common';
+import { Body, Controller, Post, Res } from '@nestjs/common';
+import { Response } from 'express';
+import { tap } from 'rxjs';
 import { AuthService } from './auth.service';
 import { DoLoginDto } from './dto/do-login.dto';
 
 @Controller('auth')
 export class AuthController {
-  constructor(private authService: AuthService) {}
+  constructor(private authService: AuthService) { }
 
   @Post('login')
-  login(@Body() loginDto: DoLoginDto) {
-    return this.authService.login(loginDto.code, loginDto.nonce);
+  login(@Body() loginDto: DoLoginDto, @Res({ passthrough: true }) response: Response) {
+    return this.authService.login(loginDto.code, loginDto.nonce).pipe(
+      tap(({ accessToken, expiresAt }) => {
+        const { fingerprint, fingerprintHash } = this.authService.calculateFingerprint(accessToken)
+        response.cookie('__Host-Fgp', fingerprint, {
+          expires: expiresAt,
+          sameSite: 'strict',
+          httpOnly: true,
+          secure: true
+        });
+        response.cookie('__Host-FgpHash', fingerprintHash, {
+          expires: expiresAt,
+          sameSite: 'strict',
+          httpOnly: true,
+          secure: true
+        });
+      })
+    );
   }
 }

+ 10 - 4
src/auth/auth.jwt.strategy.ts

@@ -1,7 +1,9 @@
 
 import { ExtractJwt, Strategy } from 'passport-jwt';
 import { PassportStrategy } from '@nestjs/passport';
-import { Injectable } from '@nestjs/common';
+import { Injectable, Req } from '@nestjs/common';
+import { Request } from 'express';
+import { AuthService } from './auth.service';
 
 const publicKey =
 `-----BEGIN CERTIFICATE-----
@@ -28,15 +30,19 @@ pMc56p6t/EzQ
 
 @Injectable()
 export class JwtStrategy extends PassportStrategy(Strategy) {
-  constructor() {
+  constructor(private authService: AuthService) {
     super({
       jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
       ignoreExpiration: false,
-      secretOrKey: publicKey
+      secretOrKey: publicKey,
+      passReqToCallback: true
     });
   }
 
-  async validate(payload: any) {
+  async validate(@Req() request: Request, payload: any) {
+    if (!this.authService.validateFingerprint(request.cookies['__Host-Fgp'], request.cookies['__Host-FgpHash'], payload)) {
+      return null;
+    }
     return { userId: payload.sub, username: payload.user_displayname };
   }
 }

+ 18 - 5
src/auth/auth.service.ts

@@ -1,16 +1,13 @@
 import { HttpService } from '@nestjs/axios';
 import { BadRequestException, Injectable } from '@nestjs/common';
+import { randomBytes, createHash } from 'crypto';
 import { readFileSync } from 'fs';
 import { catchError, map, throwError } from 'rxjs';
 
 @Injectable()
 export class AuthService {
-  // private idcsStripe =
-  //   'https://idcs-25070016ce0c4eb8b6eea18f07fe170d.identity.oraclecloud.com';
   private idcsStripe = 'https://idcs-login-stage.identity.oraclecloud.com';
-  // private clientId = '4e728d65cf5b482ea81e56bf23a9ad8a';
   private clientId = '754db2d1964d4f12ab312a2ab6f025ed';
-  // private clientSecret = 'dc547dbe-8d4b-4155-a378-f3a03d56a654';
   private clientSecret = '';
 
   constructor(private httpService: HttpService) {
@@ -27,7 +24,7 @@ export class AuthService {
 
   login(code: string, nonce: string) {
     return this.httpService
-      .post<{ access_token: string; id_token: string }>(
+      .post<{ access_token: string; id_token: string, expires_in: number }>(
         `${this.idcsStripe}/oauth2/v1/token`,
         `grant_type=authorization_code&code=${code}`,
         {
@@ -48,6 +45,7 @@ export class AuthService {
           return {
             idToken: response.data.id_token,
             accessToken: response.data.access_token,
+            expiresAt: new Date(new Date().getTime() + response.data.expires_in * 1000)
           };
         }),
         catchError((err) => {
@@ -57,6 +55,21 @@ export class AuthService {
       );
   }
 
+  calculateFingerprint(accessToken: string) {
+    const fingerprint = randomBytes(16).toString('hex');
+    const jti = this.parseJwt(accessToken).jti;
+    const fingerprintHash = createHash('sha256').update(fingerprint+jti).digest('hex');
+    return {
+      fingerprint,
+      fingerprintHash
+    }
+  }
+
+  validateFingerprint(fingerprint: string, fingerprintHash: string, accessTokenPayload: any) {
+    const calculatedFingerprintHash = createHash('sha256').update(fingerprint+accessTokenPayload.jti).digest('hex');
+    return fingerprintHash === calculatedFingerprintHash;
+  }
+
   private parseJwt(token: string) {
     const base64Url = token.split('.')[1];
     const jsonPayload = Buffer.from(base64Url, 'base64url').toString();

+ 2 - 0
src/main.ts

@@ -1,3 +1,4 @@
+import * as cookieParser from 'cookie-parser';
 import { NestFactory } from '@nestjs/core';
 import { AppModule } from './app.module';
 
@@ -6,6 +7,7 @@ async function bootstrap() {
     logger: ['log', 'error', 'warn', 'debug', 'verbose'],
   });
   app.enableCors();
+  app.use(cookieParser());
   await app.listen(3000);
 }
 bootstrap();