【NestJS x Auth0】NestJSで作成したAPIにAuth0の認証/認可を追加する 〜認証編〜
前回からの続きです。
今回は、以下の記事を参考に前回作成したサンプルAPIに認証機能を追加します。
Full-Stack TypeScript Apps: Developing a Secure API with NestJS
前提
- Auth0のアカウントがあること
1. Auth0にAPIを設定する
- Auth0 ダッシュボードにアクセスし、「Applications」>「APIs」>「+ Create API」ボタンを押します。
- 入力欄を埋めて「Create」ボタンを押します。
- issuerとaudienceの値を控えておきます。
- issuer:
https://<AUTH0-TENANT-NAME>.auth0.com/
- <AUTH0-TENANT-NAME>はAuth0 ダッシュボードの左上で確認できます。
- audience:
https://item-api.demo.com
- 上で設定した
Identifier
と同じです。
- 上で設定した
- issuer:
2. 認証メカニズム(Strategy)を定義する
NestJSに戻って認証メカニズム(Strategy)を定義します。
Strategy???
このあたりは完全に理解しているわけではないのでなんとなくの説明になります。
話は逸れますが、NestJSで認証機能を実現するためにはNestJSのGuard
機能を使います。
Guard
はハンドラー(Controllerで定義した各API)が実行される前に実行され、処理を実行して良いかを判断することができます。
Guards | NestJS - A progressive Node.js framework
NestJSでは認証のためのGuard(AuthGuard
)が用意されているため、認証メカニズム(Strategy)さえ定義すれば以下のように簡単(?)に認証機能を実装することができます。
@UseGuards(AuthGuard({Strategy名})) @Get('profile') getProfile(@Request() req) { return req.user; }
今回は参考記事のとおり、アクセストークン(JWT)による認証を実装するため、その認証メカニズムを定義する必要があります。
これから作成するStrategyなどをアプリに適用させるためにまずはmoduleを作成します。
npx nest g module authz
次にStrategyに必要なパッケージをインストールします。
npm i passport @nestjs/passport passport-jwt jwks-rsa dotenv
いよいよStrategyを作成しますが、ここはコピペです。
import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { passportJwtSecret } from 'jwks-rsa'; import * as dotenv from 'dotenv'; dotenv.config(); @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ secretOrKeyProvider: passportJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: `${process.env.AUTH0_ISSUER_URL}.well-known/jwks.json`, }), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), audience: process.env.AUTH0_AUDIENCE, issuer: `${process.env.AUTH0_ISSUER_URL}`, algorithms: ['RS256'], }); } validate(payload: unknown): unknown { return payload; } }
ここで、前のステップで控えておいた issuer、audienceの値が必要になるので、.env
ファイルを作成して環境変数を設定します。
AUTH0_ISSUER_URL=https://<AUTH0-TENANT-NAME>.auth0.com/ AUTH0_AUDIENCE=https://item-api.demo.com
最後に、上記のStrategyとPassportModuleをAuthzModule
に登録します。
import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { JwtStrategy } from './jwt.strategy'; @Module({ imports: [PassportModule.register({ defaultStrategy: 'jwt' })], providers: [JwtStrategy], exports: [PassportModule], }) export class AuthzModule {}
3. AuthGuardを適用する
上記で難しい部分は終わったので、後は各APIにAuthGuard
を適用していきます。
今回はCreate、Update、Delete操作に対して認証を必須にしたいので、以下のように各ハンドラの上に@UseGuards(AuthGuard('jwt'))
を記述します。
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, } from '@nestjs/common'; import { ItemsService } from './items.service'; import { CreateItemDto } from './dto/create-item.dto'; import { UpdateItemDto } from './dto/update-item.dto'; import { AuthGuard } from '@nestjs/passport'; @Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Post() @UseGuards(AuthGuard('jwt')) create(@Body() createItemDto: CreateItemDto) { return this.itemsService.create(createItemDto); } @Patch(':id') @UseGuards(AuthGuard('jwt')) update(@Param('id') id: string, @Body() updateItemDto: UpdateItemDto) { return this.itemsService.update(id, updateItemDto); } @Delete(':id') @UseGuards(AuthGuard('jwt')) remove(@Param('id') id: string) { return this.itemsService.remove(id); } ~~ }
動作確認
AuthGuard
を適用したAPIを実行しようとすると、Unauthorized
エラーになることが確認できました。
curl --location --request POST 'http://localhost:3000/items' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Item D", "price": 4000 }' { "statusCode": 401, "message": "Unauthorized" }
念の為、GET /items
はUnauthorized
にならないことも確認しておきます。
4. AuthGuardを適用したAPIを実行したい
アクセストークンを渡せばAuthGuardを適用したAPIがきちんと実行できるのかを確認します。
クライアントアプリの準備
まず、アクセストークンを発行するためにクライアントアプリケーションを作成する必要があります。 (クライアントアプリを0から作る必要はなく、Auth0のサンプルアプリケーションを使用します。)
- Auth0 ダッシュボードにアクセスし、「Applications」>「Applications」>「+ Create Application」ボタンを押します。
- 入力欄を埋めて「Create」ボタンを押します。
- Name:
Sample Application
- Application type:
Single Page Web Applications
- Name:
- 作成したApplicationのページで「Quick Start」タブを選択しお好みのフレームワークを選択します。(今回はReactを選択しました。)
- サンプルアプリケーションの説明ページが表示されるので「DOWNLOAD SAMPLE」ボタンからサンプルアプリケーションをダウンロードし、説明に沿ってサンプルアプリを設定します。
npm install
を実行した後、npm run start
でアプリを立ち上げます。
※ ポート番号が3000でかぶっているので、NestJSかサンプルアプリのポートを変える必要があります。本記事ではNestJSを4000、サンプルアプリを3000で起動しました。- サンプルアプリ画面右上の「Login」ボタンを押すと、Auth0のログイン画面が表示されるのでログイン(またはサインアップ)します。
- Auth0 Applicationの設定が間違っているとエラー画面に飛ばされます。
- ログインできていれば、クライアントアプリの準備は完了です。
- 「External API」タブが表示される。
- 「Login」ボタンがアカウントアイコンに変わっている。など。
トークンの取得
Auth0のサンプルアプリケーションにはAPIを実行する機能があり、多少いじればクライアントアプリからAPIを実行することが可能ですが今回はめんどうなのでトークンだけ取得します。
まず、Auth0のサンプルアプリケーションに含まれているsrc/auth_config.json
に上で設定したissuer、audienceを設定します。
次に、src/views/ExternalApi.js
のcallApi
関数を修正して、console.log
などでトークンを取得します。
callApi
関数はサンプルアプリでログイン後、「External API」タブを選択し、「Ping API」ボタンを押すと実行されます。
const callApi = async () => { try { const token = await getAccessTokenSilently(); console.log(token);
ちなみに、トークンの中身がどうなっているかは以下のサイトで確認できます。
トークンを付与してAPIを実行する
ようやくトークンが手に入ったので、APIを実行してみます。
PostmanなどでヘッダーにAuthorization: Bearer {上記で入手したトークン}
を付与して実行してみると、ちゃんと実行できることが確認できました。
curl --location --request POST 'http://localhost:4000/items' \ --header 'Authorization: Bearer ${TOKEN}' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Item E", "price": 4000 }' { "id": "4dc38fdf-54ec-4750-b848-0518d0b1f42d", "name": "Item E", "price": 4000 }
クライアントアプリが必要なことをすっかり忘れていて思ったより長くなってしまいましたが
これで認証機能の実装は終わりです。