【NestJS】Hasura・NestJSをAuth0で認証処理を導入する
概要
この記事ではNestJS
のAPIエンドポイントとHasura
に対する認証を担うプラットフォームとしてAuth0
を採用した際の導入方法についてまとめまています
Hasura
をAuth0
で保護する方法
基本的にはhasura
のドキュメント・チュートリアルが充実しているためその通りに進めれば設定は完了します
Docker
で環境構築をされている場合はAUth0
の公開鍵を発行して環境変数に設定する必要があるので注意が必要です
環境変数に公開鍵を設定する
公開鍵の発行
こちらで説明されている通り公開鍵を発行してください
環境変数の設定
version: '3.6'
services:
postgres:
image: postgres
restart: always
volumes:
- db_data:/var/lib/postgresql/data
graphql-engine:
image: hasura/graphql-engine:v1.0.0-beta.6
ports:
- "8080:8080"
depends_on:
- "postgres"
restart: always
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
## uncomment next line to set an admin secret
# HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
# HASURA_GRAPHQL_JWT_SECRET: '発行した公開鍵を定義します'
volumes:
db_data:
NestJS
のAPIエンドポイントの保護
基本的にNestJS
ではHasura
で吸収できない複雑な処理のみを担ってもらいます。
Hasuraを経由する場合は前述のHasura
の保護でセキュアに保つことができます。
そこでNestJS
側のエンドポイントを直接叩かれないように同様にAuth0
で保護していきたいと思います。
認証で利用する値を環境変数に設定
.env
に直接書くよりGCP
のCloud Run
のSecret
で環境変数を定義しておく方がよさそうですが、一旦こちらに定義しています
# DomainName
AUTH0_ISSUER_URL="https://xxxx.auth0.com/"
# Identifier
AUTH0_AUDIENCE="xxxxx"
Guard
機能を実装
リクエスト前の特定のリクエストを本当に通して良いのかを検証してくれるように実装していきます
実行時に存在する特定の条件 (パーミッション、ロール、ACL など) に応じて、与えられたリクエストがルートハンドラによって処理されるかどうかを決定します。これはしばしば認可 (authorization) と呼ばれます。
Guard
を作成する
# NestJS CLIで作成します
nest g gu auth/auth-guard
Guard
を定義する
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common'
import { GqlContextType } from '@nestjs/graphql'
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'
import { Reflector } from '@nestjs/core'
import { expressjwt, GetVerificationKey } from 'express-jwt'
import { expressJwtSecret } from 'jwks-rsa'
import { ConfigService } from '@nestjs/config'
import { promisify } from 'util'
@Injectable()
export class AuthGuard implements CanActivate {
private readonly AUTH0_AUDIENCE: string
private readonly AUTH0_ISSUER_URL: string
constructor(
@InjectPinoLogger(AuthGuard.name) private readonly logger: PinoLogger,
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) {
this.AUTH0_AUDIENCE = this.configService.get('AUTH0_AUDIENCE')
this.AUTH0_ISSUER_URL = this.configService.get('AUTH0_ISSUER_URL')
}
async canActivate(context: ExecutionContext): Promise<boolean> {
if (context.getType<GqlContextType>() === 'graphql') {
return true
}
// Auth0に対してJwtTokenの整合性を確認
const checkJwtToken = await promisify(
expressjwt({
secret: expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${process.env.AUTH0_ISSUER_URL}.well-known/jwks.json`,
}) as GetVerificationKey,
audience: this.AUTH0_AUDIENCE,
issuer: this.AUTH0_ISSUER_URL,
algorithms: ['RS256'],
}),
)
try {
await checkJwtToken(
context.switchToHttp().getRequest(),
context.switchToHttp().getResponse(),
)
return true
} catch (e: unknown) {
throw new UnauthorizedException(e)
}
}
}
API
への保護を有効にします
import { HttpAdapterHost, NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { Logger } from 'nestjs-pino'
import { AllExceptionsFilter } from './common/filter/all-exceptions.filter'
async function bootstrap() {
// nestjs-pino https://github.com/iamolegga/nestjs-pino
const app = await NestFactory.create(AppModule, { bufferLogs: true })
app.useLogger(app.get(Logger))
const adapterHost = app.get(HttpAdapterHost)
const httpAdapter = adapterHost.httpAdapter
const instance = httpAdapter.getInstance()
app.useGlobalFilters(new AllExceptionsFilter(instance))
app.enableCors({
origin: '*',
allowedHeaders:
'Origin, X-Requested-With, Content-Type, Accept, Authorization',
})
await app.listen(3000)
}
bootstrap()
検証
@UseGuards(AuthGuard)
で保護しているエンドポイントに対してid_token
付きでリクエストを投げていきます
import { Controller, Get, UseGuards } from '@nestjs/common'
import { AppService } from './app.service'
import { AuthGuard } from './common/guard/auth/auth-guard.guard'
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello()
}
@UseGuards(AuthGuard)
@Get('/private')
async private() {
return { message: '成功したお' }
}
}
Auth0
からトークン取得するスクリプト
こちらを参考にさせていただきました🙇♂️
以下のスクリプトを実行し成功するとid_token
が発行されるのでコピーしておく
#!/usr/bin/env bash
auth_url=https://xxx.auth0.com
client_id=xxxx
client_secret=xxxx
username="Auth0に登録されているアカウント"
password="アカウントのパスワード"
echo "success🎁 id_tokenを使用してください"
curl -s --request POST \
--url ${auth_url}/oauth/token \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=password \
--data username=xxxx \
--data password=xxxx \
--data client_id=xxxx \
--data client_secret=xxxx \
echo "\n"
リクエスト
正常にid_token
を渡すとレスポンスが返ってくることを確認できます
試しにid_token
の一部を消して再度リクエストを行うと認証に失敗します
curl -i -X GET 'http://localhost:3000/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
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 34
ETag: W/"22-hShrnoFIPYGoHz/Bm72qqTZK7ss"
Date: Fri, 19 Aug 2022 16:25:08 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"message":"成功したお"}
おわりに
今回は直近で開発した内容を忘れないように備忘録として残しました。
これから開発される方々のお役に立てば幸いです
Discussion