通过工作负载身份联合使用令牌进行身份验证

本文档介绍如何通过工作负载身份联合使用令牌向 Google Cloud 进行身份验证。借助工作负载身份联合,您可以授予本地或多云 SAP 工作负载对 Google Cloud 资源的访问权限,而无需使用服务账号密钥。您可以将工作负载身份联合与 Amazon Web Services (AWS) 或任何支持 OpenID Connect (OIDC) 的身份提供方 (IdP)(例如 Microsoft Azure 或 SAML 2.0)搭配使用。

工作负载身份联合遵循 OAuth 2.0 令牌交换规范。您可以将 IdP 的凭据提供给 Security Token Service,该服务会验证凭据的身份,然后返回联合令牌进行交换。 您可以使用此令牌模拟服务账号并获取短期有效的 OAuth 2.0 访问令牌。短期有效的访问令牌可让您调用服务账号有权访问的任何 Google Cloud API。

对于通过工作负载身份联合使用令牌进行身份验证,简要配置步骤如下:

  1. 准备外部 IdP
  2. 在 Google Cloud 中,配置工作负载身份联合
  3. 在 Google Cloud 中,创建服务账号
  4. 在 Google Cloud 中,允许外部工作负载模拟服务账号
  5. 在 ABAP SDK for Google Cloud 中,实现 ABAP 代码以从 IdP 检索安全令牌
  6. 在 ABAP SDK for Google Cloud 中,配置客户端密钥

并非所有 Google Cloud 产品都支持工作负载身份联合。在使用工作负载身份联合设置身份验证之前,请查看支持的产品和限制列表。如需了解详情,请参阅员工身份联合:支持的产品和限制

准备外部 IdP

您需要准备 IdP,以便 SAP 工作负载获取可交换为 Google OAuth 2.0 安全令牌的凭据。

如需准备外部 IdP,请根据您的 IdP 执行步骤:

配置工作负载身份联合

在 Google Cloud 中,配置工作负载身份池和提供方。

您可以配置身份池,即可让您管理外部身份的实体。您还可以配置工作负载身份池提供方,即描述 Google Cloud 和 IdP 之间关系的实体。

如需配置工作负载身份联合,请根据您的外部 IdP 执行步骤:

请记下以下各项:

  • 项目编号:您在其中创建了工作负载身份池的 Google Cloud 项目的编号。
  • 池 ID:用于标识工作负载身份池的唯一 ID。
  • 提供方 ID:用于标识工作负载身份池提供方的 ID。

ABAP SDK 客户端密钥配置需要使用这些项。

创建服务账号

在 Google Cloud 控制台中,创建一个专用 IAM 服务账号以访问 Google Cloud API。此服务账号必须是包含您计划通过 SDK 使用的 Google Cloud API 的 Google Cloud 项目中的主账号。

  1. 在 Google Cloud 控制台中,启用 IAM Service Account Credentials APISecurity Token Service API,以及您计划使用 SDK 访问的任何其他受支持的 API

    转到 API 库

    如需了解如何启用 Google Cloud API,请参阅启用 API

  2. 创建一个代表工作负载的服务账号

  3. 为该服务账号授予访问 API 功能所需的 IAM 角色。。如需了解 Google Cloud API 的角色要求,请参阅各个 API 文档并遵循最小权限原则。如需详细了解 API 特定预定义角色,请参阅查找适用于 Google Cloud API 的 IAM 角色

允许外部工作负载模拟服务账号

如需允许外部工作负载模拟服务账号,请根据您的外部 IdP 执行步骤:

实现 ABAP 代码以从 IdP 检索安全令牌

ABAP SDK for Google Cloud 提供了一个抽象类 /GOOG/CL_AUTH_WIF_BASE,它具有从 Security Token Service 检索 OAuth 2.0 安全令牌以及从 IAM Service Account Credentials API 检索 OAuth 2.0 访问令牌的逻辑。 作为开发者,您需要在命名空间中创建一个子类,该子类从抽象类 /GOOG/CL_AUTH_WIF_BASE 继承。

