nestjs:jwt認証の実装わからんくなったのでメモ

# nestjs apiでjwt認証を実装する

公式読みながらやってもよくわからなかったのでめもをする

## 前提

nestjsのコマンド操作がわかることとjwtの基礎知識多少わかるぐらい。
認証のチュートリアルやってみたけどイマイチ理解できなかった向け。
[本家チュートリアルはこちら](https://docs.nestjs.com/techniques/authentication)

## まずはuserとauthで必要なものを揃える

以下のコマンドで生成

```bash
nest g mo user
nest g mo auth
nest g co user
nest g s user
nest g s auth
touch src/user/user.entity.ts
touch src/auth/local.strategy.ts
touch src/auth/jwt.strategy.ts
```

## 必要なパッケージの導入

```bash
yarn add -D @nestjs/typeorm @nestjs/passport @nestjs/jwt passport passport-local passport-jwt @types/passport @types/passport-local @types/passport-jwt typeorm sqlite bcrypt @types/bcrypt
```

## 実装する

今回は楽するためsqlite使ってます。

```typescript:app.module.ts
    TypeOrmModule.forRoot(
      {
        type: 'sqlite',
        database: 'db/test.db',
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: true,
      },
    ),
```

必要なファイル、フォルダの追加

```bash
mkdir db
touch db/test.db
```

そしてコネクション

```typescript:app.module.ts
export class AppModule {
  constructor(private readonly connection: Connection) {}
}
```

全体眺めるとこんな感じ

```typescript:app.module.ts
// app.module
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';

@Module({
  imports: [
    UserModule,
    AuthModule,
    TypeOrmModule.forRoot(
      {
        type: 'sqlite',
        database: 'db/test.db',
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: true,
      },
    ),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
  constructor(private readonly connection: Connection) {}
}
```

### まずはlocal戦略の実装

今回のuser.entityは以下のような形にしておきました

```typescript:user/user.entity.ts
// user/user.eneity.ts
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ length: 500 })
    name: string;

    @Column('text')
    password: string;

    @Column()
    age: number;
}
```

dtoはこんな感じ

```typescript:user/user.dto.ts
// user/user.dto.ts
export class LoginDTO {
    name: string;
    password: string;
}

// tslint:disable-next-line: max-classes-per-file
export class SignUpDTO extends LoginDTO {
    age: number;
}
```

検証用のメソッドも含めて諸々実装します。

```typescript:auth/auth.controller.ts
// auth controller
import { User } from './user.entity';
import { SignUpDTO } from './user.dto';
import { Controller, Body, Post, Get } from '@nestjs/common';
import { UserControllerInterface } from './user.interface';
import { UserService } from './user.service';

@Controller('user')
export class UserController implements UserControllerInterface {
    constructor(private readonly userService: UserService) {}

    @Post('signup')
    public signUp(@Body() req: SignUpDTO): Promise<User> {
        return this.userService.signUp(req.name, req.password, req.age);
    }

    @Get('findAll')
    public findAll(): Promise<User[]> {
        return this.userService.findAll();
    }
}
```

```typescript:user/user.service.ts
// user service
import { User } from './user.entity';
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { UserServiceInterface } from './user.interface';
import { InjectRepository } from '@nestjs/typeorm';
import bcrypt = require('bcrypt');

@Injectable()
export class UserService implements UserServiceInterface {
    constructor(
        @InjectRepository(User)
        private readonly userRepository: Repository<User>,
    ) { }

    async findOne(name: string): Promise<User | undefined> {
        return this.userRepository.findOne({ name });
    }

    findAll(): Promise<User[] | undefined> {
        return this.userRepository.find();
    }

    signUp(name: string, password: string, age: number): Promise<User> {
        const user = new User();
        user.name = name;
        user.password = bcrypt.hashSync(password, 15);
        user.age = age;
        return this.userRepository.save(user);
    }
}
```

```typescript:user/user.module.ts
// user module
import { User } from './user.entity';
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}
```

次にauth側の実装です

```typescript:auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UserService } from './../user/user.service';
import { User } from './../user/user.entity';
import bcrypt = require('bcrypt');
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
    constructor(
        private readonly userService: UserService,
        private readonly jwtService: JwtService,
    ) { }

    async validateUser(name: string, pass: string): Promise<any> {
        const user: User = await this.userService.findOne(name);
        if (user && bcrypt.compareSync(pass, user.password)) {
            const { password, ...result } = user;
            return result;
        }
        return null;
    }

    async login(user: any) {
        const payload = { name: user.name, id: user.id };
        return {
            access_token: this.jwtService.sign(payload),
        };
    }
}
```

こちらのloginはjwt認証の時に重要な部分がありますのでまた後で説明します。

そして今回の ***キモの部分*** がこちらになります。

```typescript:auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
        usernameField: 'name',
        passwordField: 'password',
    });
  }

  async validate(name: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(name, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
```

ドキュメントをぱっと見自分はわからなかったのですが（見逃していたかもしれない）デフォルトのpassport戦略は、**username**と**password**になっています。

よくあるサービスだとメールアドレスとパスワードで認証したくて、カラム名もわかりやすくしたい場合は ***オーバーライド*** してあげる必要があります。

ここがなかなか調べてもわからなかったのが辛かったです…

こうすることで、postのjsonのbodyにsuperでオーバーライドしたキーで渡すことができるようになります。

```typescript:auth/auth.module.ts
// auth module
import { UserModule } from './../user/user.module';
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { jwtConstants } from './constants';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    UserModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '30m' },
    }),
    PassportModule.register({ defaultStrategy: 'jwt' }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}
```

```typescript:app.controller.ts
// app controller
import { AuthService } from './auth/auth.service';
import { Controller, Request, Post, UseGuards, Get } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('api')
export class AppController {
  constructor(private readonly authService: AuthService) {}
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req: any) {
    return this.authService.login(req.user);
  }

  @UseGuards(AuthGuard('jwt'))
  @Get('me')
  getProfile(@Request() req: any) {
    return req.user;
  }
}
```

## jwt戦略の実装

二つ目に重要になってくる部分はjwt戦略の実装です

```typescript:auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { id: payload.id, name: payload.name };
  }
}
```

こちらのソースコードのpayloadに注目

```typescript:auth/jwt.strategy.ts
async validate(payload: any) {
return { id: payload.id, name: payload.name };
}
```

このpayloadの中に入ってくるのは、先ほどauth/auth.service.tsのメソッドの中にある

```typescript:auth/auth.service.ts
async login(user: any) {
    const payload = { name: user.name, id: user.id };
    return {
        access_token: this.jwtService.sign(payload),
    };
}
```
このconstのなかで定義してあげる必要があります。

チュートリアルのままだと、userIdとsubが渡ることになっていますので、ここを変えないとなぜか動かないという状態になってしまいます。

ログインした後ヘッダにトークン含めてリクエストするとき、jwt戦略が走るわけですが

```typescript:app.controller.ts
// app controller
@UseGuards(AuthGuard('jwt'))
@Get('me')
getProfile(@Request() req: any) {
    return req.user;
}
```
このjwt戦略が走った時にまず最初にauthサービスのloginメソッドが呼ばれます。
その後、トークンが正しければそのままアクセストークンをjwtService経由で参照されます。参照先は先ほど定義したjwt.strategy.tsの中にあるvalidateです。

auth.serviceのloginのsignのなかのpayload == jwt.strategy.tsのvalidateのpayload引数という認識でOKなので、一応DTOは仕込めます。

仕込む場合

```typescript:auth/auth.service.ts
async login(user: User) {
    const payload = { name: user.name, id: user.id };
    return {
        access_token: this.jwtService.sign(payload),
    };
}
```

```typescript:auth/jwt.strategy.ts
async validate(payload: any) {
    return { id: payload.id, name: payload.name };
}
```

と言った感じになるでしょうか。

ここを注意すれば後はチュートリアル通りで普通に組めると思います