Laravel-NestJsの認証共有
以下のリポジトリで例を見つけることができます: GitHub - dabiddo/laravel-nestjs-auth
なぜこれを行うのか?
私たちは、大規模なLaravelのモノリスアプリをマイクロサービスに移行しています。私たちのプロジェクトマネージャーは、チームのほとんどがそれを使用した経験を持っているため、NestJsを選択しました。そして、大きな疑問の一つは、現在のユーザーを新しいアーキテクチャにどのように移行するかということでした。ユーザーテーブルはあまり変更されないため、私たちはそれをそのまま残し、NestJsがLaravel JWTをマイクロサービス認証に受け入れるようにしました。それによって、一週間の調査と開発が行われ、この記事では、バックエンドチームがそれを実現するために行ったことをまとめています。
Laravel + Passport
この記事では、認証部分に焦点を当てるため、laravel-passportのチュートリアルを要約します。チュートリアルはLaravel Passportの公式ドキュメントを参照するか、この Laravel 10 Passport API Authentication Tutorial を参照してください。私はここでLaravelのコードをコピーアンドペーストしました。
基本的な要約
基本的なLaravelプロジェクトを作成し、JWTトークンを生成するためにlaravel-passportをインストールします。
composer create-project laravel/laravel example-app
Passportのインストール
composer require laravel/passport
マイグレーションの実行
php artisan migrate
Passportのセットアップ
php artisan passport:install
これにより、storage/フォルダ内に2つのキーが生成されます。
oauth-private.key
oauth-public.key
NestJs + Passport
次のパートでは、NestJsプロジェクトを作成し、jwtの生成と認証のためにパスポートライブラリをインストールする必要があります。
基本的なNestJsプロジェクト
$ npm i -g @nestjs/cli
$ nest new project-name
Passportのインストール
続ける前に、公式のNestJs認証チュートリアルを読み、NestJs-Passportのチュートリアルに従って必要なパッケージをインストールしてください。
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
もしチュートリアルに従っている場合、NestJsのauth
フォルダ内にjwt.strategy.ts
というファイルがあるはずです。このファイルを変更してRS256の暗号化を受け入れるようにします。
データベース
LaravelとNestJsはユーザーデータを抽出するために同じDBを共有するため、NestJsのチュートリアルで使用されているTypeOrmを使用しました。必要に応じて変更してください。
npm install --save @nestjs/typeorm typeorm mysql2
その他のパッケージ
$ npm i --save @nestjs/config
$ npm i jwks-rsa
$ npm i bcrypt
$ npm i -D @types/bcrypt
Laravel JWT アルゴリズム
LaravelからJWTトークンを生成し、それをjwt.ioにコピー&ペーストすると、検出されるアルゴリズムは RS256
です。このアルゴリズムは、JWTを暗号化および復号化するために秘密鍵と公開鍵を使用してペイロード情報を取得します。
この情報を知っているので、NestJs Passportも同じアルゴリズムを受け入れ、鍵を共有して同じペイロードを暗号化/復号化する必要があります。
NestJs 修正済み JWT
NestJsがLaravelのJWTトークンを受け入れるために変更する必要のある2つの重要なファイルがあります。jwt-auth.guard.ts
とjwt.strategy.ts
です。
JWT ガード
このファイルは、公開鍵を使用してJWTトークンを復号化し、復号化できたら結果のペイロードをリクエストに注入する役割を担います。
import { AuthGuard } from '@nestjs/passport';
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
import * as fs from 'fs';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
) {
super({
jwtOptions: {
algorithms: ['RS256'],
},
});
}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
try {
const token = request.headers.authorization.split(' ')[1];
const publicKey = fs.readFileSync('/app/laravel-auth/storage/oauth-public.key');
const payload = this.jwtService.verify(token, {
secret: publicKey,
algorithms: ['RS256']
});
request.user = payload;
return true;
} catch (error) {
console.log(error);
throw new UnauthorizedException('Invalid token');
}
}
}
JWT Strategy
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import * as jwksRsa from 'jwks-rsa';
import jwt, { Secret, GetPublicKeyOrSecret } from 'jsonwebtoken';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
algorithms: ['RS256'],
secretOrKeyProvider: (
request: any,
rawJwtToken: string,
done: (err: any, secretOrPublicKey?: string | object) => void,
) => {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
return done('Invalid token');
}
const { header } = decodedToken;
const { alg, kid } = header;
const jwksClient = jwksRsa({
jwksUri: 'http://localhost/',
});
jwksClient.getSigningKey(kid, (err, key) => {
if (err) {
return done(err);
}
const signingKey = key.getPublicKey();
done(null, signingKey);
});
},
});
}
}
これらのファイルを変更したら、Laravelからトークンを生成し、NestJsの保護されたルートにアクセスできるはずです。
NestJsからトークンを生成することは可能でしょうか?
NestJsからLaravelが受け入れるトークンを生成することは可能ですが、私はこれをまだ実現できていません。現在の進捗状況は次のとおりです。
AuthController
@Post('login')
async login(@Body() req)
{
const response = await this.usersService.findOneByEmail(req.email);
const hash = response.password.replace(/^\$2y(.+)$/i, '$2a$1');
const match = await bcrypt.compare(req.password, hash);
if(match === true) {
const payload = { username: response.name, email: response.email, sub: response.id };
const privateKey = fs.readFileSync('/app/laravel-auth/storage/oauth-private.key');
return {
access_token: jwt.sign(payload, privateKey, { algorithm: 'RS256', expiresIn: '1h' })
};
}
else {
throw new UnauthorizedException();
}
}
パスワードのハッシュ化方法がLaravelとNestJsで少し異なるため、比較時に「Incorrect password」が表示されるため、置換を行う必要があります。
現時点では、Jwtトークンが生成され、NestJsのルートでは機能しますが、Laravelでは機能しません。