NestJSのSwaggerでCognito・LINEのOAuth2を設定する
Hirayama 2024.01.11
最近業務でNestJSを使ったAPIサーバーを構築する機会があり、その際、ユーザー認証をAmazon CognitoとLINE Loginで行いました。
認証自体はNestJSのGuardsでそれぞれの認可サーバーを使用したトークンの検証を実装すればいいのですが、開発中はトークンを簡単に発行できる手段がなかったため、PostmanなどのAPIクライアントツールで毎回トークンを発行し、リクエストヘッダーに張り付けて検証しており、とても不便でした。
そこで、本記事では、Swaggerの定義ファイルにOAuth2の認証情報を書き込むことで、Authorizeボタンのクリックするだけでトークンを発行できるようにし、この不便さを解消したいと思います。
本記事で作成するNestJSアプリケーションは、以下のリポジトリからクローン可能です。
GitHub - FOURIER-Inc/nestjs-swagger
Contribute to FOURIER-Inc/nestjs-swagger development by creating an account on GitHub.
https://github.com/FOURIER-Inc/nestjs-swagger
前提
全て一から説明すると膨大な文章量になってしまうため、本記事では以下の前提で説明します。
- AWS CLIの設定が完了している
- Amazon Cognitoでユーザープールの作成が完了している
- LINE developers accountで、LINE Loginチャネルの作成が完了している
- NestJSをある程度使ったことがあり、基本的な設定方法が分かる
環境構築
まずは、NestJSの初回セットアップから、Swaggerドキュメントを表示できるところまで準備します。
npm i -g @nestjs/cli
nest new nestjs-swagger
次に、Swaggerを表示するのに必要な、@nestjs/swagger
をインストールします。
npm i -D @nestjs/swagger
インストール後、src/app.controller.ts
を以下のように書き換えます。Controllerのエンドポイントは、それぞれCognitoとLINEのGuardを設定し、認証してからでないとアクセスできないようにする予定です。
import { Controller, Get } from '@nestjs/common';
import { ApiOkResponse, ApiProperty } from '@nestjs/swagger';
class MessageContainer {
@ApiProperty()
message: string;
}
@Controller()
export class AppController {
@Get('/cognito')
@ApiOkResponse({ type: MessageContainer })
getCognitoHello(): MessageContainer {
return {
message: 'Authorized by Cognito!',
};
}
@Get('/line')
@ApiOkResponse({ type: MessageContainer })
getLineHello(): MessageContainer {
return {
message: 'Authorized by LINE!',
};
}
}
最後にsrc/main.ts
ファイルを以下のように編集し、Swaggerドキュメントが生成・表示されるようにします。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
buildOpenApiDocument(app);
await app.listen(3000);
}
bootstrap().then();
function buildOpenApiDocument(app: INestApplication): void {
const options = new DocumentBuilder().setTitle('Nestjs Swagger').build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('doc', app, document);
}
ここまでセットアップ出来たら、NestJSアプリケーションを立ち上げ、
npm run start:dev
ブラウザからhttp://localhost:3000/doc
を開き、Swagger Documentが表示されれば、初期セットアップは完了です。
現時点では、認証設定は何もしていないので、Authorizeボタンも表示されませんし、/cognito
と/line
も制限なくアクセスできます。
Amazon CognitoとLINEのセットアップ
環境構築だけでもなかなか大変ですが、このセクションも結構手間がかかります。
内容自体はこのブログの主題から逸れてしまうため、サッとスクリーンショットを中心にどのような設定をしたか説明し、この後の実装で必要になるパラメータを確認していきます。
Amazon Cognito
ユーザープールを作成したあと、OAuth2に関連する設定がされているか確認します。
この時、以下のパラメータをメモしておいてください。
- ユーザープールID
- Cognitoドメイン(URL)
- クライアントID
- クライアントシークレット
ユーザープールの概要
ユーザープールIDをメモします。
アプリケーションの統合タブ
ドメイン
Cognito ドメインをメモします。
リソースサーバー
リソースサーバーが1つ設定されていればOKです。
アプリケーションクライアントのリスト
1つアプリケーションが設定されていればOKです。
アプリケーションクライアント > アプリケーションクライアントに関する情報
クライアントIDとクライアントシークレットをメモします。
アプリケーションクライアント > ホストされたUI
スクリーンショットのように設定します。
この際、許可されているコールバックに、http://localhost:3000/doc/oauth2-redirect.html
を設定します。
http://hogehoge.localhost
といったようにホスト名を変えている場合、コールバックURLに登録することができません。
自分も同じ問題にハマりましたが、http://localhost?redirect=http://hogehoge.localhost/doc/oauth2-redirect.html
といった風に、クエリパラメータとしてリダイレクト先を設定し、Nginxの設定でredirect
パラメータが来た場合はリダイレクトするように設定すると、うまく動きます。LINE Login
LINE Loginチャネルのチャネル基本設定タブを開き、以下の項目を確認します。
- Channel ID
- Channel Secret
また、LINEログイン設定のコールバックURLにhttp://localhost:3000/doc/oauth2-redirect.html
を設定します。
Guardの追加
次にGuardを追加し、設定したエンドポイントで認証するように設定します。
Guardも簡単な紹介にとどめますが、リポジトリには、今回作成したNestJSアプリケーションがあるので、そちらも参考にしてください。
GitHub - FOURIER-Inc/nestjs-swagger
Contribute to FOURIER-Inc/nestjs-swagger development by creating an account on GitHub.
https://github.com/FOURIER-Inc/nestjs-swagger
Amazon Cognito
まずは、Amazon Cognitoのトークンを検証するため、aws-jwt-verify
パッケージを追加します。
npm i aws-jwt-verify
次に、以下のGuardのAWS_COGNITO_USER_POOL_ID
とAWS_COGNITO_USER_POOL_CLIENT_ID
を置き換えてください。
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
import { CognitoJwtVerifierSingleUserPool } from 'aws-jwt-verify/cognito-verifier';
import { CognitoAccessTokenPayload } from 'aws-jwt-verify/jwt-model';
@Injectable()
export class AmazonCognitoGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authorization = request.headers['authorization'];
if (!authorization) return false;
const result = await this.getToken(authorization);
return result !== undefined;
}
async getToken(
authorization: string,
): Promise<CognitoAccessTokenPayload | undefined> {
const token = authorization.replace('Bearer ', '');
try {
return await this.makeVerifier().verify(token);
} catch (e) {
return undefined;
}
}
makeVerifier(): CognitoJwtVerifierSingleUserPool<{
userPoolId: string;
tokenUse: 'access';
clientId: string | string[] | null;
}> {
return CognitoJwtVerifier.create({
userPoolId: 'AWS_COGNITO_USER_POOL_ID', // Replace with your user pool id
tokenUse: 'access',
clientId: 'AWS_COGNITO_USER_POOL_CLIENT_ID', // Replace with your user pool client id
});
}
}
LINE Login
LINEはAWSと違いパッケージ等がないので、自分でトークンをサーバーに送信し、検証する必要があります。
送信するためのHTTPクライアントとして、axios
パッケージをインストールします。axios
の使用は個人的な好みなので、fetchでも可能だと思います。
npm i axios
次に、以下のGuardのLINE_LOGIN_CHANNEL_ID
を置き換えます。
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import http from 'http';
import axios from 'axios';
@Injectable()
export class LineLoginGuard implements CanActivate {
async canActivate(_: ExecutionContext): Promise<boolean> {
const request = _.switchToHttp().getRequest() as http.IncomingMessage;
const authorization = request.headers['authorization'];
if (!authorization) return false;
const token = authorization.replace('Bearer ', '');
const data = await this.verify(token);
if (
data.expires_in < 1 ||
data.client_id !== 'LINE_LOGIN_CHANNEL_ID' // Replace with your LINE Login channel id
) {
return false;
}
return data.scope.includes('openid');
}
async verify(token: string): Promise<{
client_id: string;
expires_in: number;
scope: string;
}> {
const response = await axios.get<{
client_id: string;
expires_in: number;
scope: string;
}>('/verify', {
baseURL: 'https://api.line.me/oauth2/v2.1',
params: {
access_token: token,
},
});
return response.data;
}
}
これらのGuardをControllerに設定します。後述しますが、複数のOAuth2スキーマを持つ場合、@ApiOAuth2
に名前も指定する必要があります。
@UseGuards(AmazonCognitoGuard)
@ApiOAuth2(['openid'], 'Amazon Cognito')
getCognitoHello(): MessageContainer;
@UseGuards(LineLoginGuard)
@ApiOAuth2(['openid'], 'LINE Login')
getLineHello(): MessageContainer;
Swagger Documentに鍵マークが表示されれば完了です。
この時点でAPIをたたくと、Guardでの認証に失敗し、403レスポンスが返ってくると思います。
SwaggerのAuthorization設定
ようやく本題となるSwaggerの設定ですが、以下のように書けば、Amazon CognitoとLINE LoginのOAuth2スキーマを登録できます。
function buildOpenApiDocument(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle('Nestjs Swagger')
.addOAuth2(
{
type: 'oauth2',
description: 'Amazon Cognito user pool authentication',
flows: {
authorizationCode: {
authorizationUrl:
'AMAZON_COGNITO_USER_POOL_DOMAIN/oauth2/authorize',
tokenUrl: 'AMAZON_COGNITO_USER_POOL_DOMAIN/oauth2/token',
scopes: {
openid: 'openid token',
},
},
},
},
'Amazon Cognito',
)
.addOAuth2(
{
type: 'oauth2',
description: 'LINE Login authentication',
flows: {
authorizationCode: {
authorizationUrl:
'https://access.line.me/oauth2/v2.1/authorize/oauth2/authorize',
tokenUrl: 'https://api.line.me/oauth2/v2.1/token',
scopes: {
profile: 'user profile',
'profile openid': 'user profile and openid',
'profile openid email': 'user profile, openid and email',
openid: 'openid',
'openid email': 'openid token and email',
},
},
},
},
'LINE Login',
)
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('doc', app, document, {
swaggerOptions: {
oauth2RedirectUrl: 'http://localhost:3000/doc/oauth2-redirect.html',
},
});
}
このコードのポイントは以下の通りです。
authorizationUrl
とtokenUrl
にはOAuth2の認証エンドポイントを指定します。Amazon Cognitoの場合はドメイン+固定パス、LINE Loginの場合はドキュメントを参考に指定します。scopes
にopenid
を必ず含めます。addOAuth2
の第2引数に名前を指定します。この名前はControllerの@ApiOAuth2
で指定した名前と一致している必要があります。SwaggerModule.setup
のswaggerOptions.oauth2RedirectUrl
に、http://localhost:3000/doc/oauth2-redirect.html
を指定します。
この設定後、Swagger Documentを開き、Authorizeをクリックすると、2つのOAuth2スキーマが設定されているのが確認できます。
SwaggerModule.setup
関数のswaggerOptions
にてinitOAuth
を設定することで、client_id
とclient_secret
のデフォルト値を設定することができます。確認
全ての設定が完了したので、実際に認証をしてみます。
Amazon Cognito
/cognito
エンドポイントの右側の錠マークをクリックし、client_id
、client_secret
、scopes
を入れた後、Authorizeをクリックします。
正しく設定されていれば、以下のように認証画面が出てくるので、ログインかサインアップをします。
ログインに成功すると、Swagger Documentにリダイレクトされます。
これで、/cognito
エンドポイントを叩いた時、トークンの認証が行われ、200レスポンスが返るようになります。
LINE Login
LINEの認証の場合も同様に、client_id
、client_secret
、scopes
を入れた後、Authorizeをクリックします。
正しく設定されていれば、以下のようにLINEのログイン画面が出てくるので、ログインします。
ログインに成功すると、Swagger Documentにリダイレクトされます。
これで、/line
エンドポイントを叩いた時、トークンの認証が行われ、200レスポンスが返るようになります。
まとめ
この記事では、SwaggerのAuthorization機能で、Amazon CognitoとLINE LoginとのOAuth2認証をするための設定をし、発行したトークンでNestJSのGuardで認証できることを確認しました。
設定の主要ポイントは以下の通りです。
- OAuth2の認証には、
クライアントID
とクライアントシークレット
が必要 - NestJSのSwaggerには、
/oauth2-redirect.html
エンドポイントがあるので、認可サーバーのコールバックURLとSwaggerにリダイレクトURLに設定する - トークンの検証サーバーは、LINE LoginのようにURLで指定する方法や、Amazon Cognitoのようにサービスごとの固有パラメータで指定する方法がある
2つの認可サーバーを設定したので、サーバーごとの違いもある程度理解し、OAuth2についても理解が深まりました。
この記事の内容が、皆さんのAPI開発に役立てられれば幸いです。
Hirayama / Engineer
1997年生まれ、南伊豆出身。学生時代にC#で画像処理アプリケーションを作ったりしていました。業務では主にLaravelを使用してサーバーサイドのプログラミングをしています。趣味はドライブとシミュレーションゲーム。