为了使用工作负载身份联合从 ABAP SDK for Google Cloud 调用 Cloud Run functions,SDK 提供了另一个抽象类 /GOOG/CL_AUTH_WIF_ID_TOKEN。如果您使用工作负载身份联合设置身份验证,则需要在命名空间中再创建一个子类,该子类从抽象类 /GOOG/CL_AUTH_WIF_ID_TOKEN 继承。在配置客户端密钥以调用 Cloud Run functions 时,您可以在授权类字段中指定此子类。

请务必在子类中实现 GET_EXT_IDP_TOKEN 方法,并编写从 IdP 获取安全令牌的逻辑。填充以下字段:

  • CV_TOKEN:从 IdP 检索到的 string 格式的令牌。
  • CV_TOKEN_TYPE:从 IdP 检索到的安全令牌的类型。支持的令牌类型包括:
    • urn:ietf:params:oauth:token-type:jwt
    • urn:ietf:params:oauth:token-type:id_token
    • urn:ietf:params:aws:token-type:aws4_request
    • urn:ietf:params:oauth:token-type:access_token
    • urn:ietf:params:oauth:token-type:saml2

然后,抽象类 /GOOG/CL_AUTH_WIF_BASE 的方法会使用 CV_TOKENCV_TOKEN_TYPE 中填充的值来交换和检索在 API 调用中使用的最终 OAuth 2.0 令牌。

以下示例展示了其他云服务提供商(例如 AWS 和 Azure)的方法 GET_EXT_IDP_TOKEN 的实现示例。

AWS

class ZCL_AUTH_WIF_AWS definition
  public
  inheriting from /GOOG/CL_AUTH_WIF_BASE
  final
  create public .

public section.

  types:
    BEGIN OF t_header_field,
      key type string,
      value TYPE string,
    END OF t_header_field .
  types:
    tt_header_field type STANDARD TABLE OF t_header_field WITH DEFAULT KEY .
  types:
    BEGIN OF t_token_request,
     url type string,
     method type string,
     headers type tt_header_field,
   END OF t_token_request .
protected section.

  methods GET_EXT_IDP_TOKEN
    redefinition .
private section.
ENDCLASS.



CLASS ZCL_AUTH_WIF_AWS IMPLEMENTATION.


