Technical Articles
ABAP acting as a Resource Server. App2App integration with OAuth2SAMLBearerAssertion flow.
|
We are so used to say “Alexa play Spotify” and the likes and no longer realise how much manual work it takes to set up the ubiquitous OAuth2SAML2Bearer Assertion flow with a vanilla SAP ABAP backend system. This instalment will walk you through this challenge! |
Disclaimer:
|
Employee Central Payroll (ECP)
The initial task was to set up the SuccessFactor Employee Central integration with the SFSF ECP (Employee Central Payroll) twin via outbound OAuth. In a nutshell, ECP (PDF link) is an ABAP payroll engine with S/4HANA OP that can be accessed:
Looking at the overview of the required and documented steps (aka Using OAuth 2.0 to Integrate Employee Central and Employee Central Payroll) the task seemed relatively straightforward. Let’s see…
|
Putting it all together.
A short reminder of the steps to accomplish:
This is an overview of the configuration steps that are needed to set up OAuth 2.0 in Employee Central Payroll (ECP).Please note.
|
Last but not least…
|
Step 1. x509 key pair – Creating OAuth X509 Keys
Even if my hands got a little bit rusty with SAP GUI I was able to go through steps 2 – 4 relatively smoothly, as depicted below. Step 2. SAML2 – Configuring OAuth Identity Provider
Step 4. SOAUTH2 – Registering OAuth Client
|
And finally I was ready to test this “ubiquitous” authorisation flow (initially using postman). But all I was getting was an 401 error (=logon error). [Please goto troubleshooting section for detailed explanation.]
And while contemplating my bad luck I happened to come across the following community post. The answer provided by Wolfgang Janzen is spot-on!
When I read through it I said to myself – this is it. The missing S_SCOPE object must be the culprit!
|
Let’s get it done!
![]() |
After having completed the whole ABAP server side configuration with SAML2 / SU01 / SOAUTH2 / PFCG it is time to create the saml bearer assertion and then call into the ABAP OAuth client to obtain a bearer access token. The bearer access token will carry all the necessary authorisations to enable a remote and password-less access to ODATA resources. |
Step 5. Configuring Outbound OAuth
At this stage we shall deviate from the SFSF/ECP documentation. Instead of relying on the SFSF Security Center intrinsic outbound destination facility to generate the saml assertion and request an ECP OAuth client to yield the access bearer token, we shall be generating the saml assertion (sub-steps 5.1a and 5.1b) and then calling into ECP OAuth client to yield the access bearer token (sub-steps 5.2 and 5.3) programmatically! on our own. Why ? This may be needed because:
|
Please note, SAP BTP destination service can help generate the saml bearer assertion in either use case (even if the server or application have no public internet exposure)! You may refer to my sibling blog if you want to skip 5.1a and 5.1b and get the generation of saml bearer assertion done and dusted… |
1a. Generate SAML bearer assertion.
- A saml assertion identifies the resource owner!
- The produced saml assertion is both base64- and URL- encoded.
- The nodejs code snippet below is provided “as-is”.
Please pay attention to the nameIdentifierFormat is use.
It must be set to ‘urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified’ rather than ‘urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified’
|
// tokenUrl is saml assertion recipient
// audienceUrl is saml assertion audience
// clientId is saml assertion client_id
// userName is saml assertion NameID with the urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified tag
//
async function generateSAMLBearerAssertion(tokenUrl, audienceUrl, clientId, userName, use_email=false) {
const cert = '-----BEGIN CERTIFICATE-----\nMIIFGzCCAwMCBGBb1dwwDQYJKoZIhvcNAQELBQAwUjELMAkGA1UEBhMCVVMxDDAK\nBgNVBAoMA1NBUDEVMBMGA1UECwwMYi\ne3pZsV0QGgSCMZ8kNQobunEPnfkXysLhUvWzniY0UI9uLY7F9934p3PLnZAJOhLJ\nO0X6cHCFbMC+6GxXTdisQVivIOKUURdaHVX6B270SUDiP6TDPApn9E+IaISzPRpk\nXT6c0QNVYg37DBU/qhSN\n-----END CERTIFICATE-----\n';
const key = '-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDFz/eQv30tj5oC\nLjT1Im7OtVAVo6mB/wQbEpbOh3LSI8h/f00fwLMJ/uQ3nYHiwqsElTvKA0h0B5tm\n79w/Z1FBx/vrjqrbKvQEFVQ/zH3YVEsdBPkn4C7iMvumwMECrgbhNTFOAAViJGRqkeRIArXvScbLwq62ViESgOIOU8TdR0n3fachXehZLgRUTa2IGI6zKuVSaXLq\nWBgr0UKz5CLYl4kvZ8ECFbb/I8psoa5LSxBTGdiZznqgLKnImxU1WDSA2xlKJy7J\nAwx8lLYgANSJ7qkKPgPR/t5ZHrx/plY=\n-----END PRIVATE KEY-----\n';
var options = {
//cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
//key: fs.readFileSync(__dirname + '/test-auth0.key'),
cert: Buffer.from(cert, 'utf-8'),
key: Buffer.from(key, 'utf-8'),
issuer: 'quovadis/ateam-isveng',
lifetimeInSeconds: 3600,
attributes: {
'client_id': clientId,
},
includeAttributeNameFormat: true, //false,
// uid: 'b94a5e98-386a-4ce4-b4a2-80a48c2e2222',
sessionIndex: '_faed468a-15a0-4668-aed6-3d9c478cc8fa',
// https://wiki.scn.sap.com/wiki/display/Security/Security+Token+Service+Configuration
authnContextClassRef: 'urn:none',
//'urn:oasis:names:tc:SAML:2.0:ac:classes:x509',
//'urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession',
nameIdentifierFormat: use_email === true
? 'urn:oasis:names:tc:SAML:2.0:attrname-format:email'
: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
nameIdentifier: userName,
recipient: tokenUrl,
audiences: audienceUrl,
// signatureAlgorithm: rsa-sha256',
// digestAlgorithm: 'sha256',
signatureNamespacePrefix: 'ds',
//prefix: '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;
}
1b. Decode SAML Bearer Assertion into XML format.
Please make a note of the saml assertion Recipient below.
It must have the ?sap-client=<ABAP CLIENT NUMBER> query parameter attached to it.
I recommend you use the token_uri from the downloaded OAuth client configuration
as the Recipient of the saml assertion.
Failure to do so may result in the saml assertion rejection!
|
<?xml version="1.0"?>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="_Jl7DgoG8CvmWEGWn6BhWhafqdGb6U8eW" IssueInstant="2021-05-25T15:07:46.193Z">
<script/>
<saml:Issuer>quovadis/ateam-isveng</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_Jl7DgoG8CvmWEGWn6BhWhafqdGb6U8eW">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>zoO33FgZbZQVeJA3HNalK4sGaHPnN6SKGNeV1AioZ***</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>d+XUFfzCGeM7IBS9RkTMfB8arTBl5Mnr1Ip5D9RM2xmdRaoWV2nmhLtMejDRHCKzEQBIDp+e2djtQQmqOZWM3XasHW1gVrec033xO9+xjuk1tfEqJEl9YwAhSu/DGUrvT06l11t0q/JnoNFSGI55DtzThzaeJbuCZqS51TOGM8ioFRJsjBYeI4FgopngdbtDB69MVq90jj44z9njuL4YXMTUiqQ59tzs4Ih/zjH36emsViV2VXnNVk6hzoHNyzxw7Snb70Fmp+XPrGkhIIc1xQlS63P/Qr7CwcCo6z0=</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIFGzCCAwMCBGBb1dwwDQYJKoZIhvcNAQELBQAwUjELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA1NBUsLhUvWzniY0UI9uLY7F9934p3PLnZAJOhLJO0X6cHCFbMC+6GxXTdisQVivIOKUURdaHVX6B270SUDiP6TDPApn9E+IaISzPRpkXT6c0QNVYg37DBU/qhSN</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">QUOVADIS_ECP</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2021-05-25T17:07:46.193Z" Recipient="https://<host>.<domain>:<port>/sap/bc/sec/oauth2/token?sap-client=666"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2021-05-25T15:07:46.193Z" NotOnOrAfter="2021-05-25T17:07:46.193Z">
<saml:AudienceRestriction>
<saml:Audience>QJ9_666</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2021-05-25T15:07:46.193Z" SessionIndex="_faed468a-15a0-4668-aed6-3d9c478cc8fa">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:none</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<saml:Attribute Name="client_id" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">QUOVADIS</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
2. OAuth 2.0 Access Token Request
After receiving a SAML assertion, which identifies the resource owner user, the OAuth 2.0 client will send an access token request directly at the Gateway system where the OData service is hosted on to get OAuth 2.0 access token.
The userName is the name of the resource owner. It must exist and have necessary scopes assigned in its profile. |
//
async function ecp_oauth_access_token(event, userName, use_email=false) {
const credentials_EC_ADM_OAUTH = { // QJ9_666: EC_ADM_OAUTH
client: {
id: 'EC_ADM_OAUTH',
secret: '<EC_ADM_OAUTH system user password>'
},
auth: {
tokenHost: 'https://<host>.<domain>:<port>/sap/bc/sec',
tokenPath: 'oauth2/token'
},
options: {
authorizationMethod: 'body'
}
};
var scope1 = 'HRSFEC_ECP_INFO_SRV_0001 HRSFEC_INFOTYPE_SRV_0001';
const credentials_EC_ESS_OAUTH = { // QJ9_666: EC_ESS_OAUTH
client: {
id: 'EC_ESS_OAUTH',
secret: '<EC_ESS_OAUTH system user password>'
},
auth: {
tokenHost: 'https://<host>.<domain>:<port>/sap/bc/sec',
tokenPath: 'oauth2/token'
},
options: {
authorizationMethod: 'body'
}
};
var scope2 = 'HRSFEC_PAY_OVERVIEW_SRV_0001 HRSFEC_PAYCTRL_REC_SRV_0001';
const credentials_QUOVADIS_ECP = { // QJ9_666: QUOVADIS_ECP
client: {
id: 'QUOVADIS_ECP',
secret: '<QUOVADIS_ECP system user password>'
},
auth: {
tokenHost: 'https://<host>.<domain>:<port>/sap/bc/sec',
tokenPath: 'oauth2/token'
},
options: {
authorizationMethod: 'body'
}
};
var scope4 = 'ZUI_TRAVELAPPROVERMMY_0001 ZUI_TRAVELPROCESSORMMY_0001';
let credentials = credentials_EC_ADM_OAUTH;
let scope = scope1;
let audienceUrl = 'QJ9_666';
if (typeof (event.extensions.request.query.oauthclientid) !== 'undefined') {
oauthclientid = event.extensions.request.query.oauthclientid;
console.log('oauthclientid: ', oauthclientid);
if (oauthclientid === 'EC_ESS_OAUTH' || oauthclientid === '2') {
credentials = credentials_EC_ESS_OAUTH;
scope = scope2;
}
else
if (oauthclientid === 'QUOVADIS_ECP' || oauthclientid === '4') {
credentials = credentials_QUOVADIS_ECP;
scope = scope4;
}
}
let tokenUrl= credentials.auth.tokenHost + '/' + credentials.auth.tokenPath;
saml_bearer_assertion = await generateSAMLBearerAssertion(
tokenUrl+ '?sap-client=666',
audienceUrl,
credentials.client.id,
userName,
use_email);
console.log('saml_bearer_assertion=', decodeURIComponent(saml_bearer_assertion));
const options = {
headers: {
'Accept': 'application/json',
'Authorization': 'Basic ' + btoa(credentials.client.id + ':' + credentials.client.secret)
}
};
var params = new URLSearchParams();
params.append('client_id', credentials.client.id);
params.append("scope", scope);
params.append('grant_type', "urn:ietf:params:oauth:grant-type:saml2-bearer");
params.append("assertion", decodeURIComponent(saml_bearer_assertion));
let documents;
try {
const response = await axios.post(tokenUrl + '?sap-client=666', params , options);
documents = JSON.stringify(response.data, null, 2 /*identation */);
console.log(documents);
console.log(response.status);
}
catch(error) {
console.log(error.message);
documents = JSON.stringify(error, null, 2 /*identation */);
};
return documents;
}
3. OAuth 2.0 Access Token Response
After successful authentication and authorization check for the OAuth client and the resource owner the token endpoint inside the AS ABAP will send an OAuth 2.0 bearer access token back.
Here go examples of successful responses:
a. EC_ADM_OAUTH client - admin services
{
"access_token": "-hY-kcapHuuvsU9KHYiuPe0U6p8Xt1rhMr5F4eqkjdRD1***",
"token_type": "Bearer",
"expires_in": "3600",
"scope": "HRSFEC_ECP_INFO_SRV_0001 HRSFEC_INFOTYPE_SRV_0001"
}
b. EC_ESS_OAUTH client - self services
{
"access_token": "-hY-kcapHuuvsd4swh8vxmtVoTf3R187pIQXkV0KX57BQ***",
"token_type": "Bearer",
"expires_in": "3600",
"scope": "HRSFEC_PAYCTRL_REC_SRV_0001 HRSFEC_PAY_OVERVIEW_SRV_0001"
}
c. QUOVADIS_ECP client - bespoke travel services
{
"access_token": "-hY-kcapHuuvseHGU63vyPYrQxq7diXlXooux8SFxMQ4v***",
"token_type": "Bearer",
"expires_in": "3600",
"refresh_token": "-hY-kcapHuuvseHGU64PyO5uqYEOUaWH2-XBrfeCi1S5Y***",
"scope": "ZUI_TRAVELAPPROVERMMY_0001 ZUI_TRAVELPROCESSORMMY_0001"
}
4. OData Service Request and Response
The OAuth client uses the access token in the HTTP bearer authorization header to access the OData service (ZUI_TRAVELPROCESSORMMY).
Request: GET https://<host>.<domain>:<port>/sap/opu/odata/sap/ZUI_TRAVELPROCESSORMMY/?sap-client=666
let url = 'https://<host>.<domain>:<port>/sap/opu/odata/sap/ZUI_TRAVELPROCESSORMMY' ;
try {
const options = {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type': 'application/atomsvc+xml',
}
};
const response = await axios.get(url + '/?sap-client=666', 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);
};
Response: |
{
"d": {
"EntitySets": [
"SAP__Currencies",
"SAP__UnitsOfMeasure",
"TravelAgency",
"xDMOxI_Airport",
"Airline",
"FlightConnection",
"Passenger",
"Flight",
"Supplement",
"SupplementText",
"Country",
"Currency",
"I_DraftAdministrativeData",
"Booking",
"BookingSupplement",
"Travel"
]
}
}
Conclusion.
The official SAP help documentation, namely Using OAuth 2.0 to Integrate Employee Central and Employee Central Payroll describes quite accurately all the necessary configuration steps. Still, the OAuth setup on NW ABAP side can be challenging as of such – as there are many tiny details to pay attention to (as depicted in the amber coloured sections along this blog post). Last but not least, I hope you have enjoyed reading this blog…Please leave your questions and comments in the add comment section below. |
__________
Troubleshooting.
Let me share a hint on how to easily establish a connection with any ABAP backend system using a .sapc formatted connection file.
Good to know:
|
The main troubleshooting note is 1688545 – OAuth 2.0 Server in AS ABAP Troubleshooting.
And the transaction SA38 with SEC_TRACE_ANALYZER is your friend.
HTTP Trace with SA38 (or SE38) SEC_TRACE_ANALYZER Good to know:
Here goes the trace for the 401 logon error I encountered initially:
Here goes the rationale of the above 401 error: To generate access token for client_credentials grant type you must pass the Client ID and Client Secret as a Basic Authentication header (Base64-encoded) Otherwise, all form parameters must be x-www-form-urlencoded. |
__________
Appendix
Using OAuth 2.0 from a Web Application with SAML Bearer Assertion Flow
Configuration Guide for this scenarioTo get this scenario running several configuration steps have been performed. Click on the links below to see the step-by-step descriptions for the various components involved. All configuration steps are based on the leave request example.
|
OAuth 2.0 Resource Owner Authorization Configuration
Create OAuth 2.0 client user and add authorization object S_SCOPEWith OAuth 2.0, the access to a resource / service is not done by a user directly, but by an OAuth client. The client logs on to Gateway and sends the user’s access token to the service. Therefore, as a first step we need to create the OAuth 2.0 client in SAP Gateway. This client is not an app, it is a user account of type system that the actual client app will use to log on to SAP Gateway. To do this run transaction SU01 and create a new system user (user type: system). With this technical user, the OAuth client app can log on to SAP Gateway. In theory this is enough to allow access to the SAP Gateway service. The client could now send an access token and its client secret to be authorized. As this is not secure enough, the client must not only authenticate itself with User ID and Password or X509 but must also have the authorization to access the service with the given scope and client id. Within the SAP Backend the authorization object S_SCOPE is used for this purpose. To enable the OAuth client user to act as an OAuth client, you must assign and configure the authorization object S_SCOPE. This is done by creating a new role, adding S_SCOPE object and assigning the role to the user. Run transaction PFCG and create a new role. For our example we call the role ZOAUTHSERVICE. The following storyboards describe ZOAUTHSERVICE role configuration steps: |
__________
Additional resources:
|
This is amazing, Piotr.
I just stumbled across it because of another problem with saml bearer assertion provider setup.
Great job!
Hello Johannes Bacher,
Thanks for your comment;
Indeed I wrote this rather lengthy instalment with the idea it can help people implementing OAuth with saml bearer assertion with NW ABAP.
kind regards; Piotr
Hi, Trying to still get a hang of Oauth concept
I see in your example .. the oauth client_secret is nothing but the system user password
is that how it is expected to be in the context of ABAP server acting as Oauth Server / Service provider?
How is this better than a regular user/pwd based connection. The whole point of oauth was to shield the password from being shared with clients
Thx
Hello,
When it comes to OAuth2 clients they typically support a number of so called grants or authentication flows, for instance client credentials or saml bearer assertion flow;
The whole point of OAuth2 is to exchange a resource access grant against a resource access bearer token.
Any platform that allows to create OAuth2 clients will offer a method to generate client id and client secret. With ABAP you do it by creating a technical user with a system generated password. Then you can proceed and create an OAuth2 client with the token issuance endpoint and scopes etc.
But what really matters is how you will be calling into the OAuth2 server token issuance endpoint. By merely exchanging the client credentials or by doing a full fledged IDP delegated flow on behalf of a user.
The client credentials flow is typically used for an automated service to service type of communication whereas the saml bearer assertion flow is used whenever a business user identity needs to be propagated. In either case the goal is to have an unmanned and secured access to a resource.
In this blog I deal with the latter flow (OAuth2SAML2BearerAssertion) only as it is pretty much the only way to propagate users identities in a secured way. I hope that helps; regards;Piotr
PS. You may want to read through the Golden selection of help resources at the end of this blog