サービス間の認証

ユーザーの認証だけでなく、他のサービスに API の使用を許可する場合があります。ユーザーが認証情報を送信できるように、クライアント アプリケーションでウェブログイン プロンプトを表示することがありますが、サービス間で安全な通信を行うには別の手段が必要になります。このページでは、サービス間で認証を実装する際のおすすめの方法とサンプルコードを紹介します。

概要

API にリクエストを送信するサービスを識別するには、サービス アカウントを使用します。呼び出し側のサービスはサービス アカウントの秘密鍵を使用して安全な JSON Web Token(JWT)に署名し、署名した JWT をリクエストに含めて API に送信します。

API と呼び出し側のサービスにサービス間認証を実装するには:

  1. 呼び出し側のサービスのサービス アカウントとキーを作成します。
  2. Cloud Endpoints サービスの OpenAPI ドキュメントに認証サポートを追加します。
  3. 次の処理を行うコードを呼び出し側のサービスに追加します。

    • JWT を作成し、サービス アカウントの秘密鍵で署名する。
    • リクエストで署名済みの JWT を API に送信する。

ESP は、リクエストを API に転送する前に、JWT のクレームが OpenAPI ドキュメントの構成と一致することを検証します。ESP は、サービス アカウントに付与されている Cloud Identity の権限を確認しません。

前提条件

すでに以下を行っていることを前提としています。

サービス アカウントとキーを作成する

サービス アカウントには、呼び出し側のサービスが JWT の署名に使用する秘密鍵ファイルが必要です。API にリクエストを送信するサービスが複数ある場合は、すべての呼び出し側サービスに同じ 1 つのサービス アカウントを使用することもできます。サービスによって権限が異なる場合など、サービスを区別する必要がある場合は、呼び出し側のサービスごとにサービス アカウントとキーを作成します。

このセクションでは、Google Cloud Platform Console と gcloud コマンドライン ツールでサービス アカウントと秘密鍵ファイルを作成し、サービス アカウントにサービス アカウント トークン作成者の役割を割り当てる方法を説明します。この操作を API で行う方法については、サービス アカウントの作成と管理をご覧ください。

サービス アカウントとキーを作成するには:

GCP Console

  1. GCP Console で [サービス アカウントキーの作成] ページに移動します。

    [サービス アカウントキーの作成] ページに移動

  2. 使用するプロジェクトを選択します。
  3. [サービス アカウント] プルダウン メニューから [新しいサービス アカウント] を選択します。
  4. [サービス アカウント名] フィールドに名前を入力します。
  5. [役割] プルダウン メニューで、[サービス アカウント]、[サービス アカウント トークン作成者] の順に選択します。
  6. [キーのタイプ] で、デフォルトの [JSON] を使用します。
  7. [作成] をクリックします。サービス アカウントの秘密鍵を含む JSON ファイルがパソコンにダウンロードされます。

gcloud

ローカルマシン上の Cloud SDK で以下のコマンドを実行します。このコマンドは、Cloud Shell 内でも実行できます。

  1. gcloud のデフォルト アカウントを設定します。複数のアカウントがある場合は、GCP プロジェクトで使用するアカウントを選択します。

    gcloud auth login
    
  2. GCP プロジェクトのプロジェクト ID を表示します。

    gcloud projects list
    
  3. デフォルト プロジェクトを設定します。PROJECT_ID は、使用する GCP プロジェクト ID に置き換えます。

    gcloud config set project PROJECT_ID
  4. サービス アカウントを作成します。SA_NAMESA_DISPLAY_NAME は、使用する名前と表示名にそれぞれ置き換えます。

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
    
  5. 作成したサービス アカウントのメールアドレスを表示します。

    gcloud iam service-accounts list
    
  6. サービス アカウント トークン作成者の役割を追加します。SA_EMAIL_ADDRESS は、サービス アカウントのメールアドレスに置き換えます。

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/serviceAccountTokenCreator
    
  7. 現在の作業ディレクトリにサービス アカウントキー ファイルを作成します。FILE_NAME は、キーファイルに使用する名前に置き換えます。gcloud コマンドは、デフォルトで JSON ファイルを作成します。

    gcloud iam service-accounts keys create FILE_NAME.json \
      --iam-account SA_EMAIL_ADDRESS
    

上記のコマンドの詳細については、gcloud リファレンスをご覧ください。

秘密鍵を保護する方法については、認証情報を管理する際のベスト プラクティスをご覧ください。

認証をサポートするように API を構成する

