SAP BTP:如何使用SAML和OAuth跨区域调用受保护的应用程序[2]。教程

204 阅读9分钟

在本教程中,我们将创建2个小应用程序,并将其部署到2个不同的试用账户。
我们配置信任(基于SAML),并创建一个OAuth2SAMLBearerAssertion类型的目标配置。
本教程基于之前博文中的解释。

快速链接:
快速指南
示例代码

内容

第一部分:理论-折磨
0.1.介绍
0.2.先决条件
0.3.准备工作

第二部分:实践-聚会(本博客)
1.创建后端应用程序
2.创建前端应用程序
3.配置信任
3.1.前台子账户
3.2.后台子账户
4.创建目的地
5.运行方案
6.清理
附录1:示例代码后端应用程序
附录2。示例代码 前端应用程序
附录3:示例代码 目的地配置

1.创建后端应用程序

我们的后端应用程序提供了一个REST端点,可以被其他应用程序调用。
但是,它受到OAuth的保护,所以它需要一个有效的JWT令牌与请求一起发送。

1.0.准备工作

我们登录到代表后台的试用账户。
在我的例子中,我创建了一个名为Backend_Subaccount的子账户。
我们打开子账户的概览页面,查看并复制API端点。
我们需要它将我们的CLI指向这个CF api并使用CLI登录。
然后我们可以切换到命令提示符并运行登录命令,后面是api端点和(可选)org名称。
在我的例子中。

cf login -a api.cf.ap21.hana.ondemand.com -o backendorg

1.1.创建XSUAA服务实例

我们的后端应用程序端点是受OAuth保护的。因此,我们需要创建一个XSUAA服务实例
我们的服务实例的配置可以在文件backend-security.json中找到。
它不包含任何有趣的东西,所以我们就继续。
在命令行中,我们导航到文件夹c:\crossapp\backend并执行以下命令:
cf cs xsuaa application backendXsuaa -c backend-security.json

1.2.创建后端应用程序

我们的后端应用是一个小的node.js服务器应用,尽可能的简约,基于Express
为了满足我们教程的需要,它只暴露了一个REST端点,由我们的外部前端应用调用。
REST端点是用OAuth保护的。
保护由passport中间件处理,它用xssec验证传入的JWT令牌。
我们的实现抓取JWT令牌,读取它并将一些用户信息写入日志。
在最后,它将JWT令牌作为响应返回。

app.get('/endpoint', passport.authenticate('JWT', {session: false}), (req, res) => {
    const auth = req.authInfo  
    console.log(`===> [backendapp] called by user '${auth.getGivenName()}' from subdomain '${auth.getSubdomain()}' with oauth client: '${auth.getClientId()}'`)
    res.json({
        'message': 'BACKEND successfully called',
        'jwtToken': auth.getAppToken()})
})

完整的示例代码可以在附录1中找到。

1.3.部署

部署后,我们可以打开我们的REST终端的URL,以确保它是正确的。
在我的例子中:
backend.cfapps.ap21.hana.ondemand.com/endpoint
结果我们得到一个错误,因为我们没有发送JWT令牌--现在可以了。

2.创建前台应用程序

现在我们的后台应用程序已经在后台试用账户中运行了,我们把它告诉我们的朋友.....,事实上,有一个朋友(一定是一个非常好的朋友)想从他的前端应用程序中调用我们的愚蠢的应用程序。
不幸的是,他有自己的试用账户,所以我们必须找到一种方法,使他能够调用我们的终端。

2.0.准备工作

目前,我们切换身份:
现在我们登录用于前端的试用账户。
该试用账户托管在不同的地区,在我的例子中是在美东(VA)。
同样,我们需要找出 Cloud Foundry 的 API 端点,我们需要配置我们的 Cloud Foundry 命令行客户端。
我们在子账户的概览屏幕中找到 API 端点。
在命令行中,我们运行修改后的登录命令。
在我的例子中。

cf login -a api.cf.us10.hana.ondemand.com -o frontendorg

2.1.创建XSUAA服务实例

在我们的教程中,我们并不真正需要保护我们的前端应用程序。
然而,我们需要一个用户登录来获得一个用户-JWT-token。
而这样一个用户-token必须包含一定的范围,这是目标服务所要求的(我们将在后面解释)。
因此,我们为我们的前端应用程序创建一个安全配置,如下所示。

{
    "xsappname": "frontendxsuaa",
    "tenant-mode": "dedicated",
    "role-templates": [{
        "name": "uaaUserDefaultRole",
        "description": "Default role uaa.user required for user centric scenarios",
        "scope-references": ["uaa.user"]
    }],
    "role-collections": [{
          "name": "Frontend_Roles",
          "role-template-references": [ "$XSAPPNAME.uaaUserDefaultRole" ]
        }
    ]    
}

我们可以看到,我们将uaa.user范围添加到一个角色模板中。
这个角色稍后将分配给我们的最终用户。
这个uaa.user范围在 Cloud Foundry 中是默认可用的,所以我们不需要在这里定义一个范围。请参见 Cloiud Foundry文档
此外,我们还要定义一个角色集合。

这通常是由管理员在驾驶舱中完成的步骤:创建角色集合并添加角色和分配用户。
不过,这个预定义的角色集合包含我们的角色,这将使我们在学习本教程时更加轻松。

