.env
you created during lab setup is for configuring the tool httpYac. This is to avoid typing the configuration every time. We will use this tool extensively in the exercises.Side Note:There are a few methods for trusting and accepting tokens from clients registered with identity zones/subaccounts in the same SAP BTP region. There is no need for additional steps to establish trust as they are all based on the same trusted domain. These methods provide means for a client to add another client's scopes to the tokens it receive from XSUAA service. This results in adding the target client to audience of the tokens they receive from XSUAA service. So these ae accepted by the library validating the tokens. In the case of multitenant scenario, the same client has authorization to request tokens from other subdomains, those which have subscribed to the application. The tokens issued for these clients from other subdomains also will have this shared client in the audience and the shared scopes as scopes. So these are naturally accepted by the library. Net result is that the tokens issued for foreign subdomains or other clients are accepted by the Business Logic Application when using the standard libraries from SAP BTP for validating tokens. The methods I refer to here are:
We will side-step a discussion of these. For the purpose of the exercises below, applications in other subaccounts of the same region are also external applications. They have not "established trust" using any of the methods listed above. Additionally, @Sap/xssec can be configured to accept tokens from multiple clients since version 3.1.0. |
Pre-requisite:Only an SAP Cloud Identity Services tenant can be configured as Open ID Connect based identity provider. SAP BTP trial does not offer SAP Cloud Identity Services tenant. This environment will not be sufficient for this exercise. You should have a Custom Application Identity Provider with protocol OpenID Connect listed in Trust Configuration. Perform the steps outlined in Establish Trust and Federation Between UAA and Identity Authentication to have this. Refer to this blog by Kumar Amar for performing this setup using API. The Custom Application Identity Provider is set as Available for User Logon. The expected state is as illustrated in the picture below (picture from the blog by Kumar Amar. You have credentials of a user configured in the Custom Application Identity Provider with which you can login. The subaccount has entitlement for application plan of service "Cloud Identity Services" (service name: identity). |
---
_schema-version: '3.1'
ID: cf-application-ext-3a
extends: cf-application
version: 1.0.0
resources:
- name: cf-application-uaa
parameters:
config:
oauth2-configuration:
grant-types:
- urn:ietf:params:oauth:grant-type:jwt-bearer
- password
- name: cf-approuter-uaa
disabled: true
parameters:
config:
oauth2-configuration:
grant-types: []
> cf deploy cf-application_1.0.0.mtar -e mta-ext-ex3a.yaml -f --no-start
..
> btp get security/app cf-application!t53187 --subaccount $subaccount
..
grant-types:
- urn:ietf:params:oauth:grant-type:jwt-bearer
..
> btp get security/app cf-approuter!t53187 --subaccount $subaccount
..
grant-types:
..
{
"display-name": "Exercise 3a: JWT Bearer Token grant",
"xsuaa-cross-consumption": true,
"oauth2-configuration": {
"redirect-uris": [
"http://localhost:3030/callback"
]
}
}
> cf create-service identity application cf-external-ias -c exercise-3a-ias.json
..
> cf create-service-key cf-external-ias key1
..
> cf service-key cf-external-ias key1
{
"clientid": "0098768-b2d2-fff1-884b-dc87777773639",
"clientsecret": "HguVBwnF67543BFCElK987U2El7c2=",
"credential-type": "SECRET",
"domain": "accounts.ondemand.com",
"domains": [
"accounts.ondemand.com"
],
"url": "https://at6ysjs7ioo.accounts.ondemand.com",
"zone_uuid": "----------------------------"
}
ias_xsappname=cf-approuter!t53187
ias_clientId=0098768-b2d2-fff1-884b-dc87777773639
ias_clientSecret=HguVBwnF67543BFCElK987U2El7c2=
ias_url=https://at6ysjs7ioo.accounts.ondemand.com
ias_tokenEndpoint={{ias_url}}/oauth2/token
ias_authorizationEndpoint={{ias_url}}/oauth2/authorize
ias_scope=openid
ias_responseType=code
ias_redirectUri=http://localhost:3030/callback
#bash
httpyac oauth2 --prefix ias --flow authorization_code > ias-id-token.txt
#powershell
httpyac oauth2 --prefix ias --flow authorization_code |
Set-Content -Encoding ascii ias-id-token.txt
cat ias-id-token.txt | node decode-jwt.js
### Get Token for Business Logic Application with IAS ID Token
# @name jwt_bearer
{{
const fs = require("fs");
const jwt = fs.readFileSync(__dirname + "/ias-id-token.txt");
exports.assertion = jwt.toString('utf8').trim();
}}
# @jwt access_token
POST {{blApp_url}}/oauth/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json
Authorization: Basic {{blApp_clientId}} {{blApp_clientSecret}}
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion={{assertion}}
### Passcode
{{
const open = require("open")
const passcode_url = `${blApp_url}/passcode`
open(passcode_url);
}}
# @name passcode
# @jwt access_token
POST {{blApp_url}}/oauth/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json
Authorization: Basic {{blApp_clientId}} {{blApp_clientSecret}}
grant_type=password
&passcode={{$password Enter Passcode}}
> npx httpyac exercise-3a.http -n jwt_bearer -o body
=== Get Token for Business Logic Application with IAS ID Token ===
{
"access_token": "eyJhbGc............mdgg",
"token_type": "bearer",
"expires_in": 43199,
"scope": "openid",
"jti": "c60d8b7943b54f35a2a8646e3f885500",
"access_token_parsed": {
"jti": "c60d8b7943b54f35a2a8646e3f885500",
"ext_attr": {
"enhancer": "XSUAA",
"subaccountid": "a0622234-xxxx-xxxx-xxxx-ff27b1b0ea49",
"zdn": "my-ias-sandbox",
"oidcIssuer": "https://at6ysjs7ioo.accounts.ondemand.com"
},
"user_uuid": "128be729-f34d-40bd-a7a2-db1645776cf5",
"given_name": "John",
"xs.user.attributes": {},
"family_name": "Dow",
"sub": "1fa1d178-xxxx-xxxx-xxxx-42b28f0c6f9e",
"scope": [
"openid"
],
"client_id": "sb-cf-application!t131271",
"cid": "sb-cf-application!t131271",
"azp": "sb-cf-application!t131271",
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"user_id": "1fa1d178-e624-4ec1-8022-42b28f0c6f9e",
"origin": "sap.custom",
"user_name": "john.dow@my-sap.com",
"email": "john.dow@my-sap.com",
"rev_sig": "7a1c79bb",
"iat": 1658909961,
"exp": 1658953161,
"iss": "https://my-ias-sandbox.authentication.eu10.hana.ondemand.com/oauth/token",
"zid": "a0622234-xxxx-xxxx-xxxx-ff27b1b0ea49",
"aud": [
"openid",
"sb-cf-application!t131271"
]
}
}
> httpyac exercise-3a.http -n passcode -o body
=== Passcode ===
{
"access_token": "eyJhb...,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,4dRFY2X-eg",
"token_type": "bearer",
"id_token": "eyJhbGc...........................,,,,,,,,,,,,,jg",
"expires_in": 43199,
"scope": "openid",
"jti": "3fabff31a05444898a8875fda1dd081d",
"access_token_parsed": {
"jti": "3fabff31a05444898a8875fda1dd081d",
"ext_attr": {
"enhancer": "XSUAA",
"subaccountid": "a0622234-xxxx-xxxx-xxxx-ff27b1b0ea49",
"zdn": "my-ias-sandbox",
"oidcIssuer": "https://at6ysjs7ioo.accounts.ondemand.com"
},
"user_uuid": "128be729-f34d-40bd-a7a2-db1645776cf5",
"given_name": "John",
"xs.user.attributes": {},
"family_name": "Dow",
"sub": "1fa1d178-e624-4ec1-8022-42b28f0c6f9e",
"scope": [
"openid"
],
"client_id": "sb-cf-application!t131271",
"cid": "sb-cf-application!t131271",
"azp": "sb-cf-application!t131271",
"grant_type": "password",
"user_id": "1fa1d178-e624-4ec1-8022-42b28f0c6f9e",
"origin": "sap.custom",
"user_name": "john.dow@my-sap.com",
"email": "john.dow@my-sap.com",
"rev_sig": "7a1c79bb",
"iat": 1658910886,
"exp": 1658954086,
"iss": "https://my-ias-sandbox.authentication.eu10.hana.ondemand.com/oauth/token",
"zid": "a0622234-xxxx-xxxx-xxxx-ff27b1b0ea49",
"aud": [
"openid",
"sb-cf-application!t131271"
]
}
}
# $subaccount is the id of the subacoount and
# $user is the user id (usually an email) of the user
# in IDP sap.custom.
# Here sap.custom is the key for the trusted OpenID Connect identity provider
btp assign security/role-collection Excercise_Role_Collection_1 \
--to-user $user --of-idp sap.custom --subaccount $subaccount
Side Note:The token exchange examined here is automatically done by @Sap/xssec library from version 3.1.2. So the external application can send the id token from SAP Cloud Identity Service to the a Business Logic Application using a version of this library that supports this exchange. This illustrates that such an exchange can be made implicitly by the Business Logic Application and this can be transparently done by standard libraries. |
Pre-requisite:You need access to a SAML Identity Provider which can provide you signed SAML assertion for a user, addressed to your SAP BTP subaccount. If you don't have access to one, Appendix A describes a lab setup for an Identity Provider which is sufficient for this exercise.
|
---
_schema-version: '3.1'
ID: cf-application-ext-3b
extends: cf-application
version: 1.0.0
resources:
- name: cf-application-uaa
parameters:
config:
oauth2-configuration:
grant-types:
- urn:ietf:params:oauth:grant-type:saml2-bearer
- password
- name: cf-approuter-uaa
disabled: true
parameters:
config:
oauth2-configuration:
grant-types: []
cf deploy cf-application_1.0.0.mtar -e mta-ext-ex3b.yaml -f --no-start
..
> btp get security/app cf-application!t53187 --subaccount $subaccount
..
grant-types:
- urn:ietf:params:oauth:grant-type:saml2-bearer
..
> btp get security/app cf-approuter!t53187 --subaccount $subaccount
..
grant-types:
..
### Get Token for Business Logic Application with signed SAML assertion
# @name saml2_bearer
{{
const fs = require("fs");
const signedAssertion = fs.readFileSync(__dirname + "/saml-assertion.xml");
const assertionEncoded = Buffer.from(signedAssertion,'utf8').toString("base64")
exports.assertion = encodeURIComponent(assertionEncoded);
}}
# @jwt access_token
POST {{blApp_SAML2tokenEndpoint}}
Content-Type: application/x-www-form-urlencoded
Accept: application/json
Authorization: Basic {{blApp_clientId}} {{blApp_clientSecret}}
grant_type=urn:ietf:params:oauth:grant-type:saml2-bearer
&assertion={{assertion}}
blApp_SAML2tokenEndpoint=https://..authentication.us10.hana.ondemand.com/oauth/token/alias/...
> npx httpyac exercise-3b.http -n saml2_bearer -o body
=== Get Token for Business Logic Application with signed SAML assertion ===
{
"access_token": "eyJhbG.......................67SjiHFg",
"token_type": "bearer",
"expires_in": 43199,
"scope": "openid",
"jti": "b69886cfd81142879629687635084e7f",
"access_token_parsed": {
"jti": "b69886cfd81142879629687635084e7f",
"ext_attr": {
"enhancer": "XSUAA",
"subaccountid": "a0622234-xxxx-xxxx-xxxx-ff27b1b0ea49",
"zdn": "my-ias-sandbox"
},
"given_name": "John",
"xs.user.attributes": {},
"family_name": "Dow",
"sub": "87fcd939-2dd2-4b9c-bb29-6cc89e27dff6",
"scope": [
"openid"
],
"client_id": "sb-cf-application!t131271",
"cid": "sb-cf-application!t131271",
"azp": "sb-cf-application!t131271",
"grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer",
"user_id": "87fcd939-2dd2-4b9c-bb29-6cc89e27dff6",
"origin": "localhost.testing",
"user_name": "john.dow@my-sap.com",
"email": "john.dow@my-sap.com",
"rev_sig": "d8b8b3fe",
"iat": 1658930097,
"exp": 1658973297,
"iss": "https://my-ias-sandbox.authentication.eu10.hana.ondemand.com/oauth/token",
"zid": "a0622234-xxxx-xxxx-xxxx-ff27b1b0ea49",
"aud": [
"openid",
"sb-cf-application!t131271"
]
}
}
# $subaccount is the id of the subacoount and
# $user is the user id (email) of the user
# Here localhost.testing is the key for the SAML identity provider
btp assign security/role-collection Excercise_Role_Collection_1 \
--to-user $user --of-idp localhost.testing --subaccount $subaccount
Side Note:The token exchange examined here is implemented by destination service in SAP BTP where it acts as the trusted identity provider. See documentation on how to set up trust with destination service. When destination service is used as the SAML identity provider, it copies the user attributes from the trusted JWT token it receives as attribute statements in SAML assertion. The rules for this are mentioned in documentation (see section Propagate User Attributes.) |
npm install -D saml xpath @xmldom/xmldom
const fs = require("fs");
const {getSamlIdpMetadata} = require("./saml-localhost-idp");
if (require.main === module) {
main( process.argv.slice(2) );
}
function main(argv) {
fs.writeFileSync("saml-idp-metadaa.xml",getSamlIdpMetadata());
}
const fs = require("fs");
const {getSignedAssertion, extractFromSpMetadata} =
require("./saml-localhost-idp");
if (require.main === module) {
main( process.argv.slice(2) );
}
function main(argv) {
//https://${xsuaa.url}/saml/metadata
const sp_metadata_xml = fs.readFileSync(__dirname + "/saml-sp-metadata.xml");
const user_attributes = require("./user_attributes");
const passphrase = process.env.samlidp_key_passphrase;
const sp = extractFromSpMetadata(sp_metadata_xml)
const signedAssertion = getSignedAssertion(sp,user_attributes,passphrase)
fs.writeFileSync("saml-assertion.xml",signedAssertion);
}
const fs = require("fs");
const { createPrivateKey, X509Certificate } = require('crypto');
const IDP_ENTITY_ID = "localhost/testing";
function getSamlIdpMetadata() {
const pem = fs.readFileSync(__dirname + "/saml-idp.pem");
const cert = new X509Certificate(pem);
IDP_CERTIFICATE = cert.raw.toString('base64')
const samlidp_metadata =
`<?xml version="1.0" encoding="UTF-8"?>
<ns3:EntityDescriptor
ID="${IDP_ENTITY_ID}"
entityID="${IDP_ENTITY_ID}"
xmlns="http://www.w3.org/2000/09/xmldsig#"
xmlns:ns3="urn:oasis:names:tc:SAML:2.0:metadata">
<ns3:IDPSSODescriptor
WantAuthnRequestsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<ns3:KeyDescriptor use="signing">
<KeyInfo>
<X509Data>
<X509Certificate>${IDP_CERTIFICATE}</X509Certificate>
</X509Data>
</KeyInfo>
</ns3:KeyDescriptor>
</ns3:IDPSSODescriptor>
</ns3:EntityDescriptor>`
return samlidp_metadata;
}
function extractFromSpMetadata(sp_metadata_xml){
const xpath = require('xpath'),
dom = require('@xmldom/xmldom');
const doc = new dom.DOMParser().parseFromString(sp_metadata_xml.toString())
const select = xpath.useNamespaces({"md":"urn:oasis:names:tc:SAML:2.0:metadata"})
const SP_ENTITY_ID = select('/md:EntityDescriptor/@entityID',doc)?.[0].value
if( ! SP_ENTITY_ID )
throw("Service Provider Metadata is invalid: attribute entityID is missing.");
const TOKEN_ENDPOINT = select('/md:EntityDescriptor/md:SPSSODescriptor' +
'/md:AssertionConsumerService[@Binding="urn:oasis:names:tc:SAML:2.0:bindings:URI"]'+
'/@Location',doc)?.[0]?.value
if( ! TOKEN_ENDPOINT )
throw("Service Provider Metadata is invalid: service with URI Binding is missing.");
return {
entityId: SP_ENTITY_ID,
tokenEndPoint: TOKEN_ENDPOINT
}
}
function getSignedAssertion(sp,attributes=null,passphrase=null) {
const saml = require("saml").Saml20
cert = fs.readFileSync(__dirname + "/saml-idp.pem"),
key = fs.readFileSync(__dirname + "/saml-idp.key")
if(typeof passphrase == "string" & passphrase != "") {
pk = createPrivateKey( {passphrase:passphrase, key: key} );
key = pk.export({format:"pem",type:"pkcs1"});
}
const options = {
nameIdentifier: attributes?.email || attributes?.mail || "system@localhost.earth",
nameIdentifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
attributes: attributes,
issuer: IDP_ENTITY_ID,
recipient: sp.tokenEndPoint,
audiences: sp.entityId,
authnContextClassRef: "urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession",
lifetimeInSeconds: 6000, //+300sec
includeAttributeNameFormat: false,
cert: cert,
key: key
};
return saml.create(options)
}
exports.getSamlIdpMetadata = getSamlIdpMetadata;
exports.extractFromSpMetadata = extractFromSpMetadata
exports.getSignedAssertion = getSignedAssertion;
> openssl req -new -x509 -days 30 -nodes -keyout saml-idp.key -out saml-idp.pem
Generating a RSA private key
.+++++
..........................................+++++
writing new private key to 'saml-idp.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:.
State or Province Name (full name) [Some-State]:.
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:COM
Organizational Unit Name (eg, section) []:Experimenting
Common Name (e.g. server FQDN or YOUR name) []:localhost/testing
Email Address []:
node saml-idp-metadata.js
# copy the value of blApp_url from .env as ${blApp_url} below
curl -so saml-sp-metadata.xml ${blApp_url}/saml/metadata
{
"Groups": ["administrators","accountants"],
"first_name":"John",
"last_name":"Dow",
"mail":"john.dow@my-sap.com"
}
> node saml-signed-assertion.js
SP Token Endpoint: https://my-ias-sandbox.authentication.us10.hana.ondemand.com/oauth/token/alias/my-ias-sandbox.aws-li...
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
12 | |
11 | |
10 | |
8 | |
8 | |
8 | |
8 | |
7 | |
7 | |
7 |