ESP が署名済みの JWT でクレームを検証できるように、OpenAPI ドキュメントにセキュリティ要件オブジェクトセキュリティ定義オブジェクトが設定されている必要があります。

  1. OpenAPI ドキュメントの発行元としてサービス アカウントを追加します。

    securityDefinitions:
      DEFINITION_NAME:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "SA_EMAIL_ADDRESS"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/SA_EMAIL_ADDRESS"
    
    • DEFINITION_NAME は、このセキュリティ定義を識別する文字列に置き換えます。サービス アカウント名または呼び出し側のサービスを識別する名前に置き換えることをおすすめします。
    • SA_EMAIL_ADDRESS は、サービス アカウントのメールアドレスに置き換えます。
    • OpenAPI ドキュメントに複数のセキュリティ定義を記述できますが、定義ごとに異なる x-google-issuer が必要です。呼び出し側のサービスごとに別々のサービス アカウントを作成した場合、サービス アカウントごとにセキュリティ定義を作成できます。次に例を示します。
    securityDefinitions:
      service-1:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com"
      service-2:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
    
  2. 必要であれば、securityDefinitions セクションに x-google-audiences を追加します。x-google-audiences を追加しない場合、JWT に https://SERVICE_NAME 形式の "aud"(audience)クレームが必要になります。SERVICE_NAME は、OpenAPI ドキュメントの host フィールドで構成した Endpoints サービスの名前です。

  3. security セクションを追加します。API 全体に適用する場合はファイルのトップレベル(インデントやネストされていないレベル)に、特定のメソッドに適用する場合はメソッドレベルに追加します。security セクションを API レベルとメソッドレベルの両方で指定した場合、API レベルの設定よりもメソッドレベルの設定が優先されます。

    security:
      - DEFINITION_NAME: []
    
    • DEFINITION_NAME は、securityDefinitions セクションで使用した名前に置き換えます。
    • securityDefinitions セクションに複数の定義がある場合、これらの定義を security セクションに追加します。次に例を示します。

      security:
        - service-1: []
        - service-2: []
      
  4. 更新された OpenAPI ドキュメントをデプロイします。OPENAPI_DOC は、OpenAPI ドキュメントの名前に置き換えます。

    gcloud endpoints services deploy OPENAPI_DOC

ESP がリクエストを API に転送する前に、ESP は次のことを確認します。

  • JWT の署名。公開鍵で確認します。この署名は、OpenAPI ドキュメントの x-google-jwks_uri フィールドに指定された URI にあります。
  • JWT の "iss"(issuer)クレームが x-google-issuer フィールドの値と一致していること。
  • JWT の "aud"(audience)クレームに Endpoints サービス名が含まれているか、x-google-audiences フィールドのいずれかの値に一致していること。
  • トークンが期限切れでないこと。"exp"(expiration time)クレームで確認します。

x-google-issuerx-google-jwks_urix-google-audiences の詳細については、OpenAPI の拡張をご覧ください。

認証されたリクエストを Endpoints API に送信する

認証されたリクエストを行う場合、呼び出し側のサービスは、OpenAPI ドキュメントに指定されたサービス アカウントで署名された JWT を送信します。呼び出し側のサービスは、次の処理を行う必要があります。

  1. JWT を作成し、サービス アカウントの秘密鍵で署名します。
  2. リクエストで署名済みの JWT を API に送信します。

