クラウド同上

認証機能付きAPIが簡単に作れるCloud Endpoints入門2 JWTトークン認証

Author
kubosuke
Lv: Exp:

module kubosuke

require cloud-ace.jp/basketball/rock/backpacker v0.0.1

前回(認証機能付きAPIが簡単に作れるCloud Endpoints入門)では、APIキー認証 + Cloud Endpoints + GKE のAPI作成方法をご紹介しました。

ただし、公式ドキュメントにもある通り、APIキーによる認証は下記の理由により、安全性が高くありません。

  1. 有効期限が無い
  2. 署名付きではない
  3. クライアントが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キーですが、呼び出し回数の制限や、ログ分化でその威力を発揮します。ユースケースにマッチしている場合、トークン認証と組み合わせてみるのも良いかもしれません。

次の記事を読み込んでいます
次の記事を読み込んでいます