Google Cloud APIのアクセストークンをSDKを使わずに手に入れる
SDKを使わずにGoogle Cloud APIを使いたい。アクセストークンどうする?
以下のような状況になったとします。
- SDKが使えない状況でGoogle Cloudのサービスを利用したい。この場合、各サービスのREST APIを呼び出すことになる。
- REST APIを使うには、アクセストークンが必要である。
- アクセストークンもSDKを使わずに手に入れたい。
そこで、SDKを使わずに特定のサービスアカウントに紐づくアクセストークンを手に入れる方法を考えてみます。
前提として、OAuth 2.0の認可コードフローなどのようなユーザーの認証が挟まる方法ではなく、対象のサービスアカウントのクレデンシャル(json形式の鍵ファイル)だけを使って実現できる方法を考えます。
※そのほかのアクセストークン取得方法は、記事の後ろで紹介します。
※この記事で示すサンプルコードはすべてGolangですが、ほかの言語でも同様の動きになるはずです。
RFC 7523を活用してみる
Google Cloud APIのアクセストークンは、OAuth 2.0のアクセストークンです。なので、OAuthの枠組みの中で、アクセストークンを取る、というアプローチなら、SDKはなくても、アクセストークンを取得できそうです。Google のOAuth 2.0のトークンのエンドポイントは、https://oauth2.googleapis.com/token
なので、ここになんらかの情報をPOSTして、アクセストークンを取得することができそうです。
さて、RFC 7523 というRFCがあります。この仕様には、This specification defines the use of a JSON Web Token (JWT) Bearer Token as a means for requesting an OAuth 2.0 access token as well as for client authentication.
と書かれています。つまり、JSON Web Tokenを使って、OAuth 2.0のアクセストークンを取得する仕様、ということです。この仕様については、以下の記事に詳細に説明されています。
簡単に言えば、tokenのエンドポイントにJSON Web Token(JWT)を渡すことで、アクセストークンを取得できます。では、JWTはどのように手に入れればいいのか。2つのやり方を試してみます。
自力でJWTを作る
json形式の鍵ファイルの中身に、秘密鍵があります。これを使って、自力で、JWTを作る方法を試してみます。
package main
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/dgrijalva/jwt-go"
)
var privateKey string = `-----BEGIN PRIVATE KEY-----
...(中略)...
-----END PRIVATE KEY-----
`
func main() {
block, _ := pem.Decode([]byte(privateKey))
if block == nil {
panic("failed to parse PEM block containing the key")
}
// RSA秘密鍵のパース
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
tokenURL := "https://oauth2.googleapis.com/token"
// JWTの作成
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iss": "test@hoge.iam.gserviceaccount.com",
"sub": "test@hoge.iam.gserviceaccount.com",
"aud": tokenURL,
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
"scope": "https://www.googleapis.com/auth/cloud-platform",
})
// サービスアカウントのプライベートキーで署名
signedToken, err := token.SignedString(privateKey)
if err != nil {
panic(err)
}
// アクセストークンの取得
values := url.Values{
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
"assertion": {signedToken},
"scope": {"https://www.googleapis.com/auth/cloud-platform"},
}
resp, err := http.PostForm(tokenURL, values)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
var privateKey string
は、鍵ファイルの中のprivate_keyフィールドの値です。pem形式の秘密鍵です。実際には、コードにべた書きするのは現実的ではないので、鍵ファイルをそのまま、構造体にjson.Unmarshalするのがよいと思います。
github.com/dgrijalva/jwt-go
というライブラリを使って、JWTを生成します。JWT自体はオープンな仕様なので、ライブラリを使わなくても作成できますが、ここでは簡便化(自力でやると署名が面倒)のため、ライブラリを使います。
トークンURL(https://oauth2.googleapis.com/token
)にポストする内容は、上掲したQittaの記事などを参考にし、Content-Type: application/x-www-form-urlencoded
で以下の内容を含めました。
- grant_type ...
urn:ietf:params:oauth:grant-type:jwt-bearer
- assertion ... 作成したJWT
- scope ...
https://www.googleapis.com/auth/cloud-platform
このコードを実行してみると、以下の通りアクセストークンが取得できました。
{"access_token":"xxxx...(略)...abc_x5","expires_in":3599,"token_type":"Bearer"}
以下のエンドポイントを使うと、トークンの情報が確認できますので、これで確認してみると、正当な結果がかえってきました。
curl https://oauth2.googleapis.com/tokeninfo?access_token=xxxx....
あとは、実際にこのアクセストークンが使えるのか、確認したいです。前回書いた記事でvbsからCloud Tasksを使おうとしました。上で作成したアクセストークンをこのvbsに設定してキューにタスクが突っ込めるか確認してみたところ、無事、エンキューできました。
gcloudコマンドで取得したIDトークンでアクセストークンを手に入れようとしてみる
Google Cloudは、ユーザー/サービスアカウントのIDトークンを提供します。IDトークンは、JWTなので、わざわざ、JWTを自力でつくらなくても、Google Cloudの提供するIDトークンで代替できないか、と考えるのは自然です。
が、結論からいうとうまくいきませんでした。
Google Cloudの認証が通っている環境で、以下のコマンドを実行すれば、その認証済みのユーザー/サービスアカウントのIDトークンを取得できます。
gcloud auth print-identity-token --audiences="https://oauth2.googleapis.com/token"
アクセストークン取得で使うので、--audiences
にトークン取得のエンドポイントを設定しています。
では、これで取得したIDトークンで、以下のコードで、SDKを使わずに、アクセストークンを取得してみます。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
func main() {
// アクセストークンの取得
values := url.Values{
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
"assertion": {"<ここにgcloudで取得したIDトークンを設定>"},
"scope": {"https://www.googleapis.com/auth/cloud-platform"},
}
resp, err := http.PostForm("https://oauth2.googleapis.com/token", values)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
しかし、結果は以下の通りでエラーになりアクセストークンは取得できませんでした。
{
"error": "invalid_client",
"error_description": "The OAuth client was not found."
}
まとめ
- RFC 7523に従って、アクセストークンを取得することができる。
- この場合、Google CloudのSDKは、不要。
- JSON Web Tokenが作成できる処理系であれば、SDKが提供されていなくても、アクセストークンが取得できるので、Google Cloud APIをREST APIで活用できる。
[補足] そのほかのアクセストークンの取得方法
前回書いた記事では、gcloudコマンドを使ってアクセストークンを取得しましたが、そのほかにもいくつか取得方法があります。
SDKを使ってアクセストークンを取ってみる
Google Cloudの各サービスのSDKを使った場合、そのSDKが内部的に認証/認可の処理を行っているので、SDKがあるなら、わざわざ、アクセストークンを取得する必要はありません。ここでは、参考までに、SDKを使ってアクセストークンだけ取得する方法をGolangで示します。
package main
import (
"context"
"fmt"
"log"
credentials "cloud.google.com/go/iam/credentials/apiv1"
credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
)
func main() {
ctx := context.Background()
c, err := credentials.NewIamCredentialsClient(ctx)
if err != nil {
log.Fatal(err)
}
defer c.Close()
req := &credentialspb.GenerateAccessTokenRequest{
Name: "projects/-/serviceAccounts/test@hoge.iam.gserviceaccount.com",
Scope: []string{
"https://www.googleapis.com/auth/cloud-platform",
},
}
resp, err := c.GenerateAccessToken(ctx, req)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.GetAccessToken())
}
デフォルト認証で、なんらかのユーザー/サービスアカウントで認証が通っているという前提です。また、そのユーザー/サービスアカウントは、サービス アカウント トークン作成者(roles/iam.serviceAccountTokenCreator)
を持っている必要があります。
GenerateAccessTokenRequestですが、
-
Name
... これには、取得したいアクセストークンが紐づくサービスアカウントを指定します。projects/-/serviceAccounts/<メールアドレス>
という形式です。 -
Scope
... Google APIのOAuth 2.0 スコープ から、自分が使いたいAPIのスコープを選びます。今回は、Cloud TaskのREST APIを使ってみたいので、https://www.googleapis.com/auth/cloud-platform
を設定しています。
適切に設定が行われていれば、このコードを実行すると、アクセストークンが取得できることがわかります。
GolangのSDKで示しましたが、もちろん、ほかの言語でもあります。google cloud iam credentials <言語名>
でGoogle検索してみれば、見つかると思います。
Cloud Runからメタデータサーバにアクセスしてアクセストークンを取る
Cloud Runで動いているウェブアプリから、メタデータサーバへGETのリクエストを飛ばすだけで、アクセストークンを取ることができます。この場合、このCloud Runの実行アカウントのアクセストークンが手に入ります。この方法なら、SDKを使えない環境でも、Httpリクエストを使って、Cloud Runからアクセストークンを手に入れることができそうです。
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", handle)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil {
log.Fatal(err)
}
}
func handle(w http.ResponseWriter, r *http.Request) {
token, err := fetchMetadataToken()
if err != nil {
http.Error(w, "Error fetching access token", http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "Access Token:", token)
}
func fetchMetadataToken() (string, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token", nil)
if err != nil {
return "", err
}
req.Header.Add("Metadata-Flavor", "Google")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
同様の方法でIDトークンも取得できます。が、前掲したgcloudコマンドで取得したIDトークン同様、アクセストークン取得には利用できませんでした。
Discussion