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使ってます。
TypeOrmModule.forRoot(
{
type: 'sqlite',
database: 'db/test.db',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
},
),
必要なファイル、フォルダの追加
mkdir db
touch db/test.db
そしてコネクション
export class AppModule {
constructor(private readonly connection: Connection) {}
}
全体眺めるとこんな感じ
// 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.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
export class LoginDTO {
name: string;
password: string;
}
// tslint:disable-next-line: max-classes-per-file
export class SignUpDTO extends LoginDTO {
age: number;
}
検証用のメソッドも含めて諸々実装します。
// 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 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 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側の実装です
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認証の時に重要な部分がありますのでまた後で説明します。
そして今回の キモの部分 がこちらになります。
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でオーバーライドしたキーで渡すことができるようになります。
// 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
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戦略の実装です
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に注目
async validate(payload: any) {
return { id: payload.id, name: payload.name };
}
このpayloadの中に入ってくるのは、先ほど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
@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は仕込めます。
仕込む場合
async login(user: User) {
const payload = { name: user.name, id: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
async validate(payload: any) {
return { id: payload.id, name: payload.name };
}
と言った感じになるでしょうか。
ここを注意すれば後はチュートリアル通りで普通に組めると思います