ユーザー認証

認証を行うことで、Extensible Service Proxy(ESP)は、サービスのメソッドを呼び出しているユーザーを識別した後、それに基づいて、ユーザーにそのメソッドの使用を許可するかどうか(承認)を決定できます。このページでは、Cloud Endpoints for gRPC サービスでの認証の仕組みについて説明します。また、認証済みリクエストをサポートするように gRPC サービスで ESP を構成する方法と、gRPC クライアントから認証済みのメソッドを呼び出す方法についても説明します。

ESP は、FirebaseAuth0Google ID トークンを含む複数の認証方法をサポートしています。これらのすべての方法は、gRPC API 構成の一部として設定できます。認証方法にかかわらず、クライアントは身元を示す JSON ウェブトークン(JWT)をリクエストに含める必要があります。ESP は API に代わってトークンを検証するので、ユーザーが自分で特別な認証コードを追加する必要はありません。

認証と API キーの要求はどちらもサービスのメソッドを呼び出すことができるユーザーを制限しますが、提供されるセキュリティ レベルは同じではなく、呼び出されるサービスに渡される情報も異なります。API キーと認証の違い、およびそれぞれの方法を使用するのに適した状況について詳しくは、API キーを使用する場合と理由をご覧ください。

認証を使用した実例について詳しくは、サービス アカウントを使用した認証をご覧ください。この例では、チュートリアルで使用している Bookstore サービスに認証を追加しています。

ESP による認証の設定

Endpoints for gRPC サービスの認証を構成するには、gRPC サービス構成 YAML ファイル内の authentication セクションを使用します。認証方法と認証の提供元の詳細を、providers として指定します。各パラメータの内容は次のとおりです。

  • id の値は、rules 内で認証プロバイダを識別するために使用します。通常は認証方法の名前を使用しますが、そうではない場合もあります。

  • issuer の値は、必要なトークンの発行元であり、したがって認証方法を指定します。

  • jwks_uri 値はプロバイダの公開鍵の URI で、トークンの検証に使用されます。この値を指定する必要のない認証方法もあります。たとえば、Google ID トークンでは ESP がこの情報を自動的に取得します。

  • jwt_locations は、JWT を抽出する場所を定義するために使用されます。

同じファイルに複数のセキュリティ プロバイダを定義できますが、各プロバイダは異なる issuer を持つ必要があります。詳細については、AuthProvider をご覧ください。

これらの認証要件を使用する API メソッドを rules を使用して指定します。詳しくは、AuthenticationRule をご覧ください。

以下に、サポートされるいくつかの認証方法を対象に gRPC サービスで ESP を設定する例を示します。

Firebase

Firebase 認証をサポートするには:

authentication:
  providers:
  - id: firebase
    jwks_uri: https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com
    # Replace FIREBASE-PROJECT-ID with your Firebase project ID
    issuer: https://securetoken.google.com/FIREBASE-PROJECT-ID
    audiences: "FIREBASE-PROJECT-ID"
    # Optional.
    jwt_locations:
    # expect header "jwt-header-foo": "jwt-prefix-foo<TOKEN>"
    - header: "jwt-header-foo"
      value_prefix: "jwt-prefix-foo"
    - query: "jwt_query_bar"
  rules:
  - selector: "*"
    requirements:
      - provider_id: firebase

Auth0

Auth0 認証をサポートするには:

authentication:
  providers:
  - id: auth0_jwk
    # Replace YOUR-ACCOUNT-NAME with your service account's email address.
    issuer: https://YOUR-ACCOUNT-NAME.auth0.com/
    jwks_uri: "https://YOUR-ACCOUNT-NAME.auth0.com/.well-known/jwks.json"
    # Optional. Replace YOUR-CLIENT-ID with your client ID
    audiences: "YOUR-CLIENT-ID"
  rules:
  - selector: "*"
    requirements:
      - provider_id: auth0_jwk

Google ID トークン

Google ID トークンを使用した認証をサポートするには:

authentication:
  providers:
  - id: google_id_token
    # This "issuer" field has to match the field "iss" in the JWT token.
     # Sometime it is "accounts.google.com".
    issuer: https://accounts.google.com
    # Optional. Replace YOUR-CLIENT-ID with your client ID
    audiences: "YOUR-CLIENT-ID"
  rules:
  - selector: "*"
    requirements:
      - provider_id: google_id_token

カスタム

カスタム認証をサポートするには:

authentication:
  providers:
  - id: custom_auth_id
    # The value below should be unique
    issuer: issuer of the token
    jwks_uri: url to the public key
    # Optional. Replace YOUR-CLIENT-ID with your client ID
    audiences: "YOUR-CLIENT-ID"
 rules:
 - selector: "*"
   requirements:
     - provider_id: custom_auth_id

Firebase 認証の場合、audiences フィールドは必須で、Firebase プロジェクト ID を指定する必要があります。他のすべての認証方法では省略できます。このフィールドを指定しない場合は、ESP が aud クレーム内で https://SERVICE_NAME という形式のバックエンド サービス名を持つすべての JWT を受け入れます。他のクライアント ID がバックエンド サービスにアクセスすることを許可するには、audiences フィールドに、許可するクライアント ID をカンマ区切りの値を使用して指定します。これにより、ESP は aud クレーム内で、クライアント ID がホワイトリストに登録されている JWT を受け入れます。

認証が必要な gRPC メソッドの呼び出し

認証を必要とするメソッドを呼び出す場合、gRPC クライアントはメソッド呼び出しで、認証トークンを metadata として渡す必要があります。このメタデータのキーは authorization、値は Bearer <JWT_TOKEN> です。Python、Node.js、Java で Bookstore のサンプルを呼び出す例をご確認ください。

Python

def run(host, port, api_key, auth_token, timeout, use_tls, servername_override, ca_path):
    """Makes a basic ListShelves call against a gRPC Bookstore server."""

    if use_tls:
        with open(ca_path, 'rb') as f:
            creds = grpc.ssl_channel_credentials(f.read())
        channel_opts = ()
        if servername_override:
            channel_opts += ((
                        'grpc.ssl_target_name_override', servername_override,),)
        channel = grpc.secure_channel(f'{host}:{port}', creds, channel_opts)
    else:
        channel = grpc.insecure_channel(f'{host}:{port}')

    stub = bookstore_pb2_grpc.BookstoreStub(channel)
    metadata = []
    if api_key:
        metadata.append(('x-api-key', api_key))
    if auth_token:
        metadata.append(('authorization', 'Bearer ' + auth_token))
    shelves = stub.ListShelves(empty_pb2.Empty(), timeout, metadata=metadata)
    print(f'ListShelves: {shelves}')

Java

private static final class Interceptor implements ClientInterceptor {
  private final String apiKey;
  private final String authToken;

  private static Logger LOGGER = Logger.getLogger("InfoLogging");

  private static Metadata.Key<String> API_KEY_HEADER =
      Metadata.Key.of("x-api-key", Metadata.ASCII_STRING_MARSHALLER);
  private static Metadata.Key<String> AUTHORIZATION_HEADER =
      Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);

  public Interceptor(String apiKey, String authToken) {
    this.apiKey = apiKey;
    this.authToken = authToken;
  }

  @Override
  public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
      MethodDescriptor<ReqT,RespT> method, CallOptions callOptions, Channel next) {
    LOGGER.info("Intercepted " + method.getFullMethodName());
    ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);

    call = new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) {
      @Override
      public void start(Listener<RespT> responseListener, Metadata headers) {
        if (apiKey != null && !apiKey.isEmpty()) {
          LOGGER.info("Attaching API Key: " + apiKey);
          headers.put(API_KEY_HEADER, apiKey);
        }
        if (authToken != null && !authToken.isEmpty()) {
          System.out.println("Attaching auth token");
          headers.put(AUTHORIZATION_HEADER, "Bearer " + authToken);
        }
        super.start(responseListener, headers);
      }
    };
    return call;
  }
}

Node.js

const makeGrpcRequest = (JWT_AUTH_TOKEN, API_KEY, HOST, GREETEE) => {
  // Uncomment these lines to set their values
  // const JWT_AUTH_TOKEN = 'YOUR_JWT_AUTH_TOKEN';
  // const API_KEY = 'YOUR_API_KEY';
  // const HOST = 'localhost:50051'; // The IP address of your endpoints host
  // const GREETEE = 'world';

  // Import required libraries
  const grpc = require('grpc');
  const path = require('path');

  // Load protobuf spec for an example API
  const PROTO_PATH = path.join(__dirname, '/protos/helloworld.proto');
  const protoObj = grpc.load(PROTO_PATH).helloworld;

  // Create a client for the protobuf spec
  const client = new protoObj.Greeter(HOST, grpc.credentials.createInsecure());

  // Build gRPC request
  const metadata = new grpc.Metadata();
  if (API_KEY) {
    metadata.add('x-api-key', API_KEY);
  } else if (JWT_AUTH_TOKEN) {
    metadata.add('authorization', `Bearer ${JWT_AUTH_TOKEN}`);
  }

  // Execute gRPC request
  client.sayHello({name: GREETEE}, metadata, (err, response) => {
    if (err) {
      console.error(err);
    }

    if (response) {
      console.log(response.message);
    }
  });
};

クライアントが送信する有効な JWT を取得する方法は、認証方法によって異なります。

API での認証結果の受信

通常、ESP は受信したすべてのヘッダーを転送します。ただし、バックエンド アドレスが OpenAPI 仕様の x-google-backend または gRPC サービス構成の BackendRule で指定されている場合は、元の Authorization ヘッダーより優先します。

ESP は認証結果を X-Endpoint-API-UserInfo でバックエンド API に送信します。元の Authorization ヘッダーではなく、このヘッダーを使用することをおすすめします。このヘッダーは、base64url が JSON オブジェクトをエンコードする文字列です。JSON オブジェクトの形式は ESPv2 と ESP で異なります。ESPv2 では、JSON オブジェクトは元の JWT ペイロードになります。ESP では、JSON オブジェクトは異なるフィールド名を使用し、元の JWT ペイロードを claims フィールドに配置します。形式の詳細については、バックエンド サービスにおける JWT の取り扱いをご覧ください。

次のステップ