はじめに
Google Cloudの API Gateway は、OpenAPI 2.0仕様に準拠したAPIを、サーバーレスなバックエンドと共にスケーラブルかつ安全に公開するための便利なサービスです。公式ドキュメントではAPIキーやサービスアカウントを利用する認証方法が解説されていますが、多くのWebhookなどで今なお使われている「Basic認証」については、サポートを明言した記述を見つけることができません。
しかし、API Gatewayは 「OpenAPI 2.0に準拠している」 のが最大のヒントです。OpenAPI 2.0の仕様にはBasic認証の定義が含まれており、「ドキュメントになくても、仕様に準拠しているなら動くのでは?」という仮説が立てられます。
このブログは、その仮説を検証するための実験レポートです。API GatewayとCloud Run Functionsを組み合わせ、具体的な設定ファイルやコード、そして重要な「ハマりどころ」の解説を通じて、このプロセスを容易に理解し、実装できるようになることを目指します。
【重要:免責事項】 これから紹介する方法は、Google Cloudの公式ドキュメントで明確にサポートが謳われていない機能の検証です。将来的に仕様が変更されたり、サポート対象外となったりする可能性があります。利用する際は、そのリスクを十分に理解した上で、自己責任でご判断ください。
準備
Basic認証を処理するバックエンド(Cloud Run Functions)の作成
API Gatewayはあくまで「門番」の役割です。実際の認証情報(ユーザー名とパスワード)が正しいかどうかをチェックするのは、バックエンドに配置するサービスになります。今回はその役割をCloud Run Functionsに担ってもらいます。本ブログでは、Windowsのローカル環境からgcloudコマンドでデプロイすることを想定しております。コンソール画面やCloud Shellからも同様の操作で実施できるとは考えられますが、保証できません。
まず、以下のPythonのコードをmain.pyという名前のファイルで作成します。API Gatewayから渡された認証情報を検証するPythonコードとなります。
import functions_framework
import os
import base64
from flask import jsonify
# 環境変数から正しいユーザー名とパスワードを取得
EXPECTED_USER = 'user'
EXPECTED_PASSWORD = 'password'
def check_auth(auth_header):
"""Basic認証ヘッダーを検証する"""
if not auth_header:
return False, 'Authorization header is missing'
if not auth_header.startswith('Basic '):
return False, 'Invalid authorization scheme'
encoded_credentials = auth_header.split(' ', 1)[1]
try:
decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
user, password = decoded_credentials.split(':', 1)
if user == EXPECTED_USER and password == EXPECTED_PASSWORD:
return True, 'Authentication successful'
else:
return False, 'Invalid username or password'
except Exception:
return False, 'Invalid base64 encoding or format'
@functions_framework.http
def api_backend(request):
"""API Gatewayからのリクエストを処理するHTTP Cloud Run Function"""
# API Gateway経由の場合、元のAuthorizationヘッダーはX-Forwarded-Authorizationに入る
auth_header = request.headers.get('X-Forwarded-Authorization') or request.headers.get('Authorization')
is_authorized, message = check_auth(auth_header)
if not is_authorized:
headers = {'WWW-Authenticate': 'Basic realm="Secure Area"'}
return (jsonify({'error': message}), 401, headers)
# 認証成功
return jsonify({
'status': 'success',
'message': 'Welcome! You are authenticated.'
})
次に関数の実行に必要なライブラリを記述した以下の内容をrequirements.txtという名前のファイルで作成します。
functions-framework==3.*
上記の2つのファイルを作成が完了した後に、作成したコードをGoogle CloudのCloud Run Functionsとしてデプロイします。
Cloud Shellまたはローカルのターミナルで、先ほどファイルを作成したディレクトリに移動し、以下のコマンドを実行してください。Cloud Run Functionsのデプロイが完了すると、コンソールにurl:としてトリガーURLが表示されます。このURLは後で使うので必ずコピーしておいてください。
gcloud functions deploy basic-auth-backend `
--runtime=python312 `
--region=asia-northeast1 `
--source=. `
--entry-point=api_backend `
--trigger-http `
--no-allow-unauthenticated
API Gatewayの構成とデプロイ
API Gatewayの動作は、OpenAPI 2.0仕様書(YAMLまたはJSON形式)によって定義します。以下の内容でopenapi.yamlというファイルを作成してください。
swagger: '2.0'
info:
title: Basic Auth Verification API
version: 1.0.0
schemes:
- https
produces:
- application/json
securityDefinitions:
basicAuth:
type: basic
description: "HTTP Basic Authentication"
paths:
/resource:
get:
summary: Get a protected resource
operationId: getResource
security:
- basicAuth: []
responses:
'200':
description: Successful operation
'401':
description: Unauthorized
x-google-backend:
address: YOUR_CLOUD_FUNCTION_TRIGGER_URL # ★ここに先ほどコピーしたURLを貼り付ける
上記のYAMLの中で、1箇所だけご自身の環境に合わせて修正が必要です。YOUR_CLOUD_FUNCTION_TRIGGER_URLの部分を、先ほどCloud Run Functionsのデプロイ時にコピーしたトリガーURLに書き換えてください。
OpenAPI 2.0の仕様書の準備ができたので、API Gatewayをデプロイします。
API Configを作成するため、以下のコマンドをターミナルで実行してください。
gcloud api-gateway api-configs create basic-auth-config `
--api=basic-auth-api --openapi-spec=openapi.yaml
次にAPI Gatewayを作成するため、以下のコマンドをターミナルで実行してください。
gcloud api-gateway gateways create basic-auth-gateway `
--api=basic-auth-api --api-config=basic-auth-config `
--location=asia-northeast1
デプロイには数分かかる可能性があります。完了すると、Google CloudのAPI GatewayにゲートウェイのURLがが表示されます。このURLをコピーします
動作確認
すべてのリソースがデプロイできたので、実際に動作するか確認してみます。API GatewayのURLにアクセスしてみます。以下のURLに実際にアクセスしてBasic認証のダイアログが表示されることを確認します。[YOUR_GATEWAY_HOSTNAME]はAPI GatewayのゲートウェイのURLに置き換えてください。
[YOUR_GATEWAY_URL]/resource
無事にBasic認証のダイアログが表示されることが確認できました!あとは想定される動作になるかを実際にユーザー名やパスワードを入力して確認してみます。
認証情報なしでアクセス
まずはユーザー名やパスワードを入力せず、ログインを押してみてください。そうするとリダイレクトされてもう一度、Basic認証のダイアログが表示される動作になります。
次に、ユーザー名やパスワードを入力せず、キャンセルボタンを押してみてください。
Basic認証を入力しなかったため、エラーとなりました。今回作成したCloud Run Functionsの想定通りの動作となります。
間違った認証情報でアクセス
次に間違ったユーザー名やパスワードを入力して、ログインを実施してみます。ユーザー名には「wronguser」、パスワードには「badpass」を入力してログインを押してみます。そうするとリダイレクトされてもう一度Basic認証のダイアログが表示される動作になります。
正しい認証情報でアクセス
次に正しいユーザー名やパスワードを入力して、ログインを実施してみます。ユーザー名には「user」、パスワードには「password」を入力してログインを押してみます。
正しい認証情報を入力したため無事に成功となりました。こちらも今回作成したCloud Run Functionsの想定通りの動作となります。
今回、Basic認証の動作を確認するためにブラウザからアクセスしていますが、curlコマンドでも類似する結果が得られることは確認しています。
X-Forwarded-Authorizationヘッダー
今回の実装には一つ、非常に重要な「ハマりどころ」がありました。それは、Cloud Run Functionsのコード内にあるこの一行です。
# API Gateway経由の場合、元のAuthorizationヘッダーはX-Forwarded-Authorizationに入る
auth_header = request.headers.get('X-Forwarded-Authorization') or request.headers.get('Authorization')
クライアントが送信したAuthorization: Basic ...というヘッダーは、API Gatewayを通過する際に、API Gateway自身がバックエンドを認証するためのAuthorization: Bearer ...(JWTトークン)ヘッダーに上書きされてしまいます。
では、クライアントが送った元のBasic認証ヘッダーはどこへ行ったのか?
答えは、X-Forwarded-Authorization という別のヘッダーにコピーされてバックエンドに渡されます。
もしこの挙動を知らずに、バックエンド側でAuthorizationヘッダーだけを読み取ろうとすると、中身はBearer ...となっているため認証は必ず失敗します。これが、この実装で最も嵌まりやすいポイントでした。
終わりに
今回の記事では、公式ドキュメントに明記されていないAPI GatewayのBasic認証について、OpenAPI 2.0の仕様をヒントに実装・検証する方法を紹介しました。結果として、API Gatewayは仕様通りBasic認証を(非公式ながら)サポートしていることが確認できました。
この方法を使えば、Webhookやシンプルな内部APIなど、手軽に認証をかけたい場合に有効な選択肢となるのではないかと思います。
最後に繰り返しますが、この方法はGoogle Cloudのサポート対象外である可能性が高いです。そのリスクを理解した上で、この情報があなたの課題解決の役に立つことを願っています。是非、この方法を活用して、より柔軟なAPI開発を実現してください。
注:本ブログで作成したリソースは、ブログ公開時点ですべて削除しております。あしからず。
- カテゴリ:
- Google Cloud