사용자 인증

Extensible Service Proxy(ESP)는 인증을 통해 서비스 메서드를 호출하는 사용자를 식별하고, 이를 기준으로 사용자가 해당 메서드를 사용하도록 허가(승인)할지 여부를 결정할 수 있습니다. 이 페이지에서는 인증된 요청을 지원하도록 gRPC 서비스에서 ESP를 구성하는 방법과 gRPC 클라이언트에서 인증된 메서드를 호출하는 방법을 비롯하여 gRPC 서비스에 대한 Cloud Endpoints의 인증 방식을 설명합니다.

ESP는 Firebase, Auth0Google ID 토큰을 포함하여 gRPC API 구성의 일부로 설정할 수 있는 여러 가지 인증 메서드를 지원합니다. 각각의 경우에 클라이언트는 해당 요청에 식별 JSON 웹 토큰(JWT)을 제공해야 합니다. ESP는 API를 대신하여 토큰의 유효성을 검사하므로 특별한 인증 코드를 직접 추가할 필요가 없습니다.

인증 및 API 키 요구사항은 서비스의 메서드를 호출할 수 있는 사용자를 제한할 수 있지만 동일한 보안 수준을 제공하지 않으며 호출되는 서비스에 다른 정보를 제공합니다. API 키와 인증 간의 차이점과 각 방식을 사용하기에 적합한 경우를 알아보려면 API 키를 사용하는 시점과 이유를 참조하세요.

인증을 사용하는 작업 예시에 대해서는 가이드에서 Bookstore 서비스에 인증을 추가하는 서비스 계정을 사용하여 인증을 참조하세요.

ESP를 위한 인증 구성

authentication 섹션을 사용하여 해당 gRPC 서비스 구성 YAML 파일에서 gRPC Endpoints 서비스에 대한 인증을 구성합니다. 인증 메서드 및 인증 소스의 세부정보를 providers로 지정합니다. 각 항목의 의미는 다음과 같습니다.

  • id 값은 rules에서 인증 제공업체를 식별하는 데 사용됩니다. 일반적으로 인증 메서드의 이름을 사용하지만 꼭 그래야 하는 것은 아닙니다.

  • issuer 값은 필요한 토큰의 발급자이므로 인증 메서드를 지정합니다.

  • jwks_uri 값은 토큰의 유효성을 검사하는 데 사용되는 공급자의 공개 키 URI입니다. ESP가 정보를 자동으로 가져오는 Google ID 토큰과 같은 일부 인증 메서드에서는 사용자가 이를 지정할 필요가 없습니다.

  • jwt_locations는 JWT를 추출할 위치를 정의하는 데 사용됩니다.

동일한 파일에 여러 보안 공급자를 정의할 수 있지만 각 보안 공급자의 issuer가 달라야 합니다. 자세한 내용은 AuthProvider를 참조하세요.

AuthenticationRule에 설명된 대로 rules을 사용하여 이러한 인증 요구사항을 사용하려는 API 메서드를 지정합니다.

다음 예에서는 일부 지원되는 인증 메서드에 대해 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 클라이언트는 키가 authorization이고 값이 Bearer <JWT_TOKEN>인 메서드 호출을 통해 인증 토큰을 메타데이터로 전달해야 합니다. Python, Node.js 또는 자바에서 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}')

자바

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에서 백엔드 주소를 지정할 때 ESP는 원래 Authorization 헤더를 재정의합니다.

ESP는 X-Endpoint-API-UserInfo의 인증 결과를 백엔드 API에 전송합니다. 원래 Authorization 헤더 대신 이 헤더를 사용하는 것이 좋습니다. 이 헤더는 base64url이 JSON 객체를 인코딩하는 문자열입니다. JSON 객체 형식은 ESPv2와 ESP 간에 다릅니다. ESPv2의 경우 JSON 객체는 정확히 원래 JWT 페이로드입니다. ESP의 경우 JSON 객체는 서로 다른 필드 이름을 사용하고 원래 JWT 페이로드를 claims 필드에 넣습니다. 형식에 대한 자세한 내용은 백엔드 서비스에서 JWT 처리를 참조하세요.

다음 단계