LoginSignup
0
0

LaravelとNestJsの認証の共有

Last updated at Posted at 2023-05-30

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.tsjwt.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では機能しません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0