METHOD get_ext_idp_token.
**********************************************************************
*  Copyright 2024 Google LLC                                         *
*                                                                    *
*  Licensed under the Apache License, Version 2.0 (the "License");   *
*  you may not use this file except in compliance with the License.  *
*  You may obtain a copy of the License at                           *
*      https://www.apache.org/licenses/LICENSE-2.0                   *
*  Unless required by applicable law or agreed to in writing,        *
*  software distributed under the License is distributed on an       *
*  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,      *
*  either express or implied.                                        *
*  See the License for the specific language governing permissions   *
*  and limitations under the License.                                *
**********************************************************************

  DATA: ls_key       TYPE /goog/client_key.

  /goog/cl_utility=>get_client_key( EXPORTING iv_keyname    = iv_keyname
                                        IMPORTING es_client_key = ls_key ).


  DATA: lv_awsdate TYPE string.

  DATA: lv_date         TYPE dats,
        lv_time         TYPE tims,
        lv_timestamp    TYPE timestampl,
        lv_tz_utc       TYPE timezone VALUE 'UTC',
        lv_awsts        TYPE string,
        lv_timechar(32) TYPE c.

  GET TIME STAMP FIELD lv_timestamp.

  CONVERT TIME STAMP lv_timestamp TIME ZONE lv_tz_utc INTO DATE lv_date TIME lv_time.
  MOVE lv_timestamp TO lv_timechar.
  CONDENSE lv_timechar.

  lv_awsdate = lv_date(4) &&
               lv_date+4(2) &&
               lv_date+6(2) &&
               'T' &&
               lv_time(2) &&
               lv_time+2(2) &&
               lv_time+4(2) &&
               'Z'.

  TRANSLATE lv_awsdate TO UPPER CASE.

  DATA: lv_lf TYPE string.
  DATA: lv_secret_key TYPE string.
  DATA: lv_accesskey TYPE string.
  DATA: lv_datepart TYPE string.
  DATA: lv_service TYPE string.
  DATA: lv_method TYPE string.

  lv_lf = cl_abap_char_utilities=>newline.
  lv_accesskey = '<Populate AWS Access Key>'.
  lv_secret_key = '<Populate AWS Secret Access Key>'.
  lv_datepart = lv_awsdate(8).
  lv_service = 'sts'.
  lv_method = 'GET'.


  DATA: lv_canonical_query_params TYPE string.
  DATA: lv_host TYPE string.
  DATA: lv_region TYPE string.
  DATA: lv_canonical_resource_path TYPE string.

  lv_canonical_query_params = 'Action=GetCallerIdentity&Version=2011-06-15'.
  lv_host = 'sts.amazonaws.com'.
  lv_region = '<Populate your AWS Region>'.   "Example: 'us-east-1'
  lv_canonical_resource_path = '/'.

  DATA: lv_canonical_header_names TYPE string.
  DATA: lv_canonical_headers TYPE string.

  lv_canonical_header_names = 'host;x-amz-date'.
  lv_canonical_headers = 'host:' && lv_host && lv_lf && 'x-amz-date:' && lv_awsdate && lv_lf.

  DATA: lv_canonical_request TYPE string.

  CONCATENATE lv_method lv_lf
              lv_canonical_resource_path lv_lf
              lv_canonical_query_params lv_lf
              lv_canonical_headers lv_lf
              lv_canonical_header_names
              INTO lv_canonical_request.

  DATA: lv_canonical_request_hash TYPE string.

  TRY.
      cl_abap_message_digest=>calculate_hash_for_char(
       EXPORTING
         if_algorithm = 'SHA-256'
         if_data = lv_canonical_request
       IMPORTING
         ef_hashstring = lv_canonical_request_hash ).
    CATCH cx_abap_message_digest.
      "Handle error
      RETURN.
  ENDTRY.

  TRANSLATE lv_canonical_request_hash TO LOWER CASE.

  DATA: lv_algorithm TYPE string.

  lv_algorithm = 'AWS4-HMAC-SHA256'.

  DATA: lv_credential_scope TYPE string.

  CONCATENATE lv_datepart '/' lv_region '/' lv_service '/' 'aws4_request' INTO lv_credential_scope.

  DATA: lv_string_to_sign TYPE string.

  CONCATENATE lv_algorithm lv_lf
              lv_awsdate lv_lf
              lv_credential_scope lv_lf
              lv_canonical_request_hash
              INTO lv_string_to_sign.

  DATA: lv_awskey TYPE string.

  CONCATENATE 'AWS4' lv_secret_key INTO lv_awskey.

  DATA: lv_ksecret TYPE xstring.

  TRY.
      lv_ksecret = cl_abap_hmac=>string_to_xstring( lv_awskey ).
    CATCH cx_abap_message_digest .
      "Handle error
      RETURN.
  ENDTRY.

  DATA: lv_kdate  TYPE xstring.
  TRY.
      cl_abap_hmac=>calculate_hmac_for_char(
        EXPORTING
           if_algorithm = 'SHA256'
           if_key = lv_ksecret
           if_data = lv_datepart
        IMPORTING
           ef_hmacxstring = lv_kdate ).
    CATCH cx_abap_message_digest. "
      "Handle error
      RETURN.
  ENDTRY.

  DATA: lv_kregion TYPE xstring.
  TRY.
      cl_abap_hmac=>calculate_hmac_for_char(
        EXPORTING
           if_algorithm = 'SHA256'
           if_key = lv_kdate
           if_data = lv_region
        IMPORTING
             ef_hmacxstring = lv_kregion ).
    CATCH cx_abap_message_digest.
      "Handle error
      RETURN.
  ENDTRY.

  DATA: lv_kservice TYPE xstring.
  TRY.
      cl_abap_hmac=>calculate_hmac_for_char(
         EXPORTING
           if_algorithm = 'SHA256'
           if_key = lv_kregion
           if_data = lv_service
           IMPORTING
             ef_hmacxstring = lv_kservice ).
    CATCH cx_abap_message_digest.
      "Handle error
      RETURN.
  ENDTRY.

  DATA: lv_ksigningkey TYPE xstring.
  TRY.
      cl_abap_hmac=>calculate_hmac_for_char(
         EXPORTING
           if_algorithm = 'SHA256'
           if_key = lv_kservice
           if_data = 'aws4_request'
         IMPORTING
             ef_hmacxstring = lv_ksigningkey ).
    CATCH cx_abap_message_digest.
      "Handle error
      RETURN.
  ENDTRY.

  DATA: lv_stringtosign TYPE string.

  lv_stringtosign = 'AWS4-HMAC-SHA256' && lv_lf &&
                   lv_awsdate && lv_lf &&
                   lv_datepart && '/' &&
                   lv_region && '/' &&
                   lv_service && '/aws4_request' && lv_lf &&
                   lv_canonical_request_hash.

  DATA: lv_ssignature TYPE string.

  TRY.
      cl_abap_hmac=>calculate_hmac_for_char(
         EXPORTING
           if_algorithm = 'SHA256'
           if_key = lv_ksigningkey
           if_data = lv_stringtosign
         IMPORTING
           ef_hmacstring = lv_ssignature ).
    CATCH cx_abap_message_digest.
      "Handle error
      RETURN.
  ENDTRY.

  TRANSLATE lv_ssignature TO LOWER CASE.

  DATA: lv_authorization_header TYPE string.

  lv_authorization_header = 'AWS4-HMAC-SHA256 Credential=' &&
                            lv_accesskey && '/' &&
                            lv_credential_scope &&
                            ', SignedHeaders=' &&
                            lv_canonical_header_names &&
                            ', Signature=' &&
                            lv_ssignature.

  DATA: ls_token_request TYPE t_token_request.

  ls_token_request-url = 'https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15'.
  ls_token_request-method = 'POST'.

  DATA: ls_header_field TYPE t_header_field.
  ls_header_field-key = 'Authorization'.
  ls_header_field-value = lv_authorization_header.
  APPEND ls_header_field TO ls_token_request-headers.

  CLEAR: ls_header_field.
  ls_header_field-key = 'host'.
  ls_header_field-value = 'sts.amazonaws.com'.
  APPEND ls_header_field TO ls_token_request-headers.

  CLEAR: ls_header_field.
  ls_header_field-key = 'x-amz-date'.
  ls_header_field-value = lv_awsdate.
  APPEND ls_header_field TO ls_token_request-headers.

  CLEAR: ls_header_field.
  ls_header_field-key = 'x-goog-cloud-target-resource'.
  ls_header_field-value = '//iam.googleapis.com/projects/' &&
                               ls_key-project_id &&
                               '/locations/global/workloadIdentityPools/' &&
                               ls_key-auth_param1 &&
                               '/providers/' &&
                               ls_key-auth_param2.
  APPEND ls_header_field TO ls_token_request-headers.

  cv_token = /ui2/cl_json=>serialize(  ls_token_request ).
  cv_token_type = 'urn:ietf:params:aws:token-type:aws4_request'.

