#GCP #GAE #Pub/Sub
- GCP
- [push サブスクリプションの使用 | Cloud Pub/Sub ドキュメント | Google Cloud https://cloud.google.com/pubsub/docs/push#authentication_and_authorization_by_the_push_endpoint] まずはこれを読むと良い
- 
- 
- サンプルコード、説明は少ないけどドキュメントより詳しい
-  auth-libraryのoauth client
- 
- GCP の認証周りの Document、これが一番くわしいと思うけどちゃんと読めてない
- JWT / OpenID
- [JSON Web Token(JWT)の紹介とYahoo! JAPANにおけるJWTの活用 - Yahoo! JAPAN Tech Blog https://techblog.yahoo.co.jp/advent-calendar-2017/jwt/] ド基礎
- [IDトークンが分かれば OpenID Connect が分かる - Qiita https://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06] (適宜RFCを参照)
- [OpenID Connect の JWT の署名を自力で検証してみると見えてきた公開鍵暗号の実装の話 - Qiita https://qiita.com/bobunderson/items/d48f89e2b3e6ad9f9c4c#%E5%85%AC%E9%96%8B%E9%8D%B5%E3%81%AE%E5%85%A5%E6%89%8B]
- JWK
gcloud pubsub subscriptions create
で endpoint を指定するだけ
https://cloud.google.com/pubsub/docs/push#authentication_and_authorization_by_the_push_endpoint
Pub/Sub サブスクリプションでは、すべてのメッセージを Webhook(push エンドポイント)の URL に対する HTTP POST リクエストとして送信するように構成できます。通常、push エンドポイントは、一般公開された HTTPS サーバーにします。これは、認証局が署名した有効な SSL 証明書を提示し、DNS によるルーティングが可能なサーバーである必要があります。
https://cloud.google.com/pubsub/docs/push#setting_up_for_push_authentication
- push を行うサービスアカウントに
roles/iam.serviceAccountTokenCreator
を付与する- これはどうやら、自らの IDTokenを作成、JWTに署名をするための権限
-
サービス アカウント権限を使用できます(OAuth2 アクセス トークンを作成し、blob または JWT などに署名します)。
gcloud pubsub subscriptions create
で subsription を作成するときに、先程roles/iam.serviceAccountTokenCreator
を付与したサービスアカウントを--push-auth-service-account
で指定。--push-auth-token-audience
どちらでも良い(後述)。
これで、Pub/Sub push subscription が 指定した endpoint に post リクエストを投げるときに Authorization
ヘッダに署名済み Id Token を投げてくれる。
こういうの
"Authorization" : "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkNjgwZDhjNzBkNDRlOTQ3MTMzY2JkNDk5ZWJjMWE2MWMzZDVh YmMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXpwIjoiMTEzNzc0M jY0NDYzMDM4MzIxOTY0IiwiZW1haWwiOiJnYWUtZ2NwQGFwcHNwb3QuZ3NlcnZpY2VhY2NvdW50LmNvb SIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NTAxODU5MzUsImlhdCI6MTU1MDE4MjMzNSwia XNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEzNzc0MjY0NDYzMDM4MzIxO TY0In0.QVjyqpmadTyDZmlX2u3jWd1kJ68YkdwsRZDo-QxSPbxjug4ucLBwAs2QePrcgZ6hhkvdc4UHY 4YF3fz9g7XHULNVIzX5xh02qXEH8dK6PgGndIWcZQzjSYfgO-q-R2oo2hNM5HBBsQN4ARtGK_acG-NGG WM3CQfahbEjZPAJe_B8M7HfIu_G5jOLZCw2EUcGo8BvEwGcLWB2WqEgRM0-xt5-UPzoa3-FpSPG7DHk7 z9zRUeq6eB__ldb-2o4RciJmjVwHgnYqn3VvlX9oVKEgXpNFhKuYA-mWh5o7BCwhujSMmFoBOh6mbIXF cyf5UiVqKjpqEbqPGo_AvKvIQ9VTQ" (from https://cloud.google.com/pubsub/docs/push#using_json_web_tokens_jwts )
nodejs の場合 google-auth-library
を使うと楽ちん
- 
- サンプルコード
code:javascript
- import { OAuth2Client } from 'google-auth-library'
- const authClient = new OAuth2Client()
- // ...
- // Authorization header から bearer token (JWT) を取得
- const bearer = req.header('Authorization')
- if (!bearer) {
- res.status(400).send('Missing mandatory header "Authorization".')
- return
- }
- const found = bearer?.match(/Bearer (.*)/)
- const token = found?.[1]
- if (!token) {
- res.status(400).send('Invalid Authorization header.')
- return
- }
- // google-auth-library の verifyIdToken を使って JWT を検証
- const ticket: LoginTicket = await authClient.verifyIdToken({
- idToken: token,
- // A single, case-insensitive string that can be used by the webhook to validate the intended audience of this particular token.
- audience: 'example.com', // TDOO: audience の設定
- })
- // 上の過程で検証に失敗した場合は Promise abort が返される、処理を try / catch で囲って、catch の中で 400エラーを返す。
サンプルコードでは loginTicket.getPayload
してるけど、getPayload はJWTのpayload部分を返すだけなので認証処理には関係ない。 LoginTicket
が返ってきたら検証成功してる。
audience には何を指定するのか
- audience には Pub/Sub push subscriber から渡された JWT の payload の中の
aud
claim と同じものを渡す必要がある。- [aud claim https://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06#73-aud-%E3%82%AF%E3%83%AC%E3%83%BC%E3%83%A0]
- Pub/Sub push subscriber が指定してくる JWT の aud には
--push-auth-token-audience
で指定した文字列が渡ってくる。- 指定しない場合は push endpoint
- 例えば
--push-auth-token-audience='screening'
とした場合はverifyIdToken
のaudience
にもscreening
を渡すと良い。

verifyIdToken
は受け取った id token を検証するわけだが、具体的に何をしているのか
https://github.com/googleapis/google-auth-library-nodejs/blob/cc10eb75b9387b782c6ea2d2a8f1798f60a52707/src/auth/oauth2client.ts#L976-L1022
やってることは主に2つ
- getFederatedSignonCertsAsync でGCPサーバーから(JWTの署名に利用した秘密鍵に対応する)公開鍵を取得
- verifySignedJwtWithCertsAsync で取得した公開鍵を使って、JWTの署名を検証
getFederatedSignonCertsAsync
- https://www.googleapis.com/oauth2/v3/certs をとってくるだけ(JWKなら)
- JWK Set Document (JWKのリスト)
- 各鍵にはkidがふられていて、JWS header で指定されていた kid を使って特定の kid に対応する鍵をとってくる https://github.com/googleapis/google-auth-library-nodejs/blob/cc10eb75b9387b782c6ea2d2a8f1798f60a52707/src/auth/oauth2client.ts#L1239
- [~ 気になる] https://www.googleapis.com/oauth2/v3/certs は global な公開鍵(2020/4/14現在2つだけ)? JWT 署名に利用した秘密鍵もGCP全体で2,3個しか管理されてないやつを使っている?
- certsはインスタンスキャッシュされてる https://github.com/googleapis/google-auth-library-nodejs/blob/cc10eb75b9387b782c6ea2d2a8f1798f60a52707/src/auth/oauth2client.ts#L1075-L1081
- format が変わったり、expire がきれるまでキャッシュされる
- format はそんな変わらんだろ、変わらんよね?
verifySignedJwtWithCertsAsync
- 取得した公開鍵を使って、JWTの署名を検証
- 詳しくは見てない。