为了创建XSUAA实例,我们切换到命令行,进入C:\crossapp\frontend文件夹并执行以下命令:
cf cs xsuaa application frontendXsuaa -c frontend-security.json

2.2.创建目标服务实例

在本教程中,目的地是一个重要的部分,因为它帮助我们从SAML承载断言中获得JWT令牌。
为了从我们的代码中访问目的地配置,我们需要一个目的地服务的实例。
我们创建该实例,不需要配置文件,只需运行以下命令(从任何文件夹)。

cf cs destination lite frontendDestination

2.3.创建核心应用程序

我们的前端应用程序是一个以用户为中心的应用程序。
为什么它需要以用户为中心?为什么我们不能在没有用户的情况下进行应用程序与应用程序之间的通信?
原因:
SAML在没有用户的情况下是没有意义的,因为它被设计为在系统之间传输用户登录信息。
因此,我们的前端应用程序由两个组件组成:
核心组件是一个提供REST端点的服务器应用程序。
approuter,负责处理用户登录并转发到该端点。

像往常一样,我们的应用程序尽可能的简单--然而,有几个步骤是需要的。

1.掌握用户的JWT-token
2.进行令牌交换
3.调用目标服务
4.调用后端应用程序

代码演练

2.3.1.用户-JWT-令牌

什么是用户-JWT-令牌?
我们的最终用户将通过一个指向approuter的URL访问我们的应用程序。
Approuter(与XSUAA服务器一起)将处理登录。
如果登录成功,XSUAA服务器将发出一个JWT令牌。
该令牌将由approuter转发到核心应用程序的受保护端点,该端点将验证并接受该令牌。
该令牌包含关于登录用户的信息,例如电子邮件和作用域。
这就是为什么我称之为用户-JWT-令牌。

该令牌在授权头中发送,但有一个方便的方法来访问它:
我们可以使用方便的对象authInfo来访问它。

app.get('/endpoint', passport.authenticate('JWT', {session: false}), async (req, res) => {
const userJwtToken = req.authInfo.getAppToken()

2.3.2.进行令牌交换

在第三步中,我们想调用目标服务,因为它可以帮助我们为后端应用获取一个令牌。
然而,目标服务是受OAuth保护的,所以我们需要获取一个JWT令牌来调用它。
这就是我们现在要做的。

为什么要交换令牌?
目的地服务在绑定中提供其证书。
我们可以直接使用它们,但对于某些类型的目的地配置,需要一个特定的范围(uaa.user)。
这个范围(被包裹在一个角色中)已经被添加到用户中(我们在创建XSUAA实例时已经看到它),所以user-JWT-token包含这个必要的范围。
因此,我们在获取destinations service-token的同时发送该用户令牌。
这就是我们使用令牌交换的原因。
这是将uaa.user范围纳入destinations service-token的诀窍。
如果我们通过客户端证书获取destinations service-token,我们就不会在令牌中获得uaa.user范围。

要了解更多关于令牌交换的信息,我建议你看一下这个非常有用的教程

我们调用我们的辅助方法,该方法反过来使用@sap/xssec辅助方法来进行令牌交换。

xssec.requests.requestUserToken(bearerToken, DESTINATION_CREDENTIALS, null, null, null, null, (error, token)=>{

2.3.3.调用目标服务

通常情况下,调用目标服务是为了读取目标配置中的细节。
这基本上是我们想要调用的目标(后端)应用程序的URL。此外,目标服务提供了便利,为我们提供了调用该URL所需的凭证。
换句话说,目标服务能够为我们完成OAuth流程,以获取JWT令牌。
而在我们的案例中,这是至关重要的。
因为在我们的案例中,我们需要一个复杂的流程:
如前所述,我们需要跨越有高墙和宽河(有鳄鱼)保护的边界,以到达目标后端应用程序。
为了管理它,我们将在鳄鱼的两边配置信任。
这种信任是基于SAML的。
在正常的SAML场景中,最终用户在登录后得到一个SAML承载断言(身份提供者),然后被发送到目标系统(服务提供者)。
在我们的例子中,目标应用程序不接受SAML承载断言(XML),它需要一个OAuth JWT令牌(JSON)。
为了将SAML交换为OAuth,有一个特殊的OAuth流程。
这就是目标服务为我们处理的内容。
真的很感谢你,Dundee。

目的地服务是通过其REST API调用的。

const destServiceUrl = `${DESTINATION_CREDENTIALS.uri}/destination-configuration/v1/destinations/${destinationName}`
const options = {
   headers: { Authorization: 'Bearer ' + jwtToken}
}
const response = await fetch(destServiceUrl, options)
const responseJson = await response.json()

注意:
为了节省几行代码,我们使用node-fetch库进行HTTP调用。
结果是一个包含我们需要的2个信息的对象:
我们的后端应用的目标URL,我们在目标配置中输入。
调用的授权,目标服务获取的JWT令牌。
我们在下一步使用它。

2.3.4.调用后端应用程序

所以最后,我们准备调用我们的目标后端应用程序。
我们有一个JWT令牌,应该被OAuth保护的端点所接受。

async function _callBackend (destination){
   const backendUrl = destination.destinationConfiguration.URL
   const options = {
      headers: {
         Authorization : destination.authTokens[0].http_header.value  // contains the "Bearer" plus space
      }
   }
   const response = await fetch(backendUrl, options)
   const responseText = await response.text()

之后,我们对结果的处理并不值得用2.3.5的编号,因为在成功调用后端应用程序后,我们的教程实际上应该在这里结束。
不过,我们可以花几行代码在浏览器中可视化所有涉及的JWT令牌。
因此,我们打印:
用户JWT令牌。
我们在令牌交换后得到的目的地服务令牌。
目的地服务为我们获取的后台应用令牌。
后台应用的响应,即收到的令牌(应该是一样的)。

res.send(`
   <h4>JWT after user login:</h4>${userJwtTokenDecoded}
   <h4>JWT after token exchange (used to call destination service):</h4>${destJwtTokenDecoded}
   <h4>JWT issued by OAuth2SAMLBearerAssertion destination:</h4>${samlBearerJwtTokenDecoded}
   <h4>Response of BACKEND call:</h4>${responseJson.message}. The token: ${responseJwtTokenDecoded} `)

2.4.创建Approuter

关于approuter,没有什么好解释的。
,如果需要新人的信息,我可以推荐我的approuter教程

我们的approuter与核心应用程序绑定在同一个XSUAA实例上。因此,发出的JWT令牌将被核心应用程序的端点所接受。
approuter只配置了一个路由。

    "source": "^/tofrontend/(.*)$",
    "target": "$1",
    "destination": "destination_frontend",
    "authenticationType": "xsuaa"

该路由需要基于XSUAA的认证。
这正是我们想要的,因为它将导致用户登录屏幕。
该路由通过目的地将传入的调用委托给我们的核心应用。
这个目的地可以在驾驶舱中创建,但我们只是在清单中将其定义为环境变量。

    env:
      destinations: >
        [
          {
            "name":"destination_frontend",
            "url":https://frontend.cfapps.us10.hana.ondemand.com,
            "forwardAuthToken": true

请注意,使用env var并不是有效的方法。
我们这样做只是为了避免使我们的教程变得更长。

我们可以看到,目的地被配置为转发JWT令牌。
这意味着:
用户登录后,XSUAA服务器会发出JWT令牌,approuter将其保存在会话中,仅在需要时转发。
在我们的案例中,我们的终端需要它。
目的地指向我们的前端核心服务的基本URL。
在调用approuter时,必须将该服务所需的端点段附加到URL中。
它可以通过路由定义中的 "目标 "属性访问。
这使得目的地很灵活。

因此,在最后,我们将调用approuter的URL如下:
frontendrouter.cfapps.us10.hana.ondemand.com/tofrontend/…
每当approuter收到这个请求,它将读取路由+目的地并转发到
frontend.cfapps.us10.hana.ondemand.com/endpoint

该端点是受保护的,需要JWT令牌。
,但请记住:我们在目的地中指定,approuter应该转发JWT令牌。因此,我们的用户令牌(在登录时发出并由approuter存储)将被转发到端点。
一切正常。

2.5.部署

在我们部署之前,先看一下清单

applications:
  - name: frontend
    routes:
    - route: frontend.cfapps.us10.hana.ondemand.com
    services:
      - frontendXsuaa
      - frontendDestination
  - name: frontendrouter
    routes:
    - route: frontendrouter.cfapps.us10.hana.ondemand.com
    env:
      destinations: >
        [
          {
            "name":"destination_frontend",
            "url":"https://frontend.cfapps.us10.hana.ondemand.com",
            "forwardAuthToken": true
          }
        ]      
    services:
      - frontendXsuaa

正如我们所看到的,没有什么好解释的。
我们部署两个模块,这将导致两个应用程序在 Cloud Foundry 中运行。
我们的核心应用程序被绑定到两个服务实例,即 XSUAA 和目的地。
我们还可以看到,approuter 应用程序将有一个环境变量,它将是一个名称为 "目的地 "的 JSON 对象,是一个有一个条目的 JSON 数组。
如前所述,这种定义目的地的方式仅在原型设计中使用,在实际部署中会被真正的目的地取代。

部署后,我们不打开我们的应用程序,因为它还没有工作。
原因:
目标配置还不存在。
我们还没有在子账户之间建立信任。

所以让我们去做吧。

3.配置信任

在我们解释并部署了我们的2个愚蠢的应用程序之后,我们就来到了本教程的有趣部分。
ok ok ok - 我同意:这也很无聊。

在这第三章(无聊),我们要配置两个子账户之间的信任。
不记得哪个信任和哪个子账户了?
介绍中,我们盯着一张图看。

我们看到,信任被配置在两个试用账户的子账户级别上。
在细节上,我们了解到:
前端方想要调用,所以它需要显示它是值得信任的。
这是通过显示它的元数据来实现的
后端方是害怕的,不会因为它的名字好听就相信任何人。
因此,它采取并存储和验证元数据。

配置信任很容易。
以下步骤在SAP BTP Cockpit中完成。

3.1.前端子账户

我们需要做的第一件事是进入想要访问的子账户。
这是我们部署前端应用程序的子账户。
前端应用程序想要调用后台应用程序端点。因此,是前端子账户必须证明它是值得信任的。
为了证明这一点,它必须显示其SAML元数据。

下载IdP元数据

我们可以在
Subaccount->Connectivity->Destinations
找到SAML元数据,然后我们按 "Download IDP Metadata "按钮。

下载的文件可以复制到前端应用程序文件夹,并重命名为IdP_Metadata_Frontend.xml
IDP元数据文件包含例如以下的实体ID。

<ns3:EntityDescriptor
entityID="cfapps.us10.hana.ondemand.com/22cece36-3c8c-4420-bad5-60e1787230da"

这很有趣:
,我们可以看到实体ID是一个包含域和子账户ID的连接字符串。
我们可以通过看一下我们的试用账户的驾驶舱,在子账户的概览标签中来验证这个。

我们可以看到子账户ID,但它看起来很无聊......

我们还可以看到,元数据中包含一个描述身份提供者SSO机制的标签。

<ns3:IDPSSODescriptor
   WantAuthnRequestsSigned="true"
   protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
   <ns3:KeyDescriptor use="signing">
      <KeyInfo>
         <X509Data>
            <X509Certificate>
               MIIFiDCCA3CgAwIBAgIKA+0DUF8qXHmtnzANBgkqhkiG9w0BAQ0FADCBgjFLMEkG
               A1UEAwxCY2ZhcHBzLnVzMTAuaGFuYS5vbmRlbWFuZC5jb20vMjJjZWNlMzYtM2M4
               Yy00NDIwLWJhZDUtNjBlMTc4NzIzMGRhMQwwCgYDVQQKDANTQVAxJTAjBgNVBAsM
               blablabla 

3.2.后台子账户

在我的例子中,我登录到我的另一个试用账户,在那里我创建了一个名为Backend_Subaccount的子账户。
我们的后台应用程序部署在这里,所以这个子账户必须接受来自前端的来电。
因此,此子帐户必须信任前端子帐户。
如何在 Cloud Foundry 子帐户中配置信任?
要信任前端子帐户,我们需要前端 IdP 的 SAML 元数据。
我们已经在上一步中下载了它。

配置信任

我们去子账户 -> 安全 -> 信任配置
我们按 "新信任配置 "按钮

在对话框中,我们应该输入我们想要信任的身份提供者的元数据。
很好,我们是主动的,从Frontend_Subaccount下载了元数据,它被存储在我们的frontend文件夹中,称为IdP_Metadata_Frontend。xml
所以我们需要做的就是在这个对话框中上传这个文件。
此外,我们为这个信任配置输入一个名字,我的例子是:frontend_IDP
同时,我们禁用 "Available for User Logon "复选框

在按下保存后,frontend_IDP被添加到受信任的身份提供者列表中。
换句话说:我们有一个新的身份提供者(frontend),我们信任它。
或者:来自frontend_IDP的用户现在被允许了

总结

要配置信任,我们只需将IdP元数据从一个子账户复制到另一个子账户。更准确地说:
后台子账户需要一个新的信任配置,其中包含调用前端子账户的IdP元数据。

4.创建目的地

在这一点上,我们已经在2个不同地区的2个试用账户的2个子账户之间配置了信任。
更确切地说,Frontend_Subaccount被允许访问Backend_Subaccount。换句话说,部署在Frontend_Subaccount的应用程序将能够调用部署在Backend_Subaccount的应用程序。
为了调用后端应用程序,我们创建一个目的地。
我们需要这个目的地来处理必要的安全流。

我们在哪里创建目的地?
为了避免混淆,让我们试着把它说清楚。
当我们说 "目的地 "时,我们实际上是指 "目的地配置"。
在仪表板上,我们写下一些描述目标目的地终端的数据。
这样:
目的地不是在目的地子账户中创建的。
因为目标子账户已经是目标了。
好吗?
有道理吗?
这很明显,对吗?
因此,我们在前端子账户中创建目标配置,我们在那里部署调用目标的前端应用程序。
换句话说,目标是在需要它的应用程序旁边创建的。
下图使它更加清晰。

就像这样:
为了创建目的地,我们将登录到包含Frontend_Subaccount的试用账户。
但是:
现在不行。
首先我们应该收集所有需要的数据。

4.1.准备好目的地配置的数据

在我们真正能够创建目的地配置之前,我们应该准备所需的数据。
由于目的地指向后台,我们需要在那里收集一些数据。
因此,我们登录到后台的试用账户。

4.1.1.查看SAML元数据

在驾驶舱(后端账户),我们导航到安全->信任配置
在那里我们只需按下下载SAML元数据的按钮

注意:
同样的元数据文件可以从这个位置下载:
https://.authentication..hana.ondemand.com/saml/metadata
在我的例子中:
backendsubdomain.authentication.ap21.hana.ondemand.com/saml/metada…

下载后,我们在编辑器中打开文件。
应用xml格式化器或漂亮的打印机是有帮助的。
在我的例子中,它看起来如下。

我们盯着xml,一个字都不懂。
我们至少要澄清几个字。

SAML元数据的可选解释。

我们盯着的,是后端子账户的身份卡。
在SAML语言中,我们的后端子账户播放器是一个实体

* 这个实体由根xml元素描述。
记住,我们使用后端子账户来托管我们要调用的REST端点。
这个REST端点是我们的受保护资源(PR)。
在SAML方面,后端子账户扮演着服务提供者(SP)的角色。

*
我们可以通过xml-child-element来识别它,它包含真正有趣的信息。
还记得吗?在IDP元数据中,我们看到了这个元素
(好吧,不是每个人都看了它......

* 我们可以看到,SP支持SSO(显然,这就是为什么SAML被使用的原因)。
这意味着SP必须提供一个端点,由身份提供者调用。
这是必要的,因为IDP将发送用户的登录信息文件(断言)到该端点。
因此,该端点是为消费者断言提供的服务。
简介:<AssertionConsumerService>
(见屏幕截图)

* 元数据还描述了名字ID应该是什么样子。
什么?
记住,身份提供者在登录后会发布一个描述用户的小文件(断言)。
用户有用户信息,但是SAML IdP也会创建一个名字Identifier
这个可以有不同的格式,服务提供者声明他理解哪些方式。

* 元数据中包含的另一个信息是 ,
服务提供者的公钥。

* 值得注意的是:
,元素的顺序是固定的。这个信息使我们更容易找到所需的元素。

那么现在,我们需要这个神秘的xml的什么信息?
-> 三个属性

首先:
我们需要实体ID
在我的例子中:
backendsubdomain.authentication.ap21.hana.ondemand.com

第二:
我们需要NameIDFormat
值:
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress

第三:
我们需要最后一个AssertionConsumerService,用于URI绑定
在我的例子中:
backendsubdomain.authentication.ap21.hana.ondemand.com/oauth/token…

4.1.2.查看XSUAA凭证

我们现在要离开SAML世界,进入OAuth世界。
还是在Backend_Subaccount中,我们现在需要确定OAuth客户端的凭证。
在最后,这就是我们需要的东西。我们需要获取一个JWT令牌,这个令牌将被我们的OAuth保护的端点接受。
我们的后端应用程序被绑定到一个XSUAA的实例,它代表OAuth客户端,并在绑定中显示其证书。
在OAuth流程中,客户端需要证书,以便从OAuth-Authorization-Server获取JWT令牌。
因此,我们需要用户/密码,用更好的话说:客户/秘密。

为了查看绑定的证书,我们可以使用驾驶舱。
我们反正已经登录了后台的试用账户,所以我们可以直接进入我们部署的backendapp的细节界面。
我们点击 "环境变量",找到 "系统提供的 "部分。

从json文档中,我们注意到提到的2个属性和它的值

在我的例子中:
"clientid": "sb-backendxsuaa!t7722"
"clientecret": "msWms8tylSHWi4HJ7pTPhHNwaiM="

注意:
命令行用户输入:cf env backend

4.1.3.查看应用程序的URL

最后,对于目标配置,我们需要最突出的信息:目标URL。
这是我们想从前端应用中调用的REST端点。
我们已经在部署后端应用后试过了。
因为我们一直都打开了驾驶舱。我们可以在驾驶舱、后端应用的详情屏幕、概览页面中查看我们部署的应用的(基本)URL。

注意:
命令行用户输入:cf app backend

在我的例子中:
backend.cfapps.ap21.hana.ondemand.com/endpoint

4.1.4.查看文档

在这个页面中,我发现以下信息:
authnContextClassRef:
"urn:oasis:names:tc:SAML:2.0:ac:class:PreviousSession"

4.2.创建目的地配置

现在我们已经从后端世界收集了所有需要的信息,我们可以切换回前端世界。
因此,我们从后端试用账户注销,并登录到前端试用账户。

为什么是前台子账户?
记住。
前台应用被绑定到目标服务实例,因为它有助于调用目标URL。因此,目标配置需要在前端子账户中创建。
我们导航到Frontend_Subaccount -> Connectivity -> Destinations -> New Destination

现在我们需要填写相当多的字段。为了更好地理解,
,有一些基本数据(如名称),
,有SAML数据,
,还有XSUAA数据。

让我们一起创建目的地配置。

名称
我们输入任何我们选择的名称。
但是,我们需要确保在前端应用的代码中使用完全相同的名称。
在我的例子中:"destination_to_backend"

类型
目的地的类型是 "HTTP"。

描述
我们选择的任何描述,例如:
"目的地指向后端账户中的后端应用端点"

URL
我们的后端应用的目标端点的URL,如2.1章中所收集的。
在我的例子中:
backend.cfapps.ap21.hana.ondemand.com/endpoint

代理类型
这里我们选择 "互联网"
(因为我们不是通过云连接器调用onPrem系统)。

认证
这里我们选择 "OAuth2SAMLBearerAssertion "
(它打开了大约上百个字段,声称需要填写

密钥存储位置
我们忽略这个字段

Key Store Password
我们也忽略这个字段(不错)

Audience
这是认证机制的预期接收者。
在我们的例子中,它是SAML服务提供商,由其实体ID识别
(实体ID是目标子账户的SAML元数据的一个属性)
我们收集上面的值。
在我的例子中:
backendsubdomain.authentication.ap21.hana.ondemand.com

AuthnContextClassRef
这里我们输入上面收集的值:
"urn:oasis:names:tc:SAML:2.0:ac:class:PreviousSession"

客户端密钥
这里我们输入上面收集到的属性clientid的值
在我的例子中:
"sb-backendxsuaa! t7722"

Token Service URL Type
这里我们选择 "Dedicated",无论如何这是默认值。
(对于多租户应用,必须选择 "Common "值,其中URL为每个租户修改)。

Token Service URL
这里我们使用准备工作中收集的SAML元数据。
URL是从AssertionConsumerService元素的 "Location "属性中复制的
在我的例子中:
backendsubdomain.authentication.ap21.hana.ondemand.com/oauth/token…

Token Service User
还记得我们在讨论用于获取JWT令牌的user和pwd吗?
它们可以在XSUAA实例的证书中找到,与后端应用程序绑定。
它是上面收集的clientid,与我们已经用于 "Client Key "字段的相同
在我的例子中:
sb-backendxsuaa! t7722

令牌服务密码
最后,要填写的最后一个字段是在获取JWT令牌时使用的密码。
它是我们上面收集的属性clientecret的值。
在我的例子中:
msWms8tylSHWi4HJ7pTPhHNwaiM=

额外的属性
我们在填充字段方面很有乐趣,所以我们按 "新属性 "来增加一个字段(其实是最后一个)。

新属性名称:
我们输入 "nameIdFormat "作为新的属性名称。

新属性值
这里我们输入上面收集的nameIDFormat:
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress

在填写完所有字段后,我们不应该忘记按 "保存"。
此外,我们可以导出目标配置,这样我们就可以在下一次重复使用它。

5.运行方案

在完成了两个必要的配置后,我们(仍然)不能调用我们的应用程序。
我们首先需要把我们在xs-security文件中定义的角色分配给我们的用户。

5.1.指派角色

记住:在我们的前端XSUAA实例中,我们已经定义了一个角色模板。
,这是必须的,因为oa.user范围必须包含在user-JWT-token中。
然而,只有当相应的角色被分配给用户时,这个范围才会被添加到token中。
在我们的安全配置文件中,我们已经定义了一个角色集合,它已经配置了角色。
所以现在我们只需要找到那个角色集合,并将我们的用户添加到其中。

所以现在我们需要接管管理员的任务:用角色集合配置我们的用户。
我们确保登录到我们的前端试用账户。
我们导航到frontend_subaccount -> Security -> Role Collections。
找到并选择名称为 "Frontend_Roles "的集合,按 "Edit"。
添加我们的用户
使用代码补全很重要,只有代码补全会正确填写所有区域。
最后按 "Save"

5.2.运行

为了启动流程,我们通过approuter URL进入我们的前端应用程序。
这就是:
ApprouterURL + route + endpoint + slash

在我的例子中:
frontendrouter.cfapps.us10.hana.ondemand.com/tofrontend/…

结果,我们得到一个登录屏幕,在其中输入我们有效的BTP用户的凭证。

之后,我们被重定向到我们前端应用程序的主页,也就是我们核心服务器模块的/homepage端点。
如果我们没有看到错误信息,我们可以感到高兴。
枯燥的教程结束了。
然而....,如果我们累了,有充分的理由,让我们在仔细看看结果JWTs之前不要放弃教程(不,向下滚动到教程的结尾是不可能的)。

JWT内容的解释。

5.2.1.用户-JWT-令牌

第一个输出是我们在登录后得到的经过解码和格式化的JWT令牌。

正如我们所知,Approuter与XSUAA服务器一起处理登录,所使用的OAuth流程为

这个流程需要一个额外的安全步骤:在输入正确的用户/密码后,服务器会向配置的重定向URI发送一个 "代码"。Approuter负责提供这样的重定向端点,并根据代码获取JWT令牌。
在截图中,我们可以看到OAuth令牌端点的URL,approuter正在从绑定中读取。
就像客户ID一样。
另外,我们可以看到起源,它是与SAP BTP中的子账户相关的默认身份提供者。
一个重要的主张是受众,它指出JWT令牌的目的是被OAuth客户端frontendxsuaa理解,也就是我们的XSUAA实例
在扩展属性部分,我们可以看到身份区,在我的例子中,它被称为frontendsubdomain。
有了这些信息,我们就可以确认JWT令牌是由前端XSUAA发出的。
作为附加信息,我们可以看到属于我的用户的数据:姓名、角色集合和范围,这是我们需要的。

底线:
正如预期,JWT是一个用户令牌,属于前端世界。

5.2.2.目的地-JWT-令牌

下一个无聊的文本属于我们在令牌交换后收到的JWT令牌。
记住:
我们需要一个令牌来调用目标服务。
这个OAuth流程的授予类型被称为JWT承载令牌授予,这就是我们在下面的截图中可以看到的。

在第二个JWT中还有什么有趣的?
没有什么真正有趣的,但让我们考虑一下:
我们仍然在同一个身份区。
OAuth客户端现在是destinationService,它也包含在观众中。
这意味着,我们可以安全地将这个令牌发送到目的地服务的REST API。
令牌包含了使用目的地服务所需的所有作用域。
此外,我们可以看到,所有的用户特定数据都保留在令牌中:姓名、作用域、角色集合。这很有意思,是的。

5.2.3.SAML-JWT-token

此时,我们已经使用第二个JWT令牌来调用目的地服务。
结果,我们收到了目的地的配置信息,此外,我们还收到了一个JWT令牌,我们用它来调用后台应用程序。
目的地服务内部已经执行了SAML2承载授权流程。
这个流程已经完成了不可能完成的任务:跨越区域边界获取一种用户令牌。
(OMG-多么无聊啊

截图显示,发行人、子账户和身份区现在位于后台世界。
客户端ID和受众指向我们在后台试用账户中创建的XSUAA实例。
用户信息已被保留,我们可以看到名字。
有趣的是原产地声明:*cfapps.us10.hana.ondemand.com22cece3
*还记得吗?
在所有这些冗长的教程之后,我们有理由忘记这些无聊的文字。
要记住它,我们只需要查看前端子账户的IDP元数据
<ns3:EntityDescriptor
entityID="cfapps.us10.hana.ondemand.com/22cece36-3c8c-4420-bad5-60e1787230da"
身份提供者的实体ID是前端试用的域名和子账户ID的组合。
然而,它并不完全匹配。
谁在乎呢?
然而,这将我们引向后端试用账户,我们在这里配置了第二个身份提供者。
更准确地说,我们根据前端账户中IDP的SAML元数据,配置了信任配置。
而在后端账户中,security->trust,我们可以准确地找到我们正在寻找的字符串:
cfapps.us10.hana.ondemand.com22cece3

结论
这让我们得出了惊人的结论:
所有的配置努力、信任、SAML,都导致了将前端用户信息跨越边界传送到后端,甚至使其成为后端XSUAA服务器发出的JWT令牌。
哇--超级棒。

6.清理

前台子账户:
手动删除目的地配置

cf login -aapi.cf.us10.hana.ondemand.com-o frontendorg
cf d frontend -r -f
cf d frontendrouter -r -f
cf ds frontendXsuaa -f
cf ds frontendDestination -f

后台子账户:
手动删除信任配置。

cf login -aapi.cf.ap21.hana.ondemand.com-o backendorg
cf d backend -r -f
cf ds backendXsuaa -f

总结

本教程的折磨告诉我们:
如何在 SAP BTP、Cloud Foundry 的 2 个子帐户之间配置信任。
如何创建 OAuth2SAMLBearerAssertion 类型的目的地并填写字段。
了解场景、SAML 和 OAuth 以及 JWT 属性。
如何编写受保护并使用目的地服务的最小无用节点应用程序。

快速指南

配置信任:
从前端子账户驾驶舱的Destinations菜单中下载IdP元数据。
在后端子账户驾驶舱中创建新的信任配置,并粘贴IdP元数据。
创建目的地:
使用后端子账户的SAML元数据和XSUAA凭证。
注意:token Url不取自凭证。

附录1:示例代码 后台应用程序

backend-security.json

{
    "xsappname": "backendxsuaa",
    "tenant-mode": "dedicated"
}

manifest.yml

---
applications:
  - name: backend
    path: app
    memory: 64M
    routes:
    - route: backend.cfapps.ap21.hana.ondemand.com
    services:
      - backendXsuaa 

应用程序

package.json

{
  "dependencies": {
    "@sap/xsenv": "latest",
    "@sap/xssec": "latest",
    "express": "^4.17.1",
    "passport": "^0.4.0"  
  }
}

server.js

const xsenv = require('@sap/xsenv')
const UAA_CREDENTIALS = xsenv.getServices({myXsuaa: {tag: 'xsuaa'}}).myXsuaa

const express = require('express')
const app = express();
const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
app.use(passport.initialize())
app.use(express.json())


// start server
app.listen(process.env.PORT)


app.get('/endpoint', passport.authenticate('JWT', {session: false}), (req, res) => {
    const auth = req.authInfo  
    console.log(`===> [backendapp] called by user '${auth.getGivenName()}' from subdomain '${auth.getSubdomain()}' with oauth client: '${auth.getClientId()}'`)
    res.json({
        'message': 'BACKEND successfully called',
        'jwtToken': auth.getAppToken()})
})

附录2:示例代码 前端应用程序

frontend-security.json

{
    "xsappname": "frontendxsuaa",
    "tenant-mode": "dedicated",
    "role-templates": [{
        "name": "uaaUserDefaultRole",
        "description": "Default role uaa.user required for user centric scenarios",
        "scope-references": ["uaa.user"]
    }],
    "role-collections": [{
          "name": "Frontend_Roles",
          "role-template-references": [ "$XSAPPNAME.uaaUserDefaultRole" ]
        }
    ]    
}

manifest.yml

---
applications:
  - name: frontend
    path: app
    memory: 64M
    routes:
    - route: frontend.cfapps.us10.hana.ondemand.com
    services:
      - frontendXsuaa
      - frontendDestination
  - name: frontendrouter
    routes:
    - route: frontendrouter.cfapps.us10.hana.ondemand.com
    path: approuter
    memory: 128M
    env:
      destinations: >
        [
          {
            "name":"destination_frontend",
            "url":"https://frontend.cfapps.us10.hana.ondemand.com",
            "forwardAuthToken": true
          }
        ]      
    services:
      - frontendXsuaa

应用程序

package.json

{
  "dependencies": {
    "@sap/destinations": "latest",
    "@sap/xsenv": "latest",
    "@sap/xssec": "^3.2.13",
    "express": "^4.17.1",
    "node-fetch": "2.6.2",
    "passport": "^0.4.0"
  }
}

server.js

const xsenv = require('@sap/xsenv')
const INSTANCES = xsenv.getServices({
    myXsuaa: {tag: 'xsuaa'},
    myDestination: {tag: 'destination'}
})
const XSUAA_CREDENTIALS = INSTANCES.myXsuaa
const DESTINATION_CREDENTIALS = INSTANCES.myDestination

const fetch = require('node-fetch')
const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
passport.use('JWT', new JWTStrategy(XSUAA_CREDENTIALS))
const express = require('express')
const app = express();
app.use(passport.initialize())
app.use(express.json())


// start server
app.listen(process.env.PORT)

 
// calling destination service with user token and token exchange
app.get('/homepage', passport.authenticate('JWT', {session: false}), async (req, res) => {

    const userJwtToken = req.authInfo.getAppToken()
 
    // instead of client creds, we must use token exchange
    const destJwtToken = await _doTokenExchange(userJwtToken)

    // read destination
    const destination = await _readDestination('destination_to_backend', destJwtToken)
    const samlbearerJwtToken = destination.authTokens[0].value

    // call backend app endpoint
    const response = await _callBackend(destination)
    const responseJson = JSON.parse(response)
    const responseJwtTokenDecoded = decodeJwt(responseJson.jwtToken)

    // print token info to browser
    const htmlUser = _formatClaims(userJwtToken) 
    const htmlDest = _formatClaims(destJwtToken) 
    const htmlBearer = _formatClaims(samlbearerJwtToken) 

    res.send(`  <h4>JWT after user login</h4>${htmlUser}
                <h4>JWT after token exchange</h4>${htmlDest}
                <h4>JWT issued by OAuth2SAMLBearerAssertion destination</h4>${htmlBearer}
                <h4>Response from Backend</h4>${responseJson.message}. The token: <p>${JSON.stringify(responseJwtTokenDecoded)}</p>`)
})


/* HELPER */

async function _readDestination(destinationName, jwtToken, userToken){
    const destServiceUrl = `${DESTINATION_CREDENTIALS.uri}/destination-configuration/v1/destinations/${destinationName}`
    const options = {
       headers: { Authorization: 'Bearer ' + jwtToken}
    }
    const response = await fetch(destServiceUrl, options)
    const responseJson = await response.json() 
    return responseJson
}

async function _doTokenExchange (bearerToken){
    return new Promise ((resolve, reject) => {
       xssec.requests.requestUserToken(bearerToken, DESTINATION_CREDENTIALS, null, null, null, null, (error, token)=>{
          resolve(token)
       })  
    })  
}

async function _callBackend (destination){  
    const backendUrl = destination.destinationConfiguration.URL
    const options = {
       headers: { 
          Authorization : destination.authTokens[0].http_header.value  // contains the "Bearer" plus space
       }
    }
    const response = await fetch(backendUrl, options)
    const responseText = await response.text()
    return responseText
}

function decodeJwt(jwtEncoded){
    return new xssec.TokenInfo(jwtEncoded).getPayload()
}

function _formatClaims(jwtEncoded){
    // const jwtDecodedJson = new xssec.TokenInfo(jwtEncoded).getPayload()
    const jwtDecodedJson = decodeJwt(jwtEncoded)
    console.log(`===> The full JWT: ${JSON.stringify(jwtDecodedJson)}`)
 
     const claims = new Array()
     claims.push(`issuer: ${jwtDecodedJson.iss}`)
     claims.push(`<br>client_id: ${jwtDecodedJson.client_id}</br>`)
     claims.push(`grant_type: ${jwtDecodedJson.grant_type}`)
     claims.push(`<br>scopes: ${jwtDecodedJson.scope}</br>`)
     claims.push(`ext_attr: ${JSON.stringify(jwtDecodedJson.ext_attr)}`)
     claims.push(`<br>aud: ${jwtDecodedJson.aud}</br>`)  
     claims.push(`origin: ${jwtDecodedJson.origin}`)
     claims.push(`<br>name: ${jwtDecodedJson.given_name}</br>`)
     claims.push(`xs.system.attributes: ${JSON.stringify(jwtDecodedJson['xs.system.attributes'])}`)
     return claims.join('')
}

approuter

package.json

{
    "dependencies": {
        "@sap/approuter": "latest"
    },
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"
    }
}

xs-app.json

{
  "authenticationMethod": "route",
  "routes": [
    {
      "source": "^/tofrontend/(.*)$",
      "target": "$1",
      "destination": "destination_frontend",
      "authenticationType": "xsuaa"
    }
  ]
}

附录3:示例代码 目的地

destination_to_backend

#clientKey=<< Existing password/certificate removed on export >>
#tokenServicePassword=<< Existing password/certificate removed on export >>
#
#Fri Jun 10 07:09:11 UTC 2022
Description=Destination pointing to backend app endpoint in backend account
Type=HTTP
authnContextClassRef=urn\:oasis\:names\:tc\:SAML\:2.0\:ac\:classes\:PreviousSession
audience=https\://backendsubdomain.authentication.ap21.hana.ondemand.com
Authentication=OAuth2SAMLBearerAssertion
Name=destination_to_backend
tokenServiceURL=https\://backendsubdomain.authentication.ap21.hana.ondemand.com/oauth/token/alias/backendsubdomain.azure-ap21
ProxyType=Internet
URL=https\://backend.cfapps.ap21.hana.ondemand.com/endpoint
nameIdFormat=urn\:oasis\:names\:tc\:SAML\:1.1\:nameid-format\:emailAddress
tokenServiceURLType=Dedicated
tokenServiceUser=sb-backendxsuaa\!t7722