Successfactors API access with OAuth2 SamlBearerAssertion and Python

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert