![]() | 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:
|
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...
|
This is an overview of the configuration steps that are needed to set up OAuth 2.0 in Employee Centr...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
|
|
![]() | 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. |
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... |
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;
}
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>
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;
}
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"
}
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"
]
}
}
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. |
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:
|
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) If you try to pass them as form parameters client_id and client_secret you will get 401 error! Otherwise, all form parameters must be x-www-form-urlencoded. |
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.
|
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: |
![]() |
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
11 | |
11 | |
8 | |
7 | |
7 | |
6 | |
6 | |
6 | |
6 | |
6 |