אימות משתמשים

אימות מאפשר ל-Extensible Service Proxy‏ (ESP) לזהות את המשתמשים שקוראים לשיטות של השירות, ואז, על סמך זה, להחליט אם לאפשר להם להשתמש בשיטה הזו (הרשאה). בדף הזה מוסבר איך האימות פועל עם Cloud Endpoints בשירותי gRPC, כולל איך להגדיר את ESP בשירות gRPC כדי לתמוך בבקשות מאומתות, ואיך לקרוא לשיטות מאומתות מלקוח gRPC.

ב-ESP יש תמיכה בכמה שיטות אימות, כולל Firebase,‏ Auth0 ואסימונים מזהים של Google. את כל השיטות האלה אפשר להגדיר כחלק מההגדרה של gRPC API. בכל מקרה, הלקוח צריך לספק אסימון אינטרנט מסוג JSON‏ (JWT) שמזהה אותו בבקשות שלו. ה-ESP מאמת את הטוקן בשם ה-API שלכם, כך שלא צריך להוסיף קוד אימות מיוחד בעצמכם.

גם דרישות האימות וגם דרישות מפתח ה-API מאפשרות להגביל את הגישה לשיטות של השירות, אבל הן לא מספקות את אותה רמת אבטחה, והן מספקות מידע שונה לשירות שאליו מתבצעת הקריאה. במאמר מתי ולמה כדאי להשתמש במפתחות API מוסבר על ההבדלים בין מפתחות API לבין אימות, ומתי כדאי להשתמש בכל אחת מהשיטות.

דוגמה מלאה ופעילה לשימוש באימות זמינה במאמר אימות באמצעות חשבון שירות, שבו מוסבר איך להוסיף אימות לשירות Bookstore מתוך המדריכים שלנו.

הגדרת אימות לספק ESP

מגדירים אימות לשירות Endpoints for gRPC בקובץ ה-YAML של הגדרת שירות gRPC באמצעות הקטע authentication. מציינים את שיטת האימות ואת פרטי מקור האימות בתור providers, כאשר:

  • הערך id משמש לזיהוי ספק האימות כשמשתמשים בו ב-rules: בדרך כלל משתמשים בשם של שיטת האימות, אבל לא חייבים.

  • הערך של issuer הוא המנפיק של הטוקנים הנדרשים, ולכן הוא מציין את שיטת האימות.

  • הערך של jwks_uri הוא ה-URI של המפתח הציבורי של הספק, שמשמש לאימות אסימונים. בשיטות אימות מסוימות לא צריך לציין את זה, למשל באסימוני Google ID שבהם ספק ה-ESP מקבל את המידע באופן אוטומטי.

  • התג jwt_locations משמש להגדרת המיקומים שמהם יחולץ ה-JWT.

אפשר להגדיר כמה ספקי אבטחה באותו קובץ, אבל לכל אחד מהם צריך להיות issuer שונה. מידע נוסף זמין בכתובת AuthProvider.

כדי לציין את שיטות ה-API שבהן רוצים להשתמש בדרישות האימות האלה, משתמשים ב-rules, כמו שמתואר במאמר AuthenticationRule.

בדוגמאות הבאות אפשר לראות איך מגדירים את ESP בשירות gRPC לכמה שיטות אימות נתמכות:

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

כדי לתמוך באימות באמצעות אסימון מזהה של Google:

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. לכל שאר שיטות האימות, המאפיין הזה הוא אופציונלי. אם לא מציינים את השם, ESP מקבל את כל ה-JWT עם שם שירות ה-backend בצורה של https://SERVICE_NAME בטענת aud. כדי לאפשר למזהי לקוחות נוספים לגשת לשירות העורפי, אפשר לציין את מזהי הלקוחות המורשים בשדה audiences באמצעות ערכים מופרדים בפסיקים. לאחר מכן, ה-ESP מקבל את אסימוני ה-JWT עם מספרי הלקוח שברשימת ההיתרים בטענת aud.

קריאה לשיטה מאומתת מ-gRPC

אם שיטה מסוימת דורשת אימות, לקוחות gRPC צריכים להעביר את אסימון האימות כמטא-נתונים עם הפעלת method, כאשר המפתח הוא authorization והערך הוא Bearer <JWT_TOKEN>. דוגמה לאופן שבו עושים זאת כשקוראים לדוגמה של חנות ספרים ב-Python, ב-Node.js או ב-Java:

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 מעבירים את כל הכותרות שהם מקבלים. עם זאת, הוא מחליף את הכותרת המקורית Authorization כשכתובת ה-Backend מצוינת על ידי x-google-backend במפרט OpenAPI או על ידי BackendRule בהגדרת שירות gRPC.

ספק ה-ESP ישלח את תוצאת האימות ב-X-Endpoint-API-UserInfo ל-API של השרת העורפי. מומלץ להשתמש בכותרת הזו במקום בכותרת המקורית Authorization. הכותרת הזו היא מחרוזת שbase64urlמקודדת אובייקט JSON. פורמט אובייקט ה-JSON שונה בין ESPv2 לבין ESP. ב-ESPv2, אובייקט ה-JSON הוא בדיוק המטען הייעודי (payload) המקורי של ה-JWT. ב-ESP, אובייקט ה-JSON משתמש בשמות שדות שונים ומציב את מטען ה-JWT המקורי בשדה claims. מידע נוסף על הפורמט זמין במאמר בנושא טיפול ב-JWT בשירות לקצה העורפי.

המאמרים הבאים