Authorization header で認証する Nest.js サーバを構築する
背景
Nest.js にてパブリックアクセス可能だけど認証が必要な API サーバを実装したかったので、 Authorization header をつけるようにしました。備忘録としてまとめておきます。同じようなことをしようとしている方の助けになれば幸いです。
ソフトウェアのバージョン
$ npm ls | grep nest
├── @nestjs/cli@10.0.0
├── @nestjs/common@10.2.5
├── @nestjs/config@3.1.1
├── @nestjs/core@10.2.5
├── @nestjs/platform-express@10.2.5
├── @nestjs/schematics@10.0.2
├── @nestjs/testing@10.2.5
方法
1. Guard
Nest.js ではリクエストのログ記録、エラーハンドリングを行う手段としてミドルウェアが存在しますが、中でも認証に関しては特に Guard と呼ばれている機能を利用することが推奨されているようです。
今回は Authorization header を必要とする Guard を以下のように実装しました。
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { Observable } from 'rxjs'
@Injectable()
export default class ApiKeyGuard implements CanActivate {
constructor(private readonly configService: ConfigService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest()
let apiKey = request.headers.authorization || ''
// 先頭の "Bearer " を削除してトークン部分だけを取得
if (apiKey.startsWith('Bearer ')) {
apiKey = apiKey.slice(7)
} else {
// 先頭に "Bearer " を含まない時点で拒否する
return false
}
const expectedToken = this.configService.get<string>('API_KEY_TOKEN')
return apiKey === expectedToken
}
}
Guard は Service 同様 @Injectable
デコレータを付与しつつ、 canActivate
method を実装します。この canActivate
method が認証の際に必ず実行される仕組みになっており、 true
を返すと認証され、 false
を返すと拒否されます。
上記の例の場合 request
の headers
に含まれる文字列を取り出し、そちらが .env
に記載の API_KEY_TOKEN
と一致しているなら true
を、一致していなければ false
を返す method となっています。
こちらの api-key.guard.ts
を実装しましたら Service 同様 Module にアサインします。
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
@Module({
imports: [ConfigModule.forRoot()],
controllers: [],
})
export default class ApiKeyGuardModule {}
続けて @UseGuards
デコレータを使って、 Controller にこの Guard のルールを適用します。
class にデコレータをアサインすると Controller すべてにルールを適用できますし method にデコレータをアサインするとその method のみにルールを適用できます。
柔軟にルールを適用する範囲を変更できるのがいいですね。
import { Get, UseGuards } from '@nestjs/common'
import { Prisma, Item } from '@prisma/client'
import ApiKeyGuard from '../guard/api-key.guard' // 先ほど実装した Guard を import します
@UseGuards(ApiKeyGuard) // UseGuards デコレータでルールを適用します
@Controller('item')
export default class ItemController {
constructor(private readonly itemService: ItemService) {}
@Get()
async findAll(): Promise<Item[]> {
return this.itemService.items({})
}
// skipping...
}
以上で実装は完了です。
テスト
では実際に Authorization header のあるなしでどのようにサーバのレスポンスが変化するのか確認しましょう。
まず .env
に以下のように簡単な API_KEY_TOKEN
を設定しサーバを起動します。
(実際の運用ではより複雑な API_KEY_TOKEN
にしてくださいね)
API_KEY_TOKEN=dev-api-key
> npm run start:dev
> backend@0.0.1 start:dev
> nest start --watch
[7:45:12 AM] Starting compilation in watch mode...
[7:45:19 AM] Found 0 errors. Watching for file changes.
Authorization header なし
> curl -X GET http://localhost:8000/v1/item
{"message":"Forbidden resource","error":"Forbidden","statusCode":403}
拒否され 403 エラーが返ってきました。
Authorization header あり
> curl -X GET http://localhost:8000/v1/item -H "Authorization: Bearer dev-api-key"
[{"id":"1","name":"XXXXXX",...}
認証され期待されるレスポンスが得られました!
💡 まとめ
- Authorization header を使用した Guard の実装ができました
- パブリックアクセスが可能ながら認証が必要な API サーバを構築できました
Discussion