LaravelとNestJsの認証の共有

# Laravel-NestJsの認証共有
以下のリポジトリで例を見つけることができます: [GitHub - dabiddo/laravel-nestjs-auth](https://github.com/dabiddo/laravel-nestjs-auth)

## なぜこれを行うのか？
私たちは、大規模なLaravelのモノリスアプリをマイクロサービスに移行しています。私たちのプロジェクトマネージャーは、チームのほとんどがそれを使用した経験を持っているため、NestJsを選択しました。そして、大きな疑問の一つは、現在のユーザーを新しいアーキテクチャにどのように移行するかということでした。ユーザーテーブルはあまり変更されないため、私たちはそれをそのまま残し、NestJsがLaravel JWTをマイクロサービス認証に受け入れるようにしました。それによって、一週間の調査と開発が行われ、この記事では、バックエンドチームがそれを実現するために行ったことをまとめています。

## Laravel + Passport
この記事では、認証部分に焦点を当てるため、laravel-passportのチュートリアルを要約します。チュートリアルは[Laravel Passportの公式ドキュメント](https://laravel.com/docs/10.x/passport)を参照するか、この [Laravel 10 Passport API Authentication Tutorial](https://www.laravelia.com/post/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認証チュートリアル](https://docs.nestjs.com/security/authentication)を読み、[NestJs-Passportのチュートリアル](https://docs.nestjs.com/recipes/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を使用しました。必要に応じて変更してください。

```bash
npm install --save @nestjs/typeorm typeorm mysql2
```

#### その他のパッケージ

```bash
$ 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トークンを復号化し、復号化できたら結果のペイロードをリクエストに注入する役割を担います。
```js
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**

```js
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**
```js
@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では機能しません。