節約プログラマー雑記

GCP API GatewayのJWT認証

最近、自宅のアプリ開発でGoogle Cloudばかり使って開発しています。その中でAPI Gatewayにおいて、セキュリティを高めるべく、JWT認証を設定するのですが、これがクセモノでマニュアルを見ても結構苦戦しましたので、その設定方法を書いていきたいと思います。

1. API Gatewayの作成

ということで、まずはGCPにてAPI Gatewayを作成していきます。メニュータブの「API Gateway」を起動し、「ゲートウェイを作成する」から次のような形で設定をしていきます。

api-gateway001.png

このとき、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のマニュアルを基に、トークンの値を設定します。それぞれ設定値は以下のようになります。

・ヘッダー設定値
algRS256
typJWT
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の構成もいろいろ試してみたいと思います。