Przeglądaj źródła

Implement basic auth strategy using JWT

Michael Bromley 7 lat temu
rodzic
commit
c55b082c1a

+ 19 - 0
modules/core/api/auth/auth.controller.ts

@@ -0,0 +1,19 @@
+import { Body, Controller, Post } from "@nestjs/common";
+import { LoginDto } from "./login.dto";
+import { AuthService } from "../../auth/auth.service";
+
+@Controller('auth')
+export class AuthController {
+
+    constructor(private authService: AuthService) {}
+
+    @Post('login')
+    async login(@Body() loginDto: LoginDto) {
+        const token = await this.authService.createToken(loginDto.username, loginDto.password);
+        if (token) {
+            return {
+                token
+            };
+        }
+    }
+}

+ 4 - 0
modules/core/api/auth/login.dto.ts

@@ -0,0 +1,4 @@
+export interface LoginDto {
+    username: string;
+    password: string;
+}

+ 3 - 0
modules/core/api/customer/customer.resolver.ts

@@ -1,8 +1,10 @@
 import { Query, ResolveProperty, Resolver } from '@nestjs/graphql';
+import { AuthGuard } from '@nestjs/passport';
 import { CustomerService } from './customer.service';
 import { Address } from '../../entity/address/address.interface';
 import { CustomerEntity } from "../../entity/customer/customer.entity";
 import { Customer } from "../../entity/customer/customer.interface";
