Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

nestjs apiでjwt認証を実装する

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

前提

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

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

以下のコマンドで生成

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

必要なパッケージの導入

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使ってます。

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

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

mkdir db
touch db/test.db

そしてコネクション

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

全体眺めるとこんな感じ

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は以下のような形にしておきました

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はこんな感じ

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;
}

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

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();
    }
}
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);
    }
}
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側の実装です

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認証の時に重要な部分がありますのでまた後で説明します。

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

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戦略は、usernamepasswordになっています。

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

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

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

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 {}
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戦略の実装です

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に注目

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

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

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戦略が走るわけですが

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は仕込めます。

仕込む場合

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

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした