先日こちらの記事で、StrapiをGAEに設置してIAPを設けることで無料で手軽に認証がかかった環境にデプロイできるようにしました。
ただこれによって、APIを叩くときも認証を超える必要が生じるようになりました。
今回はそれを解決したいと思います。
Strapi APIからコンテンツを取得する
まず、IAPがない環境でAPIからデータを取得します。
仮として、ArticlesというContent-Typeを作成したという前提で話を進めます。
API Keyを発行する
Setting
> API Tokens
> Create API Token
をクリック
APIKeyの名前と寿命、説明文とTokenTypeを選択したら、今回は閲覧権限さえあれば十分なのでそのままSave
します。
画面遷移後、APIKeyが表示されるので、忘れずにメモをする。
手が滑って画面遷移してしまったり忘れてしまった場合は、生成した鍵の編集マークをクリックして、遷移後の画面でRegenerate
をクリックするとAPIKeyを再生成してAPIKeyを確認できる。
あくまで再生成して別のTokenを発行しているだけなので、使用中のAPIKeyの場合は注意が必要。
APIからContentsを取得する
たたく前に、適当なデータをContent Managerから登録しておきましょう。公開状態にはしなくてもよいです。
APIは、VScodeのRest Client拡張を使用して叩いてみます。
拡張機能追加後、Ctrl+Nで新しタブを開き、Ctrl+K, Mで言語設定を開き、http
に変更します。
その後、以下を張り付けます。
GET http://localhost:1337/api/articles?publicationState=preview HTTP/1.1
Authorization: Bearer <token>
張り付けるとエディタ上にSend Requestと表示されるのでそちらをクリック。
問題なければ、以下のような内容が表示されます
HTTP/1.1 200 OK
/// 中略
{
"data": [
{
"id": 2,
"attributes": {
"title": "hoge",
"main": "description",
"createdAt": "2022-12-18T09:24:53.623Z",
"updatedAt": "2022-12-18T02:00:07.912Z",
"publishedAt": null
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
これで取得は完了です。
もし記事を公開状態にしている場合は、GETのクエリ文字列を削除してください。
IAPを突破する
まず、試しにURLをIAPの向こう側のものに変更してみましょう。
GET https://strapi.hogehoge.dev/api/articles?publicationState=preview HTTP/1.1
Authorization: Bearer <token>
返信はこうです。
HTTP/1.1 401 Unauthorized
X-Goog-IAP-Generated-Response: true
Date: Sun, 18 Dec 2022 11:06:42 GMT
Content-Type: text/html
Server: Google Frontend
Content-Length: 44
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Connection: close
Invalid IAP credentials: Unable to parse JWT
401エラー、想定通り認証が突破できていないことがわかります。
これを突破するには、認証情報をヘッダーに持たせる必要があります。
まず、IAP Tokenを発行してみましょう。
gcloud auth print-identity-token
でうまいことできないか試行錯誤していたのですが、うまくいかなかったので公式ドキュメントに掲載されているサービス アカウント キーファイルを利用した方法でTokenを取得します。
キーファイルを作成する
gcloud iam service-accounts keys create <生成するファイルのファイルパス> --iam-account=<サービスアカウント名>
サービスアカウントは、新規に作成するのがベストだとは思いますが、面倒であればGAEのデフォルトサービスアカウントでも問題はないです。
次に、そのサービスアカウントにIAPのIAP-secured Web App User
権限を付与します。
その後、適当な名前のシェルスクリプトファイルを作成し、以下をコピペします。
なお、内容は公式ドキュメントの最後の方を少し書き換えただけです。
#!/usr/bin/env bash
set -euo pipefail
get_token() {
# Get the bearer token in exchange for the service account credentials.
local service_account_key_file_path="${1}"
local iap_client_id="${2}"
local iam_scope="https://www.googleapis.com/auth/iam"
local oauth_token_uri="https://www.googleapis.com/oauth2/v4/token"
local private_key_id="$(cat "${service_account_key_file_path}" | jq -r '.private_key_id')"
local client_email="$(cat "${service_account_key_file_path}" | jq -r '.client_email')"
local private_key="$(cat "${service_account_key_file_path}" | jq -r '.private_key')"
local issued_at="$(date +%s)"
local expires_at="$((issued_at + 3600))"
local header="{'alg':'RS256','typ':'JWT','kid':'${private_key_id}'}"
local header_base64="$(echo "${header}" | base64)"
local payload="{'iss':'${client_email}','aud':'${oauth_token_uri}','exp':${expires_at},'iat':${issued_at},'sub':'${client_email}','target_audience':'${iap_client_id}'}"
local payload_base64="$(echo "${payload}" | base64)"
local signature_base64="$(printf %s "${header_base64}.${payload_base64}" | openssl dgst -binary -sha256 -sign <(printf '%s\n' "${private_key}") | base64)"
local assertion="${header_base64}.${payload_base64}.${signature_base64}"
local token_payload="$(curl -s \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
--data-urlencode "assertion=${assertion}" \
https://www.googleapis.com/oauth2/v4/token)"
local bearer_id_token="$(echo "${token_payload}" | jq -r '.id_token')"
echo "${bearer_id_token}"
}
main(){
# TODO: Replace the following variables:
SERVICE_ACCOUNT_KEY="service_account_key_file_path"
IAP_CLIENT_ID="iap_client_id"
URL="application_url"
# Obtain the Bearer ID token.
ID_TOKEN=$(get_token "${SERVICE_ACCOUNT_KEY}" "${IAP_CLIENT_ID}")
echo ${ID_TOKEN}
}
main "$@"
文中にある以下の項目は置き換えが必要なので置き換えましょう。
IAP_CLIENT_ID
は、GCPのIdentity-Aware Proxy
にあるAppEngineアプリ
からOAuth構成に移動
を選択し、右上に表示されるクライアントIDの値です。
SERVICE_ACCOUNT_KEY="service_account_key_file_path"
IAP_CLIENT_ID="iap_client_id"
URL="application_url"
実行するとIAP Tokenが得られるので、それを先のRest ClientのAuthorization
ヘッダーに張り付けてみましょう。
GET http://strapi.hogehoge.dev/api/articles?publicationState=preview HTTP/1.1
Authorization: Bearer <IAP Token>
すると、エラーの内容がこう変わります。
HTTP/1.1 403 Forbidden
// 中略
{
"data": null,
"error": {
"status": 403,
"name": "ForbiddenError",
"message": "Forbidden",
"details": {}
}
}
エラーが変わりました。
以前はIAPからのエラーですが、今度はStrapiからのエラーとなっています。
IAPの認証を突破できたわけですね。
……まあ、代わりにStrapiの認証が突破できなくなりましたが。
IAPの認証ヘッダとStrapiの認証ヘッダを一つのリクエストに登録する。
こうなってしまったのは、IAPで最初に案内されるヘッダーとStrapiで使用されるヘッダーが被ってしまったためです。
幸いなことに、IAPはAuthorizationヘッダーだけではなくProxy-Authorization
ヘッダーでも有効なので、そちらを記載します。
GET http://strapi.hogehoge.dev/api/articles?publicationState=preview HTTP/1.1
Proxy-Authorization: Bearer <IAP-token>
Authorization: Bearer <Strapi-token>
その結果がこちらです。
HTTP/1.1 200 OK
//中略
{
"data": [
{
"id": 2,
"attributes": {
"title": "hoge",
"main": "description",
"createdAt": "2022-12-18T18:24:53.623Z",
"updatedAt": "2022-12-18T11:00:07.912Z",
"publishedAt": null
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
ローカルで実行するのと同じ結果が返ってきていますね。
これにてIAP越しにAPIを叩くのは完了です。
今回はvscodeの拡張を使用して叩きましたが、もしnode.js等などから叩くのであれば、公式でライブラリを案内されているので、そちらに従ってください。