Technical Articles
How to generate SAML bearer assertion token for OAuth2SAMLBearerAssertion flow?
Abstract.
![]() |
This instalment is to help understand what it takes to implement the OAuth2SAMLBearerAssertion Flow without SAP BTP Destination service. Or, in other words, how to manually generate your own SAML Bearer Assertion token. Albeit that’s not the recommended approach there is still some strong DIY demand. |
Disclaimer.
Good to know:
|
Deja vu.
I already covered the OAuth2SAMLBearerAssertion flow with the following mini series of blogs, where the SAP BTP Destination Service acts both as the trusted IdP and a user propagation broker for the OAuth2SAMLBearerAssertion flow, namely:
|
Putting it all together.
Per aspera ad astra. Generate your own OAuth2-SAML-Bearer-Assertion token.From my own DIY experience that’s the hardest part of the job. Even, if this is not a recommended approach, doing this will definitely help you understand the OAuth2SAMLBearerAssertion flow mechanism at all and ultimately and very likely make you appreciate the value proposition of SAP BTP Destination service. |
SAP SuccessFactors brief.
The public SAP SFSF documentations offers a thorough guidance on how to implement Authentication Using OAuth 2.0, namely:
and describes the following three options to generate a SAML assertion:
a. Use a third-party IdP that you trust. That’s the recommended approach. Like for instance using SAP BTP Destination Service as our trusted IdP (Identity Provider). Please note that if you are using the destination service trust it will be used to sign the saml assertion. You still will need to somehow provide the user claim for the saml assertion. If using the destination service the recommended approach is to provide the user’s JWT token in the X-user-token header of the find destination call. b. Use the offline SAML bearer assertion generation tool. see SAP note – 3031657 c. Use built-in SAP SuccessFactors IdP SAML bearer assertion generation endpoint. Good to know:
SAP SuccessFactors SAML Authentication in Python SAP Jam SAML Authentication Using Python |
SAP Analytics Cloud brief.
The access to resources and assets of SAP Analytics Cloud tenant is protected with SAML2.0 protocol with the so-called SP-initiated WebSSO flow. This flow requires a user to enter her or his credentials every time the SAML assertion has expired (or has been removed from the browser cache). Also SAC tenant imposes a time limit on the duration of the HTTP session. While this is OK for the interactive usage of SAC tenant this may be not OK if a custom client application needed remote access to SAC resources or assets without user interaction. Indeed, there will be no one to acknowledge and dismiss the SAML assertion login pop-up window etc… This is where the SAP Analytics Cloud App Integration with OAuth 2.0 using the SAML bearer assertion flow comes into the scene. Arguably there is not much insight across the SAP help pages on how this can be done with the likes of SAP Analytics Cloud. Let’s try to bridge this gap. |
OAuth2SAMLBearerAssertion Flow with manually signed SAML Bearer Assertion token.
Disclaimer. The below code snippets are provided as is.
The plan for this brief is to cover the steps c, d and e, namely: a. Configuring SAP Analytics Cloud for OAuth2 authentication with SAML bearer assertion flow – already covered here. b. Generating SAML bearer assertions offline: Using our own XML-signing code based on the following SAP note – 3031657
|
ad c). Generating SAML bearer assertions manually [in nodejs]
Let’s create a simple nodejs generateSAMLBearerAssertion function:
//
// Create SAML assertions. Supports SAML 1.1 and SAML 2.0 tokens.
//
var saml = require('saml').Saml20; // https://www.npmjs.com/package/saml
async function generateSAMLBearerAssertion() {
const cert = '-----BEGIN CERTIFICATE-----\nMIIFGzCCAwMCBGBb1dwwDQYJKoZIhvcNAQELBQAwUjELMAkGA1UEBhMCVVMxDDAK\nBgNVBAoMA1NBUDEVMBMGA1UECwwMY
..........................(truncated)............................
i\ne3pZsV0QGgSCMZ8kNQobunEPnfkXysLhUvWzniY0UI9uLY7F9934p3PLnZAJOhLJ\nO0X6cHCFbMC+6GxXTdisQVivIOKUURdaHVX6B270SUDiP6TDPApn9E+IaISzPRpk\nXT6c0QNVYg37DBU/qhSN\n-----END CERTIFICATE-----\n';
const key = '-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDFz/eQv30tj5oC\nLjT1Im7OtVAVo6mB/wQbEpbOh3LSI
..........................(truncated)............................
JGRqkeRIArXvScbLwq62ViESgOIOU8TdR0n3fachXehZLgRUTa2IGI6zKuVSaXLq\nWBgr0UKz5CLYl4kvZ8ECFbb/I8psoa5LSxBTGdiZznqgLKnImxU1WDSA2xlKJy7J\nAwx8lLYgANSJ7qkKPgPR/t5ZHrx/plY=\n-----END PRIVATE KEY-----\n';
var options = {
cert: Buffer.from(cert, 'utf-8'), // x509 certificate
key: Buffer.from(key, 'utf-8'), // private key (it contains public key as well)
issuer: 'quovadis/ateam-isveng', // CN of the public certificate key which is the Provider Name in a Trusted SAML Provider in SAC/System/Administration/App Integration
lifetimeInSeconds: 3600,
attributes: {
'client_id': '<OAuth client id from SAC/System/Administration/App Integration>',
},
includeAttributeNameFormat: true, //false,
sessionIndex: '_faed468a-15a0-4668-aed6-3d9c478cc8fa',
authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession',
nameIdentifierFormat: 'urn:oasis:names:tc:SAML:2.0:attrname-format:email', // urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified
nameIdentifier: '<user name>', // user email address
recipient: '<OAuth2SAML token URL from SAC/System/Administration/App Integration>',
audiences: '<Audience from SAC/System/Administration/App Integration>>',
// signatureAlgorithm: rsa-sha256',
// digestAlgorithm: 'sha256',
signatureNamespacePrefix: 'ds',
};
var unsignedAssertion = saml.createUnsignedAssertion(options);
var signedAssertion = saml.create(options);
signedAssertion = btoa(signedAssertion);
console.log('btoa-ed signedAssertion: ', signedAssertion);
signedAssertion = encodeURIComponent(signedAssertion);
console.log('unsignedAssertion: ', unsignedAssertion);
console.log('signedAssertion: ', signedAssertion);
return signedAssertion; // unsignedAssertion;
}
// this is to create a base64 and URL encoded saml bearer assertion:
//
var samlassertion = await generateSAMLBearerAssertion();
------------------------------------------------------------------------------------
PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIFZlcnNpb249IjIuMCIgSUQ9Il81TEdreTZjVVF6UXVlWWFnb3BITkVIZXh6RkZnVmNuSiIgSXNzdWVJbnN0YW50PSIyMDIxLTA0LTA1VDA5OjE4OjM5LjIwM1oiPjxzYW1sOklzc3Vlcj5xdW92YWRpcy9hdGVhbS1pc3Zlbmc8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR
.................................... (truncated)......................................
M2E4OC00YmYwLWI4ODYtZWI5ZWM3YWY2OThhIWIxNjU4fGNsaWVudCFiNjU1PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU%2BPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPg%3D%3D
Good to know:
|
ad d). Obtaining a bearer access token.
then you can use the above SAML assertion to generate your bearer access token to unlock the SAC APIs by calling the OAuth2SAML token URL endpoint as depicted in the below nodejs snippet:
async function getoauthaccesstoken(saml_bearer_assertion) {
const credentials = { // OAuth client_id and secret from SAC OAuth client you have created
client: {
id: '<OAuth client_id>',
secret: '<OAuth secret>'
},
auth: {
tokenHost: '<OAuth2SAML token URL>',
},
options: {
authorizationMethod: 'body'
}
}
const options = {
headers: { "Authorization": "Basic <base64-encoded <client_id>:<secret>:>" }
//headers: { 'Authorization': 'Basic ' + btoa('<client_id>:<secret>') }
};
var params = new URLSearchParams();
params.append('client_id', credentials.client.id);
//params.append('client_secret', credentials.client.secret);
params.append('grant_type', "urn:ietf:params:oauth:grant-type:saml2-bearer");
params.append("assertion", saml_bearer_assertion);
params.append("scope", "openid uaa.user");
let documents;
try {
const response = await axios.post(credentials.auth.tokenHost, params , options);
documents = JSON.stringify(response.data, null, 2);
console.log(documents);
console.log(response.status);
}
catch(error) {
console.log(error.message);
documents = JSON.stringify(error, null, 2);
};
return documents;
}
https://localhost:8080/getoauthaccesstoken?assertion=PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24lPjwvc2FtbDpBdHRyaWJ1dGU%2BPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPg%3D%3D
and here goes the resulting (truncated) bearer access token:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYXRlYW0taXN2ZW5nLmF1dGhlbnRpY2F0aW9uLnVzMTAuaGFuYS5vbmRltp6vkKH0PrZvg4e0otB59iG9MvCHV3mMEWzIxccIxrgEcem8_53l3fwzvMA5M",
"token_type": "bearer",
"refresh_token": "97c6b547a5ff4a40axxxxxxxxxxxxxx",
"expires_in": 3599,
"scope": "openid uaa.user",
"jti": "c903263a1bdb411595aa103xxxxxxx"
}
Good to know:
- OAuth2SAML token URL endpoint is the equivalent of the IDP endpoint. Calling into the OAuth2SAML token URL endpoint is triggering the IDP-initiated SAML SSO flow
- The OAuth audience is the equivalent of the service provider (= application)
Here goes the same using Postman to call into OAuth2SAML token URL endpoint:
Please note the format of the payload (body) must be set to x-www-form-urlencoded . A fairly common mistake being setting only Content-Type header to application/x-www-form-urlencoded.
The signed saml assertion is only valid for one hour. When its validity has expired you would receive the following response from calling into the OAuth recipient { |
ad e). Authenticating SAC API requests.
and last but not least it is time to consume the bearer access token for instance to embed a story in an iframe or get access to the list of stories your user has access to…
__________
Additional resources.
Extending SAP SuccessFactors:
|
SAP Analytics Cloud REST API.The SAP Analytics REST API gives us access to different assets in the SAC tenant . The SAC APIs but one are based on the RESTFUL API standard. Which means these API can be called from any programming language, framework or LCNC application securely over HTTPS. |
Hi Piotr,
Thanks for the article. I found it very comprehensive to guide user to walk through the SAML bearer assertion flow in SAC. When I followed the steps to fetch the access token for SAC, everthing goes well except for one question regarding the sample nodejs code in step ad c):
signedAssertion = encodeURIComponent(signedAssertion);
It seems it intends to encode the SAML beare assertion to an URI parameter. While passing the token to the function in ad d) getoauthaccesstoken(saml_bearer_assertion), inside the function it uses:
params.append("assertion", saml_bearer_assertion);
where I think the URLSearchParams will also take care of the encoding. Thus the double URL parameter encoding will cause problem.
However, if the encoded bearer token generated by step a c) is supposed to be appended to a request directly without the nodejs function in a ad) involved, it should be fine. Not sure if you have the same issue when using the 2 functions together directly to fetch the token. If so maybe we can note that the saml_bearer_assertion passed to getoauthaccesstoken(saml_bearer_assertion) do not need to be encoded as an url parameter in advance. 🙂
Thanks,
Manu
Hello Manu,
Indeed, there is one omission in this code snippet of the getoauthaccesstoken function above (from April 2021), namely:
params.append("assertion", decodeURIComponent(saml_bearer_assertion));
My original idea was to be able to pass a generated saml assertion as a query parameter, thus the encodeURIComponent was a must;
However, if the generated assertion is never meant to be used as a query parameter, then both encodeURIComponent and decodeURIComponent could be simply dropped.
kind regards; Piotr
PS. here goes a similar code snippet from another, more recent blog of mine
Thanks for clarification.