前回(認証機能付きAPIが簡単に作れるCloud Endpoints入門)では、APIキー認証 + Cloud Endpoints + GKE のAPI作成方法をご紹介しました。
ただし、公式ドキュメントにもある通り、APIキーによる認証は下記の理由により、安全性が高くありません。
- 有効期限が無い
- 署名付きではない
- クライアントがAPIキーを参照できる
そのため、外部サービスがAPIリソースにアクセスする場合、他の手段を用いる必要があります。
本稿では、JWTトークンを用いた認証方法をご紹介致します。
目次
手順
環境は前回の「Google Kubernetes EngineでバックエンドAPIのクラスタを構築する」までを流用します。IAM Service Accountにて、JWTの署名に利用する秘密鍵ファイルを作成します。
IAM Service account を作成する
JWTトークン署名用の秘密鍵を生成するために、IAM Service account を作成します。
gcloud iam service-accounts create sa-api-client --display-name "sa-api-client"
作成されたことを確認します。
gcloud iam service-accounts list | grep sa-api
作成したService accountに、トークン作成を許可するRoleを付与します。
gcloud projects add-iam-policy-binding “[PROJECT ID]” --member serviceAccount:sa-api-client@[PROJECT ID].iam.gserviceaccount.com --role roles/iam.serviceAccountTokenCreator
作成したService accountを用いて、署名用秘密鍵を生成します。
gcloud iam service-accounts keys create credential.json --iam-account sa-api-client@[PROJECT ID].iam.gserviceaccount.com
これで署名用秘密鍵の作成が完了しました。
作成した秘密鍵は、APIにリクエストする外部サービスに渡しておきます。
Cloud Endpoints Serviceをdeployする
deployしたkubernetesのserviceを確認し、外部IPを書きとめます。
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
endpoints-service LoadBalancer 10.31.253.105 35.221.133.165 80:30751/TCP 2m35s
OpenAPIのsecurityDefinitionを設定します。
swagger: "2.0"
info:
description: "A simple Google Cloud Endpoints API example."
title: "Endpoints Example"
version: "1.0.0"
host: "sample-api.endpoints.[PROJECT ID].cloud.goog"
x-google-endpoints:
- name: "sample-api.endpoints.[PROJECT ID].cloud.goog"
target: "35.221.133.165"
consumes:
- "application/json"
- "text/html"
produces:
- "application/json"
- "text/html"
schemes:
- "http"
paths:
/hello:
get:
operationId: helloWorld
description: Returns greeting message.
produces:
- text/plain
responses:
'200':
description: returns hello world.
schema:
type: string
security:
- sample_jwt: []
securityDefinitions:
sample_jwt:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
x-google-issuer: "sa-api-client@[PROJECT ID].iam.gserviceaccount.com"
x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/sa-api-client@[PROJECT ID].iam.gserviceaccount.com"
- x-google-issuer
認証情報の発行者を指定します。
今回はService accountで秘密鍵を発行しているため、Service account のメールアドレスを指定します。 -
x-google-jwks_uri
トークンエンドポイントです。
deployします。
gcloud endpoints services deploy openapi.yaml
deployしたCloud Endpoints と Service account を紐付ける
Cloud Endpoints と、Service accountを紐づけます。
これにより、Service accountで発行したトークンが、Cloud Endpointsの認証に利用できるようになります。
Endpoints -> Services -> add member
新たに追加するメンバーとして、Service accountのメールアドレスを入力、 RoleはService Management の中にある Service Consumer を指定します。
検証する
署名付きJWTをAuthorizationヘッダに追加し、作成したエンドポイントにリクエストを送信します。
公式ドキュメントには、Go, Java, Pythonでのリクエスト例が記載されています。
下記、Goの例を示します。サンプルコードの修正は不要です。
git clone https://github.com/GoogleCloudPlatform/golang-samples.git
mv credential.json golang-samples/endpoints/getting-started/client/.
cd golang-samples/endpoints/getting-started/client
- generateJWT()
秘密鍵で署名したJWTトークンを期限指定で発行します。
// generateJWT creates a signed JSON Web Token using a Google API Service Account.
func generateJWT(saKeyfile, saEmail, audience string, expiryLength int64) (string, error) {
now := time.Now().Unix()
// Build the JWT payload.
jwt := &jws.ClaimSet{
Iat: now,
// expires after 'expiryLength' seconds.
Exp: now + expiryLength,
// Iss must match 'issuer' in the security configuration in your
// swagger spec (e.g. service account email). It can be any string.
Iss: saEmail,
// Aud must be either your Endpoints service name, or match the value
// specified as the 'x-google-audience' in the OpenAPI document.
Aud: audience,
// Sub and Email should match the service account's email address.
Sub: saEmail,
PrivateClaims: map[string]interface{}{"email": saEmail},
}
jwsHeader := &jws.Header{
Algorithm: "RS256",
Typ: "JWT",
}
// Extract the RSA private key from the service account keyfile.
sa, err := ioutil.ReadFile(saKeyfile)
if err != nil {
return "", fmt.Errorf("Could not read service account file: %v", err)
}
conf, err := google.JWTConfigFromJSON(sa)
if err != nil {
return "", fmt.Errorf("Could not parse service account JSON: %v", err)
}
block, _ := pem.Decode(conf.PrivateKey)
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("private key parse error: %v", err)
}
rsaKey, ok := parsedKey.(*rsa.PrivateKey)
// Sign the JWT with the service account's private key.
if !ok {
return "", errors.New("private key failed rsa.PrivateKey type assertion")
}
return jws.Encode(jwsHeader, jwt, rsaKey)
}
- makeJWTRequest()
JWTトークンをAuthorizationヘッダに載せて、指定したエンドポイントにリクエストします。
// makeJWTRequest sends an authorized request to your deployed endpoint.
func makeJWTRequest(signedJWT, url string) (string, error) {
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create HTTP request: %v", err)
}
req.Header.Add("Authorization", "Bearer "+signedJWT)
req.Header.Add("content-type", "application/json")
response, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("HTTP request failed: %v", err)
}
defer response.Body.Close()
responseData, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("failed to parse HTTP response: %v", err)
}
return string(responseData), nil
}
cloneしたスクリプトを利用し、作成したエンドポイントへリクエストを送ります。
go run main.go --host http://sample-api.endpoints.[PROJECT ID].cloud.goog/hello --audience https://sample-api.endpoints.[PROJECT ID].cloud.goog --service-account-file credential.json --service-account-email sa-api-client@[PROJECT ID].iam.gserviceaccount.com
http://sample-api.endpoints.[PROJECT ID].cloud.goog/hello
APIからのレスポンスが確認できました。
Response: Hello, World
トークンを指定しない場合、Validationに失敗することが確認できます。
curl http://sample-api.endpoints.[PROJECT ID].cloud.goog/hello
{
"code": 16,
"message": "JWT validation failed: Missing or invalid credentials",
"details": [
{
"@type": "type.googleapis.com/google.rpc.DebugInfo",
"stackEntries": [],
"detail": "auth"
}
]
}
まとめ
本稿では、JWTトークンを利用したCloud Endpointsの認証についてまとめました。
外部サービスからGCP内で作成したAPIリソースへリクエストする場合、この方法が有効です。
なお、認証に不適と上述したAPIキーですが、呼び出し回数の制限や、ログ分化でその威力を発揮します。ユースケースにマッチしている場合、トークン認証と組み合わせてみるのも良いかもしれません。