GCP API GatewayのJWT認証
- estoy
- 2022年7月14日
最近、自宅のアプリ開発でGoogle Cloudばかり使って開発しています。その中でAPI Gatewayにおいて、セキュリティを高めるべく、JWT認証を設定するのですが、これがクセモノでマニュアルを見ても結構苦戦しましたので、その設定方法を書いていきたいと思います。
1. API Gatewayの作成
ということで、まずはGCPにてAPI Gatewayを作成していきます。メニュータブの「API Gateway」を起動し、「ゲートウェイを作成する」から次のような形で設定をしていきます。
このとき、api.ymlの設定でゲートウェイの構成が決まります。私が設定した内容は以下のようになります。
#api.yml
swagger: '2.0'
info:
title: sample-api
description: Sample API on API Gateway with a Google Cloud Functions backend
version: 1.0.0
schemes:
- https
produces:
- application/json
securityDefinitions:
#JWT認証設定
custom_auth_id:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
x-google-issuer: "YOUR ACCOUNT EMAIL" #"サービスアカウントのclient_email"
x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/XXX" #"サービスアカウントのclient_x509_cert_url"
x-google-audiences: "test-account" #自由"
paths:
#認証が必要なAPI
/auth:
get:
summary: Greet a user
operationId: hello_get
security:
- custom_auth_id: []
x-google-backend:
address: https://us-west1-XXXXXX.cloudfunctions.net/entrypoint #"実行したいAPIのURL"
responses:
'200':
description: A successful response
schema:
type: string
post:
summary: Greet a user
operationId: hello_post
security:
- custom_auth_id: []
x-google-backend:
address: https://us-west1-XXXXXX.cloudfunctions.net/entrypoint #"実行したいAPIのURL"
responses:
'200':
description: A successful response
schema:
type: string
#認証が不要なAPI
/not_auth:
get:
summary: Greet a user
operationId: hello_get_not
x-google-backend:
address: https://us-west1-XXXXXX.cloudfunctions.net/entrypoint #"実行したいAPIのURL"
responses:
'200':
description: A successful response
schema:
type: string
post:
summary: Greet a user
operationId: hello_post_not
x-google-backend:
address: https://us-west1-XXXXXX.cloudfunctions.net/entrypoint #"実行したいAPIのURL"
responses:
'200':
description: A successful response
schema:
type: string
ここでやっていることは、認証の定義を14~20行目に記載し、認証を行いたいAPIに対して27~28、39~40行目のようにsecurityのスキーマを設定をしています。これによって、/authでは認証が必要、/not_authでは認証不要で実行できるようになってます。
認証の定義は、Goolgle のマニュアルを元に「x-google-issuer」と「x-google-jwks_uri」にGCPのサービスアカウントのメールアドレスとサービスアカウントの公開鍵へのURLを設定しています。こうすることでクライアント側が指定した鍵でのトークンを受け付けられるようにしています。(サービスアカウントの作成方法、公開鍵のURLについては、関連記事にあるCloud FunctionのJWT認証に記載していますので、良かったら見てみてください。)
「x-google-audiences」については、クライアント側のaudienceの項目に設定する項目になるのですが、自由に設定してOKなので、今回は'test-account'と設定しました。
認証の定義ができたら、今回は27~28、39~40行目のようにAPIのパスとメソッドにsecurityの項目を作成することで、認証を付与することができます。
2. クライアント側の処理(JWT作成)
API Gateway側の作成ができたら、次はクライアント側の処理になります。カスタム認証の場合、クライアント側はJWTのトークンの生成が必要になりますが、今回もGoogleのマニュアルを基に、トークンの値を設定します。それぞれ設定値は以下のようになります。
alg | RS256 |
---|---|
typ | JWT |
kid | 鍵のkid |
iat | トークンの発行日時 |
---|---|
exp | トークンの有効期限を表す日時 |
iss | サービスアカウントのメールアドレス |
aud | 「x-google-audiences」に設定した値 |
sub | サービスアカウントのメールアドレス |
生成したトークンはHTTPヘッダーに「Authorization」を追加して、「Bearer JWTトークン」という形で設定値を付与することで、API Gateway側で認証がされるようになります。Pythonでソースを書いてみると次のような感じです。
#JWT作成
#!/usr/bin/env python
import time
import json
import jwt
import requests
# 秘密鍵
key = "サービスアカウントの鍵ファイルの「private_key」の値"
#JWTの設定値
head = {
"alg": "RS256",
"typ": "JWT",
"kid":"サービスアカウントの鍵ファイルの「private_key_id」の値"
}
sa_email="サービスアカウントのメールアドレス"
audience = "「x-google-audiences」に設定した値"
#JWTトークン生成処理
def make_jwt_token(expiry_length=3600):
now = int(time.time())
global head
global sa_email
global audience
#ペイロード部分
payload = {
'iat': now,
"exp": now + expiry_length,
'iss': sa_email,
'aud': audience,
'sub': sa_email,
}
token = jwt.encode(payload,key,algorithm="RS256",headers=head)
return token
#HTTP送信処理
def jwt_request(signed_jwt, url='API GatewayのURL'):
headers = {
'Authorization': 'Bearer {}'.format(signed_jwt),
#'content-type': 'application/json' #POST等 body部が設定されてる場合に必要
}
response = requests.get(url, headers=headers)
print(response.status_code, response.content)
response.raise_for_status()
#メイン部分
if __name__ == '__main__':
expiry_length = 3600
token = make_jwt_token(expiry_length)
jwt_request(token)
上記を実行して、認証に成功すればステータスコード=200で「Hello World!」と返却され、認証に失敗すればステータスコード401などのエラーが返却されるようになります。
3. 最後に
API GatewayのJWT認証による設定とクライアント側の通信方法について、書いてみました。
ゲートウェイの構成ファイルだけで認証機能ができるようになり、API Gatewayを設けることでGCPのCloud Functionの認証も簡単にできるようになるのが、かなり便利に感じました。また、認証の方法は、今回のJWT以外にもFirebaseやAuth0もあるのですが、そちらと比べるとJWT認証はローカルでトークンを生成して通信できて、通信回数が減らせるのはメリットなのかなと思います。(秘密鍵を抱えるセキュリティ上のデメリットはあるかもしれませんが。。)
いずれは、他の認証方法を試すとともに、API Gatewayの構成もいろいろ試してみたいと思います。