ENDMETHOD.
ENDCLASS.

Azure

class ZCL_AUTH_WIF_AZURE definition
  public
  inheriting from /GOOG/CL_AUTH_WIF_BASE
  final
  create public .

public section.
protected section.

  methods GET_EXT_IDP_TOKEN
    redefinition .
private section.
ENDCLASS.



CLASS ZCL_AUTH_WIF_AZURE IMPLEMENTATION.


  METHOD GET_EXT_IDP_TOKEN.
**********************************************************************
*  Copyright 2024 Google LLC                                         *
*                                                                    *
*  Licensed under the Apache License, Version 2.0 (the "License");   *
*  you may not use this file except in compliance with the License.  *
*  You may obtain a copy of the License at                           *
*      https://www.apache.org/licenses/LICENSE-2.0                   *
*  Unless required by applicable law or agreed to in writing,        *
*  software distributed under the License is distributed on an       *
*  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,      *
*  either express or implied.                                        *
*  See the License for the specific language governing permissions   *
*  and limitations under the License.                                *
**********************************************************************

    TYPES:
      BEGIN OF t_azure_resp,
        access_token TYPE string,
      END OF t_azure_resp.

    DATA: lo_client TYPE REF TO if_http_client.

    DATA: lv_url type string.
    lv_url = 'http://169.254.169.254/metadata/identity/oauth2/token?resource=<APP_ID_URI>&api-version=2018-02-01'.
    "Replace <APP_ID_URI> with the value of Application ID URI of the application that you've configured for workload identity federation.

    cl_http_client=>create_by_url(
       EXPORTING
         url                        = lv_url
       IMPORTING
         client                     = lo_client
       EXCEPTIONS
         argument_not_found         = 1
         plugin_not_active          = 2
         internal_error             = 3
         pse_not_found              = 4
         pse_not_distrib            = 5
         pse_errors                 = 6
         oa2c_set_token_error       = 7
         oa2c_missing_authorization = 8
         oa2c_invalid_config        = 9
         oa2c_invalid_parameters    = 10
         oa2c_invalid_scope         = 11
         oa2c_invalid_grant         = 12
         OTHERS                     = 13 ).

    IF sy-subrc <> 0.
      RETURN.
    ENDIF.

    lo_client->request->set_method( 'GET' ).
    lo_client->request->set_header_field( name = 'Metadata' value = 'true' ).

    lo_client->send(
      EXCEPTIONS
        http_communication_failure = 1
        http_invalid_state         = 2
        http_processing_failed     = 3
        http_invalid_timeout       = 4
        OTHERS                     = 5 ).

    lo_client->propertytype_logon_popup = lo_client->co_disabled.

    lo_client->receive(
      EXCEPTIONS
      http_communication_failure = 1
      http_invalid_state         = 2
      http_processing_failed     = 3 ).

    DATA: lv_json TYPE string.

    lv_json = lo_client->response->get_cdata( ).

    DATA: ls_azure_resp TYPE t_azure_resp.

    /goog/cl_json=>deserialize(
      EXPORTING
        json             = lv_json
      CHANGING
        data             = ls_azure_resp ).
    cv_token = ls_azure_resp-access_token.
    cv_token_type = 'urn:ietf:params:oauth:token-type:jwt'.

  ENDMETHOD.
