Hi all,
I was struggeling to access Successfactors with OAuth2 in Python. No idea why SAP does not use the more useful OAuth2 flows Authorization Code and ClientCredentials for API access (Feb 2023).
I have found one GitHub Repository that helped a lot. Unfortunately the Python code was written for version 2.x and the Saml assertion changed. So I changed it to my purpose. You probably should be able to adapt it to other languages like NodeJs or Java. For me, one important information was how the Saml Token looks like.
Here, you can find the (old) Github Repository that helped me.
https://github.com/mtrdesign/python-saml-example
Dependencies for Python 3.9
These are the dependencies that I used.
pytz==2022.6
requests==2.28.1
cpython
xmlsec
lxml
How to Create and Sign a Saml Assertion
#sf_saml.py
import xmlsec
from lxml import etree
import pytz
from datetime import datetime, timedelta
def generate_assertion(sf_root_url, user_id, client_id):
issue_instant = datetime.utcnow().replace(tzinfo=pytz.utc)
auth_instant = issue_instant
not_valid_before = issue_instant - timedelta(minutes=10)
not_valid_after = issue_instant + timedelta(minutes=10)
audience = 'www.successfactors.com'
context = dict(
issue_instant=issue_instant.isoformat(),
auth_instant=auth_instant.isoformat(),
not_valid_before=not_valid_before.isoformat(),
not_valid_after=not_valid_after.isoformat(),
sf_root_url=sf_root_url,
audience=audience,
user_id=user_id,
client_id=client_id,
session_id='mock_session_index',
)
return SAML_ASSERTION_TEMPLATE.format(**context)
def sign_assertion(xml_string, private_key):
key = xmlsec.Key.from_memory(private_key, xmlsec.KeyFormat.PEM)
root = etree.fromstring(xml_string)
signature_node = xmlsec.tree.find_node(root, xmlsec.Node.SIGNATURE)
sign_context = xmlsec.SignatureContext()
sign_context.key = key
sign_context.sign(signature_node)
return etree.tostring(root)
SAML_ASSERTION_TEMPLATE = """
<saml2:Assertion
IssueInstant="{issue_instant}" Version="2.0"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<saml2:Issuer>{client_id}</saml2:Issuer>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">{user_id}</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotOnOrAfter="{not_valid_after}"
Recipient="{sf_root_url}/odata/v2" />
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="{not_valid_before}"
NotOnOrAfter="{not_valid_after}">
<saml2:AudienceRestriction>
<saml2:Audience>{audience}</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement AuthnInstant="{issue_instant}"
SessionIndex="{session_id}">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute Name="api_key">
<saml2:AttributeValue xsi:type="xs:string">{client_id}</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="use_username">
<saml2:AttributeValue xsi:type="xs:string">true</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue></DigestValue>
</Reference>
</SignedInfo>
<SignatureValue/>
</Signature>
</saml2:Assertion>
"""
How to Catch Bearer Token
#filename: sap_sf.py
import requests
import sf_saml
import base64
class SFSession(object):
def __init__(self, server_url, company_id, oauth_client_id, private_key, sf_user_id):
self.server_url = server_url
self.idp_url = self.url_for('/oauth/idp')
self.access_token_url = self.url_for('/oauth/token')
self.odata_url = self.url_for('/odata/v2')
self.company_id = company_id
self.oauth_client_id = oauth_client_id
self.private_key = private_key
self.sf_user_id = sf_user_id
def url_for(self, relative_url):
return self.server_url + '/' + relative_url.lstrip('/')
def get_local_assertion(self):
"""
Generate and sign the SAML assertion ourselves.
"""
user_id = self.sf_user_id
unsigned_assertion = sf_saml.generate_assertion(
sf_root_url=self.server_url,
user_id=user_id,
client_id=self.oauth_client_id
)
signed = sf_saml.sign_assertion(unsigned_assertion, self.private_key)
return base64.b64encode(signed).decode("ascii").replace('\n', '')
def get_access_token(self, assertion=None):
if not assertion:
assertion = self.get_local_assertion()
token_request = dict(
client_id=self.oauth_client_id,
company_id=self.company_id,
grant_type='urn:ietf:params:oauth:grant-type:saml2-bearer',
assertion=assertion,
new_token='true'
)
response = requests.post(self.access_token_url, data=token_request)
print(response.text)
token_data = response.json()
return (token_data['access_token'], token_data['expires_in'])
Bringing Everything Together
from sap_sf import SFSession
import requests
company = "your company id"
client_id = "the oauth api (app) id from successfactors"
private_key = "your private key as pem" #I created a key with openssl. The one from successfactors did not work for me
username = "the username in SF to access the API"
session = SFSession("https://api2.successfactors.eu", company, client_id, private_key, username)
bearer = session.get_access_token()
#print(bearer)
bearer = "Bearer " + bearer[0]
headers = {'Authorization': bearer}
url=f'https://api2.successfactors.eu/odata/v2/User?$format=JSON'
response = requests.get(url,headers=headers, cert=(), timeout=(200))
I hope this helps someone out there.