+import { UseGuards } from "@nestjs/common";
 
 @Resolver('Customer')
 export class CustomerResolver {
@@ -13,6 +15,7 @@ export class CustomerResolver {
         return this.customerService.findAll();
     }
 
+    @UseGuards(AuthGuard('jwt'))
     @Query('customer')
     customer(obj, args): Promise<Customer> {
         return this.customerService.findOne(args.id);

+ 10 - 1
modules/core/app.module.ts

@@ -10,6 +10,9 @@ import { ProductService } from './api/product/product.service';
 import { ProductResolver } from './api/product/product.resolver';
 import { LocaleService } from './locale/locale.service';
 import { PasswordService } from "./auth/password.service";
+import { AuthService } from "./auth/auth.service";
+import { AuthController } from "./api/auth/auth.controller";
+import { JwtStrategy } from "./auth/jwt.strategy";
 
 @Module({
     imports: [
@@ -26,8 +29,14 @@ import { PasswordService } from "./auth/password.service";
             database: 'test',
         }),
     ],
-    controllers: [CustomerController],
+    controllers: [
+        AuthController,
+        CustomerController
+    ],
     providers: [
+        AuthService,
+        JwtStrategy,
+        PasswordService,
         CustomerService,
         CustomerResolver,
         ProductService,

+ 6 - 0
modules/core/auth/auth-types.ts

@@ -0,0 +1,6 @@
+import { Role } from "./role";
+
+export interface JwtPayload {
+    identifier: string;
+    roles: Role[];
+}

+ 46 - 0
modules/core/auth/auth.service.ts

@@ -0,0 +1,46 @@
+import * as jwt from 'jsonwebtoken';
+import { Injectable, UnauthorizedException } from "@nestjs/common";
+import { JwtPayload } from "./auth-types";
+import { Role } from "./role";
+import { PasswordService } from "./password.service";
+import { Connection } from "typeorm";
+import { InjectConnection } from "@nestjs/typeorm";
+import { UserEntity } from "../entity/user/user.entity";
+
+export const JWT_SECRET = 'some_secret';
+
+@Injectable()
+export class AuthService {
+
+    constructor(private passwordService: PasswordService,
+                @InjectConnection() private connection: Connection) {}
+
+    async createToken(identifier: string, password: string): Promise<string> {
+        const user = await this.connection.getRepository(UserEntity)
+            .findOne({
+                where: {
+                    identifier
+                }
+            });
+
+        if (!user) {
+            throw new UnauthorizedException();
+        }
+
+        const passwordMatches = await this.passwordService.check(password, user.passwordHash);
+
+        if (!passwordMatches) {
+            throw new UnauthorizedException();
+        }
+        const payload: JwtPayload = { identifier , roles: user.roles };
+        return jwt.sign(payload, JWT_SECRET, { expiresIn: 3600 });
+    }
+
+    async validateUser(payload: JwtPayload): Promise<any> {
+        return await this.connection.getRepository(UserEntity).findOne({
+            where: {
+                identifier: payload.identifier
+            }
+        });
+    }
+}

+ 23 - 0
modules/core/auth/jwt.strategy.ts

@@ -0,0 +1,23 @@
+import { ExtractJwt, Strategy } from 'passport-jwt';
+import { AuthService, JWT_SECRET } from './auth.service';
+import { PassportStrategy } from '@nestjs/passport';
+import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { JwtPayload } from "./auth-types";
+
+@Injectable()
+export class JwtStrategy extends PassportStrategy(Strategy) {
+  constructor(private readonly authService: AuthService) {
+    super({
+      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+      secretOrKey: JWT_SECRET,
+    });
+  }
+
+  async validate(payload: JwtPayload, done: Function) {
+    const user = await this.authService.validateUser(payload);
+    if (!user) {
+      return done(new UnauthorizedException(), false);
+    }
+    done(null, user);
+  }
+}

+ 0 - 0
modules/core/auth/roles.ts → modules/core/auth/role.ts


+ 1 - 1
modules/core/entity/user/user.entity.ts

@@ -1,7 +1,7 @@
 import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
 import { AddressEntity } from '../address/address.entity';
 import { User } from './user.interface';
-import { Role } from "../../auth/roles";
+import { Role } from "../../auth/role";
 
 @Entity('user')
 export class UserEntity implements User {

+ 1 - 1
modules/core/entity/user/user.interface.ts

@@ -1,5 +1,5 @@
 import { Address } from '../address/address.interface';
-import { Role } from "../../auth/roles";
+import { Role } from "../../auth/role";
 
 /**
  * A registered user of the system, either a Customer or Administrator. The User interface / entity is responsible

+ 1 - 1
modules/testing/mock-data.service.ts

@@ -5,7 +5,7 @@ import { ProductVariantEntity } from "../core/entity/product-variant/product-var
 import { ProductEntity } from "../core/entity/product/product.entity";
 import { ProductVariantTranslationEntity } from "../core/entity/product-variant/product-variant-translation.entity";
 import { AddressEntity } from "../core/entity/address/address.entity";
-import { Role } from "../core/auth/roles";
+import { Role } from "../core/auth/role";
 import { ProductTranslationEntity } from "../core/entity/product/product-translation.entity";
 import { PasswordService } from "../core/auth/password.service";
 import { UserEntity } from "../core/entity/user/user.entity";

+ 5 - 0
package.json

@@ -24,6 +24,7 @@
     "@nestjs/core": "^5.0.0",
     "@nestjs/graphql": "^3.0.0",
     "@nestjs/microservices": "^5.0.0",
+    "@nestjs/passport": "^1.0.10",
     "@nestjs/testing": "^5.0.0",
     "@nestjs/typeorm": "^5.0.0",
     "apollo-server-express": "^1.3.6",
@@ -31,7 +32,10 @@
     "body-parser": "^1.18.3",
     "graphql": "^0.13.2",
     "graphql-tools": "^3.0.2",
+    "jsonwebtoken": "^8.2.2",
     "mysql": "^2.15.0",
+    "passport": "^0.4.0",
+    "passport-jwt": "^4.0.0",
     "reflect-metadata": "^0.1.12",
     "rxjs": "^6.2.0",
     "typeorm": "^0.2.6",
@@ -42,6 +46,7 @@
     "@types/express": "^4.0.39",
     "@types/faker": "^4.1.2",
     "@types/jest": "^21.1.8",
+    "@types/jsonwebtoken": "^7.2.7",
     "@types/node": "^9.3.0",
     "@types/supertest": "^2.0.4",
     "faker": "^4.1.0",

+ 104 - 0
yarn.lock

@@ -45,6 +45,10 @@
     json-socket "^0.2.1"
     optional "0.1.4"
 
+"@nestjs/passport@^1.0.10":
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-1.0.10.tgz#fd606a79fb444b59ecb813a5bed5938e55e9f0fa"
+
 "@nestjs/testing@^5.0.0":
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-5.0.1.tgz#a485a44f7613d8243f208d7fb3d7fce740ab5ead"
@@ -103,6 +107,12 @@
   version "21.1.8"
   resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.8.tgz#d497213725684f1e5a37900b17a47c9c018f1a97"
 
+"@types/jsonwebtoken@^7.2.7":
+  version "7.2.7"
+  resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.7.tgz#5dd62e0c0a0c6f211c3c1d13d322360894625b47"
+  dependencies:
+    "@types/node" "*"
+
 "@types/mime@*":
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
@@ -1452,6 +1462,10 @@ bser@^2.0.0:
   dependencies:
     node-int64 "^0.4.0"
 
+buffer-equal-constant-time@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+
 buffer-from@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
@@ -2264,6 +2278,12 @@ ecc-jsbn@~0.1.1:
   dependencies:
     jsbn "~0.1.0"
 
+ecdsa-sig-formatter@1.0.10:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3"
+  dependencies:
+    safe-buffer "^5.0.1"
+
 editions@^1.3.3:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
@@ -4141,6 +4161,21 @@ jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
 
+jsonwebtoken@^8.2.0, jsonwebtoken@^8.2.2:
+  version "8.2.2"
+  resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.2.2.tgz#76d7993fda79660d71bd0f933109e1f133734b20"
+  dependencies:
+    jws "^3.1.5"
+    lodash.includes "^4.3.0"
+    lodash.isboolean "^3.0.3"
+    lodash.isinteger "^4.0.4"
+    lodash.isnumber "^3.0.3"
+    lodash.isplainobject "^4.0.6"
+    lodash.isstring "^4.0.1"
+    lodash.once "^4.0.0"
+    ms "^2.1.1"
+    xtend "^4.0.1"
+
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -4150,6 +4185,21 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
+jwa@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6"
+  dependencies:
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.10"
+    safe-buffer "^5.0.1"
+
+jws@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f"
+  dependencies:
+    jwa "^1.1.5"
+    safe-buffer "^5.0.1"
+
 keyv@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
@@ -4301,6 +4351,34 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
+lodash.includes@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+
+lodash.isboolean@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+
+lodash.isinteger@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+
+lodash.isnumber@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+
+lodash.isstring@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+
+lodash.once@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+
 lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.3.0:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
@@ -4652,6 +4730,10 @@ ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
 
+ms@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+
 multer@1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/multer/-/multer-1.3.0.tgz#092b2670f6846fa4914965efc8cf94c20fec6cd2"
@@ -5189,6 +5271,24 @@ pascalcase@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
 
+passport-jwt@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065"
+  dependencies:
+    jsonwebtoken "^8.2.0"
+    passport-strategy "^1.0.0"
+
+passport-strategy@1.x.x, passport-strategy@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
+
+passport@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811"
+  dependencies:
+    passport-strategy "1.x.x"
+    pause "0.0.1"
+
 path-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
@@ -5253,6 +5353,10 @@ pause-stream@0.0.11:
   dependencies:
     through "~2.3"
 
+pause@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
+
 pbkdf2@^3.0.3:
   version "3.0.16"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"