ENDCLASS.

配置客户端密钥

  1. 在 SAP GUI 中,执行事务代码 /GOOG/SDK_IMG

    或者,执行事务代码 SPRO,然后点击 SAP Reference IMG

  2. 点击 ABAP SDK for Google Cloud > Basic Settings > Configure Client Key
  3. 点击新建条目
  4. 输入以下字段的值:

    字段 说明
    Google Cloud 密钥名称 指定客户端密钥配置的名称。
    Google Cloud 服务账号名称 指定在创建服务账号步骤中为了访问 Google Cloud API 而创建的服务账号的名称(采用电子邮件地址格式)。 例如:sap-example-svc-acct@example-project-123456.iam.gserviceaccount.com
    Google Cloud 范围 将此字段留空。
    Google Cloud 项目标识符 指定您在其中创建了工作负载身份池的 Google Cloud 项目的 ID。
    命令名称 将此字段留空。
    授权类 指定包含类 /GOOG/CL_AUTH_WIF_BASE 的实现的子类。如需了解详情,请参阅实现 ABAP 代码以从 IdP 检索安全令牌
    令牌缓存 将此字段留空。
    令牌刷新秒数 将此字段留空。
    授权参数 1 指定工作负载身份池 ID。
    授权参数 2 指定工作负载身份提供方 ID。
  5. 保存该条目。

获取支持

如果您在解决 ABAP SDK for Google Cloud 问题时需要帮助,请执行以下操作: