驗證使用者

驗證功能讓可擴充服務 Proxy (ESP) 識別呼叫您服務方法的使用者,然後據此決定是否讓他們使用該方法 (授權)。本頁面說明如何將驗證功能與 gRPC 服務專用的 Cloud Endpoints 搭配使用,包括如何在 gRPC 服務中設定 ESP,以支援經過驗證的要求,以及如何從 gRPC 用戶端呼叫經過驗證的方法。

ESP 支援多種驗證方法,包括 FirebaseAuth0Google ID 權杖,所有方法都可以在 gRPC API 設定中設定。在各種情況下,用戶端都需要在其要求中提供識別用的 JSON Web Token (JWT)。ESP 會代表您的 API 驗證權杖,因此您不需要自行新增任何特殊驗證碼。

雖然驗證和 API 金鑰規定可限制哪些人可以呼叫服務的方法,但它們提供的安全性程度不同,且會向呼叫的服務提供不同的資訊。如要進一步瞭解 API 金鑰與驗證的差異,以及何時適合使用各個方案,請參閱「API 金鑰的使用時機與原因」。

如需使用驗證功能的完整工作範例,請參閱「使用服務帳戶進行驗證」一文,瞭解如何在教學課程中為 Bookstore 服務新增驗證功能。

為 ESP 設定驗證功能

您可以使用 authentication 區段,在 gRPC 服務的 gRPC 服務設定 YAML 檔案中設定 gRPC 服務的 Endpoints 驗證。您可以將驗證方法和驗證來源的詳細資料指定為 providers,其中:

  • rules 中使用時,id 值會用於識別驗證提供者:這通常會使用驗證方法的名稱,但不一定。

  • issuer 值是必要權杖的發出者,因此會指定驗證方法。

  • jwks_uri 值是提供者公開金鑰的 URI,用於驗證權杖。部分驗證方法不需要您指定這項資訊,例如 Google ID 權杖,因為 ESP 會自動取得這項資訊。

  • jwt_locations 用於定義要擷取 JWT 的位置。

您可以在同一個檔案中定義多個安全性提供者,但每個提供者都必須有不同的 issuer。詳情請參閱 AuthProvider

您可以使用 rules 指定要使用這些驗證規定的 API 方法,如「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 會接受所有 JWT,其中 aud 憑證附加資訊的後端服務名稱為 https://SERVICE_NAME 格式。如要允許其他用戶端 ID 存取後端服務,您可以使用半形逗號分隔的值,在 audiences 欄位中指定允許的用戶端 ID。接著,ESP 會接受 aud 憑證附加資訊中含有白名單客戶端 ID 的 JWT。

從 gRPC 呼叫通過驗證的方法

如果方法需要驗證,gRPC 用戶端就需要使用方法呼叫,以中繼資料的形式傳遞驗證權杖,其中索引鍵為 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

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/grpc-js');
  const protoLoader = require('@grpc/proto-loader');
  const path = require('path');

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

  const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
  });

  const protoObj = grpc.loadPackageDefinition(packageDefinition).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 物件的字串。ESPv2 和 ESP 的 JSON 物件格式不同。對於 ESPv2,JSON 物件就是原始的 JWT 酬載。針對 ESP,JSON 物件會使用不同的欄位名稱,並將原始 JWT 酬載放在 claims 欄位下。如要進一步瞭解這類格式,請參閱「在後端服務中處理 JWT」。

後續步驟