次の例は、このプロセスを実行するサンプルコードです。

  1. 呼び出し側のサービスに以下の関数を追加し、次のパラメータを渡します。
    Java
    • saKeyfile: サービス アカウントの秘密鍵ファイルのフルパス。
    • saEmail: サービス アカウントのメールアドレス。
    • audience: x-google-audiences フィールドを OpenAPI ドキュメントに追加した場合は、x-google-audiences に指定した値の 1 つを audience に設定します。それ以外の場合は、audiencehttps://SERVICE_NAME に設定します。SERVICE_NAME は Endpoints サービスの名前です。
    • expiryLength: JWT の有効期限(秒)。
    Python
    • sa_keyfile: サービス アカウントの秘密鍵ファイルのフルパス。
    • sa_email: サービス アカウントのメールアドレス。
    • audience: x-google-audiences フィールドを OpenAPI ドキュメントに追加した場合は、x-google-audiences に指定したいずれかの値を audience に設定します。それ以外の場合は、audiencehttps://SERVICE_NAME に設定します。SERVICE_NAME は Endpoints サービスの名前です。
    • expiry_length: JWT の有効期限(秒)。
    Go
    • saKeyfile: サービス アカウントの秘密鍵ファイルのフルパス。
    • saEmail: サービス アカウントのメールアドレス。
    • audience: x-google-audiences フィールドを OpenAPI ドキュメントに追加した場合は、x-google-audiences に指定したいずれかの値を audience に設定します。それ以外の場合は、audiencehttps://SERVICE_NAME に設定します。SERVICE_NAME は Endpoints サービスの名前です。
    • expiryLength: JWT の有効期限(秒)。

    この関数は JWT を作成して秘密鍵ファイルで署名し、署名済みの JWT を返します。

    Java
    /**
     * Generates a signed JSON Web Token using a Google API Service Account
     * utilizes com.auth0.jwt.
     */
    public static String generateJwt(final String saKeyfile, final String saEmail,
        final String audience, final int expiryLength)
        throws FileNotFoundException, IOException {
    
      Date now = new Date();
      Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
    
      // Build the JWT payload
      JWTCreator.Builder token = JWT.create()
          .withIssuedAt(now)
          // Expires after 'expiraryLength' seconds
          .withExpiresAt(expTime)
          // Must match 'issuer' in the security configuration in your
          // swagger spec (e.g. service account email)
          .withIssuer(saEmail)
          // Must be either your Endpoints service name, or match the value
          // specified as the 'x-google-audience' in the OpenAPI document
          .withAudience(audience)
          // Subject and email should match the service account's email
          .withSubject(saEmail)
          .withClaim("email", saEmail);
    
      // Sign the JWT with a service account
      FileInputStream stream = new FileInputStream(saKeyfile);
      GoogleCredential cred = GoogleCredential.fromStream(stream);
      RSAPrivateKey key = (RSAPrivateKey) cred.getServiceAccountPrivateKey();
      Algorithm algorithm = Algorithm.RSA256(null, key);
      return token.sign(algorithm);
    }
    Python
    def generate_jwt(sa_keyfile,
                     sa_email='account@project-id.iam.gserviceaccount.com',
                     audience='your-service-name',
                     expiry_length=3600):
    
        """Generates a signed JSON Web Token using a Google API Service Account."""
    
        now = int(time.time())
    
        # build payload
        payload = {
            'iat': now,
            # expires after 'expirary_length' seconds.
            "exp": now + expiry_length,
            # iss must match 'issuer' in the security configuration in your
            # swagger spec (e.g. service account email). It can be any string.
            'iss': sa_email,
            # 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': sa_email,
            'email': sa_email
        }
    
        # sign with keyfile
        signer = google.auth.crypt.RSASigner.from_service_account_file(sa_keyfile)
        jwt = google.auth.jwt.encode(signer, payload)
    
        return jwt
    Go
    // 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 'expiraryLength' 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)
    }
    
  2. 呼び出し側のサービスに以下の関数を追加し、API に対するリクエストの Authorization: Bearer ヘッダーで署名済みの JWT を送信します。
    Java
    /**
     * Makes an authorized request to the endpoint.
     */
    public static String makeJwtRequest(final String singedJwt, final URL url)
        throws IOException, ProtocolException {
    
      HttpURLConnection con = (HttpURLConnection) url.openConnection();
      con.setRequestMethod("GET");
      con.setRequestProperty("Content-Type", "application/json");
      con.setRequestProperty("Authorization", "Bearer " + singedJwt);
    
      InputStreamReader reader = new InputStreamReader(con.getInputStream());
      BufferedReader buffReader = new BufferedReader(reader);
    
      String line;
      StringBuilder result = new StringBuilder();
      while ((line = buffReader.readLine()) != null) {
        result.append(line);
      }
      buffReader.close();
      return result.toString();
    }
    Python
    def make_jwt_request(signed_jwt, url='https://your-endpoint.com'):
        """Makes an authorized request to the endpoint"""
        headers = {
            'Authorization': 'Bearer {}'.format(signed_jwt),
            'content-type': 'application/json'
        }
        response = requests.get(url, headers=headers)
    
        response.raise_for_status()
        return response.text
    Go
    // 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
    }
    

JWT を使用してリクエストを送信する場合は、セキュリティ上の理由から、認証トークンを Authorization: Bearer ヘッダーに入れることをおすすめします。例:

curl --request POST \
  --header "Authorization: Bearer ${TOKEN}" \
  "${ENDPOINTS_HOST}/echo"

ENDPOINTS_HOSTTOKEN はそれぞれ、API のホスト名と認証トークンを格納する環境変数です。

リクエストの送信時にヘッダーを使用できない場合は、access_token というクエリ パラメータに認証トークンを入れることができます。例:

curl --request POST \
  "${ENDPOINTS_HOST}/echo?access_token=${TOKEN}"

API で認証の詳細を受信する

ESP は、受信したすべてのヘッダー(元の Authorization ヘッダーを含む)を API に転送します。さらに X-Endpoint-API-UserInfo ヘッダーで認証結果を API に送信します。このヘッダーは base64 でエンコードされ、次の JSON オブジェクトを格納します。

{
  "issuer": TOKEN_ISSUER,
  "id": USER_ID,
  "email" : USER_EMAIL
}

次のステップ

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

OpenAPI を使用した Cloud Endpoints
ご不明な点がありましたら、Google のサポートページをご覧ください。