現在、NestJS(Node.jsのフレームワーク)でLocalStrategyを使用したログイン認証機能を作成しています。
ログインの際に、ユーザー名とパスワード以外のパラメータをRequestから取得したいのですが、
LocalStrategyを適用したGuard内でRequestの受け取り方が分からなかったので、調べて分かった解決策を備忘録として書くことにしました。
といっても2時間ほどGoogle先生とにらめっこして、Stack Overflowでようやく見つけた解決策になるので最適解かどうかはわかりません。。
間違っていることを書いていたり、他にもっと簡単な解決策があればコメントで教えていただけると幸いです。
ファイル構成
-
AuthController:ログイン認証で最初に呼び出されるコントローラー
このコントローラーに**@UseGuardsで適用しているものが後述するLocalStrategy**になります。LocalStrategyに記載した処理が、コントローラー内のメソッドを呼び出す直前に実行されることになります。(今回だとlogin()の呼び出し直前)
ちなみにここで定義されているlogin()の処理は、ログイン成功時にトークン等の情報を返却するだけなので特に触れません。
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local')) // 後述するLocalStrategyをGuardとして使用
@Post('auth/login')
async login(@Request() req) { // これが呼び出される直前にGuardの処理が実行される
return req.user;
}
}
-
LocalStrategy:Guardとして処理する内容を記載
ログイン時に入力されたユーザー名とパスワードを使用して検証を行う部分になります。
実際にはここで定義されているvalidate()の内部で、後述するAuthServiceの**validateUser()**が呼び出されていますが、検証に必要な値を渡すことができればいいので
ここでRequest Bodyから必要なパラメータを取得したいということです。
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 authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password); // AuthServiceで定義されている、ユーザー名とパスワードを実際に検証する処理
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
-
AuthService:ユーザー名とパスワードの検証処理を記載
ここで検証を行うのですが、その際にユーザ名とパスワード以外ののRequestパラメータも欲しいのです。
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
ただ、ここで呼び出されているサービスに**@Request() req: Request**といった形で値を渡そうとしてもbodyの中身はundefinedになってしまいます。
import { Injectable, Request } from '@nestjs/common'; // Request追記
import { UsersService } from '../users/users.service';
import { Request as Req } from 'express'; // 追記
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(username: string, pass: string, @Request() req: Request): Promise<any> { // 引数にRequest追記
console.log(req.body); // undefined
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
解決策
LocalStrategyのコンストラクタは、継承している**PassportStrategy(Strategy)の内容のままsuper()**として記述するのですが、
この引数に { passReqToCallback: true } を渡してあげます。
これによりRequestをコールバックに対して引き継ぐことができるようになります。
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 authService: AuthService) {
super({ passReqToCallback: true }); // 追記
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
PassportStrategyのvalidate()メソッドにはデフォルトでusernameとpasswordのパラメータが渡されることになっていますが、
先ほど追加した { passReqToCallback: true } は第一引数に渡されます。
なので引数の定義は以下のようになります。
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Request as Req } from 'express';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ passReqToCallback: true }); // 追記
}
async validate(req: Req, username: string, password: string): Promise<any> { // 第一引数にRequest
const reqParam = req.body; // bodyのパラメータを取得可能
console.log(reqParam); // 出力
const user = await this.authService.validateUser(reqParam, username, password); // サービスメソッドにパラメータを渡せる
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
NestJSでの認証の実装については公式ページに記載されているので気になった方はご一読ください。
終わりに
ログイン認証時にユーザ名とパスワード以外を必要とするタイミングはなかなかないと思いますが(私の経験上)、
もし同じ状況の方がいればぜひこの記事が目に留まればと思います。
最後まで読んでいただきありがとうございました。