この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
この記事は、NestJSで作成したバックエンドAPIをAuth0で認証/認可する方法をまとめています。
SPAのフロントエンドがまだ作成されていない状態で、バックエンドAPIを先行して作成する場合を想定しています。
注意点
本記事内で使用するJWTやAuth0は、使い方によっては脆弱性を生む原因となります。
この記事はあくまで参考程度にとどめて頂き、実際のプロジェクトで採用する場合は適切なセキュリティ対策を行ってください。
動作環境
今回使用した動作環境は以下のとおりです。
- PC : Mac M1(Apple Silicon)チップ
- OS : macOS Big Sir
- Node.js : v16.13.2
- NestJS : Version8系
サンプルプロジェクト
このサンプルプロジェクトは簡単なREST APIです。誰でもアクセス可能な公開APIと認証/認可が必要なプライベートAPIの2つのAPIがあります。
ここでは一部コードを抜粋して説明していきます。全ファイルは私のGithubリポジトリをご参照ください。
プロジェクト構成
一部抜粋
auth0-backend-sample/ ルートディレクトリ
┣ src
┃ ┣ authz Auth0での認証/認可のためのモジュール群
┃ ┃ ┣ authz.module.ts
┃ ┃ ┗ jwt.strategy.ts
┃ ┣ main.ts
┃ ┣ sample.controller.ts
┃ ┗ sample.module.ts
┣ tool/
┃ ┣ get_auth0_token.sh Auth0テナントに登録済のユーザーのトークンを取得するスクリプト
┃ ┗ regist_auth0_user.sh Auth0テナントにユーザーを新規登録するスクリプト
┗ .env 環境変数定義ファイル
Auth0での認証/認可
まずAuth0での認証/認可部分を実装していきます。
こちらの公式ブログを参考にさせて頂きました。
必要なライブラリのインストール
以下のライブラリが必要です。新規でプロジェクトを作成する際はnpm i
またはyarn add
で追加してください。
- passport Node.jsの認証ライブラリ
- @nestjs/passport NestJS用の
passport
ライブラリ - passport-jwt JWTの検証のためのライブラリ
- jwks-rsa Auth0テナントのJWKS(JSON Web Key Set)エンドポイントからRSA署名鍵を取得するためのライブラリ
- dotenv
.env
ファイルに定義された環境変数を取得するためのライブラリ
src/authz/jwt.strategy.ts
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;
}
}
抽象クラスPassportStrategy
を拡張することで、認証戦略が設定されます。
jwksUri
のJWKSエンドポイントからRSA署名鍵を取得しJWTの検証を行います。
cache: true
とすることで、JWKSエンドポイントへの過剰なリクエストを防ぐために、取得したRSA署名鍵のキャッシュが有効になります。
署名キーのkid
ごとにキャッシュされ、次に同様のkid
が要求されたときにキャッシュが利用されます。
デフォルトのキャッシュ時間は10分で、chachMaxAge
を設定することで変更可能です。
また、攻撃者が多数のランダムなkid
のJWTを送信する攻撃を防ぐために、1分間でJWKSエンドポイントに対してリクエストを送る回数を制限しています。
jwksRequestsPerMinute
の値で変更可能で、上記設定だと1分間に5回までのリクエスト送信に制限されています。
audience
とissuer
はJWTの検証に使用するので、環境変数から値を取得します。
validate
メソッドのpayload
にはJWTのペイロードが格納されています。
validate
メソッドを拡張し、JWTの検証以外の検証ロジックを実装することも可能です。
src/authz/authz.module.ts
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 {}
passortのNestJSラッパーのPassportModule
をインポートし、JWTの認証戦略として上記で作成したjwt.strategy.ts
のロジックを指定しています。
この設定を行うことで@UseGuards()
のデコレータを使用した、APIの保護が可能になります。
APIの保護の有効化
src/main.ts
import { NestFactory } from '@nestjs/core';
import { SampleModule } from './sample.module';
async function bootstrap() {
const app = await NestFactory.create(SampleModule);
app.enableCors({
origin: '*',
allowedHeaders: 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
});
await app.listen(8080);
}
bootstrap();
CORSの設定を有効化しています。
SPAから呼び出す際はAuthorization
ヘッダーを許可する必要があります。
src/sample.module.ts
import { Module } from '@nestjs/common';
import { AuthzModule } from './authz/authz.module';
import {SampleController} from "./sample.controller";
@Module({
imports: [AuthzModule],
controllers: [SampleController],
})
export class SampleModule {}
コントローラで認証/認可モジュールを利用できるようインポートしています。
src/sample.controller.ts
import {Controller, Get, UseGuards, Request} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class SampleController {
@Get('/public')
async public(@Request() req) {
return {message: 'public api'}
}
@UseGuards(AuthGuard('jwt'))
@Get('/private')
async private(
@Request() req,
) {
return {message: 'private api', sub: req.user.sub}
}
}
/public
のAPIは認証なしに呼び出し可能です。
/private
APIは@UseGuards(AuthGuard('jwt'))
を指定することで保護されています。
Authorization
ヘッダーにBearer トークン
を指定することで呼び出すことが可能になります。
JWTトークンのペイロードには、@Requestデコレータ
で指定した引数のuser
フィールドからアクセス可能です。
上記ではreq.user.sub
とすることで、Auth0で発行したユーザーのsub
を返却しています。
Auth0側の設定
Auth0 APIでのユーザー登録、トークンの取得が可能になるようにいくつかの設定を行います。
Auth0のアカウント作成やアプリケーションの登録についてはこちらの記事をご参照ください。
- SPA用に作成したアプリケーション設定を修正します。
-
Setting
タブを開いてください。またDomain
、Client ID
、Client Secret
の値は後ほど使用するのでメモをお願いします。 -
Setting
タブそのまま下までスクロールして、Advanced Setting
を開いてください。 -
Grant Types
タブに切り替えてください。 -
Password
にチェックをいれて、Save Changes
ボタンを押してください。 -
続いて、サイドバーの
Settings
からTenant Settingsに移動しGeneral
タブを開いてください。 -
そのままスクロールし、API Authorization Settingsの
Default Directory
にUsername-Password-Authentication
と入力し、Save
ボタンを押してください。Username-Password-Authentication
はAuth0にデフォルトで用意されているユーザーの認証DBになります。
Auth0側の設定は以上になります。
動作確認
- 環境変数の設定。
.env
ファイルにメモしておいたDomain
とClient ID
を設定してください。一部抜粋
AUTH0_ISSUER_URL=https://<Domain>/ AUTH0_AUDIENCE=<Client ID>
-
バックエンドAPIの起動。下記コマンドを実行してください。
% yarn install or npm install % yarn start or npm run start
下記コマンドで公開APIを呼び出すと正常なレスポンスが返却されます。
% curl -i -X GET 'http://localhost:8080/public'
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 24
Connection: keep-alive
Keep-Alive: timeout=5
{"message":"public api"}
下記コマンドでプライベートAPIを呼び出すとステータスコード401が返却されます。
% curl -i -X GET 'http://localhost:8080/private'
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 43
Connection: keep-alive
Keep-Alive: timeout=5
{"statusCode":401,"message":"Unauthorized"}
3. Auth0に新規ユーザー登録を行います。
tool/regist_auth0_user.sh
のファイルにモしておいたDomain
とClient ID
を設定し、登録したいユーザーのemail
、username
、password
を指定してください。
ここではサンプルとしてemail=alice_test@example.com
、username=alice_test
、password=Alicetest@1
としています。
tool/regist_auth0_user.sh
#!/usr/bin/env bash
auth_url=https://<Domain>
client_id=<Client ID>
email=alice_test@example.com
username=alice_test
password=Alicetest@1
echo "Register user"
curl -X POST "${auth_url}/dbconnections/signup" \
-H 'Content-Type: application/json' \
-d "{\"client_id\": \"${client_id}\",\"email\":\"${email}\",\"password\": \"${password}\",\"connection\": \"Username-Password-Authentication\",\"username\":\"${username}\",\"name\":\"${username}\",\"nickname\": \"${username}\"}"
echo "\n"
以下コマンドを実行するとAuth0にユーザーが登録されます。
% cd ./tool
% ./regist_auth0_user.sh
Register user
{"name":"alice_test","nickname":"alice_test","_id":"xxxxxxxxxxxxxxxxxxxxxx","email_verified":false,"email":"alice_test@example.com"}\n
Auth0のダッシュボードからサイドバーのUser Managementを開きUsers
をクリックしてください。先程登録したユーザー情報が確認できます。
4. 登録したユーザーのトークンを取得します。
tool/regist_auth0_user.sh
のファイルにモしておいたDomain
とClient ID
とClient Secret
を設定し、先程登録したユーザーのusername
(メールアドレス)、password
を指定してください。
ここではサンプルとしてusername=alice_test@example.com
、password=Alicetest@1
としています。
tool/get_auth0_token.sh
#!/usr/bin/env bash
auth_url=https://<Domain>
client_id=<Client ID>
client_secret=<Client Secret>
username=alice_test@example.com
password=Alicetest@1
echo "Get Token"
curl -s --request POST \
--url ${auth_url}/oauth/token \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=password \
--data username=${username} \
--data password=${password} \
--data client_id=${client_id} \
--data client_secret=${client_secret} \ | jq -r
echo "\n"
以下コマンドを実行するとトークンが取得できます。
% cd ./tool
% ./get_auth0_token.sh
Get Token
{
"access_token": "<ACCESS_TOKEN>",
"id_token": "<ID_TOKEN>",
"scope": "openid profile email address phone",
"expires_in": 86400,
"token_type": "Bearer"
}
\n
5. 取得したIDトークン
をAuthorization
ヘッダーにBearerトークンとし設定し以下コマンドを実行すると、プライベートAPIは正常なレスポンスを返却します。
% curl -i -X GET 'http://localhost:8080/private' -H 'Authorization: Bearer <ID_TOKEN>'
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 64
Connection: keep-alive
Keep-Alive: timeout=5
{"message":"private api","sub":"auth0|xxxxxxxxxxxxxxxxxxxxx"}
最後に
NestJSは認証ライブラリが充実していたので比較的簡単に実装することができました。
繰り返しになりますが、本記事はあくまで参考程度にとどめて頂き、実際のプロジェクトでは適切なセキュリティ対策を行うようにしてください。