💡

Authorization header で認証する Nest.js サーバを構築する

2024/06/17に公開

背景

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 を以下のように実装しました。

src/guard/api-key.guard.ts
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 を返すと拒否されます。

上記の例の場合 requestheaders に含まれる文字列を取り出し、そちらが .env に記載の API_KEY_TOKEN と一致しているなら true を、一致していなければ false を返す method となっています。

こちらの api-key.guard.ts を実装しましたら Service 同様 Module にアサインします。

src/guard/api-key.guard.module.ts
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 のみにルールを適用できます。
柔軟にルールを適用する範囲を変更できるのがいいですね。

src/item/item.controller.ts
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 サーバを構築できました
Cykinso's Tech Blog

Discussion