How to authenticate against SAP SF OData endpoints in .NET?
This article explains how to obtain OAuth access tokens for SAP SuccessFactors OData endpoints using the SAML 2.0 Bearer Assertion flow.
I wouldn't normally write about SAP. It's one of the TLAs that gives me shivers, and I'd rather stay away from it.
But the corporations do love it - almost as much as they love Workday - so deal with it we must!
I'd also normally post my articles on Tuesdays - but this one was so painful to finish that it'll roll out when it's done.
Note that I'm not a SAP consultant. I'm just working for a systems integrator / ISV that needs to integrate with SAP, and who's almost completely at the mercy of the SAP teams of the customers. Everything I say or write should be understood as being more akin to scars - essentially notes written in blood, etched on my skin during the implementation project turned bad.
Or maybe just treat this article as an opinion piece that has a sprinkle of useful information about setting up authentication against SuccessFactors in it.
A funny side effect of dealing with SAP's documentation and APIs is that it makes me appreciate Microsoft's efforts (as uncoordinated as they might be) much more. 😅
Background
Table of Contents
- Background
- Assertions
- Should I use the SAP SuccessFactors SDK or roll my own?
- Authenticating to SAP SuccessFactors OData using SAML 2.0 Bearer Assertions
- Why this matters
- Conceptual overview
- Tenant and IdP prerequisites
- Token request — exact HTTP details you will send
- How to generate the offline assertion
- Method 1: Programmatically create and sign the assertion in your backend
- Example .NET architecture and implementation tips
- Method 2: Pre-generated assertions with rotation
- Generate SAML assertions using SAP-provided Offline tool
- Troubleshooting checklist
- Final notes and next steps
Table of Contents
- Background
- Assertions
- Should I use the SAP SuccessFactors SDK or roll my own?
- Authenticating to SAP SuccessFactors OData using SAML 2.0 Bearer Assertions
- Why this matters
- Conceptual overview
- Tenant and IdP prerequisites
- Token request — exact HTTP details you will send
- How to generate the offline assertion
- Method 1: Programmatically create and sign the assertion in your backend
- Example .NET architecture and implementation tips
- Method 2: Pre-generated assertions with rotation
- Generate SAML assertions using SAP-provided Offline tool
- Troubleshooting checklist
- Final notes and next steps
We originally figured out how the auth works with the "API Users" (service accounts in 2025, for real...), how to generate assertions (because SAP is too good for certificates) and how to eventually jump from the SuccessFactors OData endpoints to the Learning APIs (because apparently it makes sense to require authentication before you can call an endpoint for authenticating...), finally got everything up and running... And I was happy to bury the project!
But we keep getting requests to build new stuff, so we've had to dust off our old notes and maintain our documentation. I figured it's also a good idea to distill some of that into a blog article.
Assertions
A SAML assertion is an XML document that an Identity Provider (IdP) sends to a Service Provider (SP) to confirm a user's authentication and authorization. It contains information about who the user is, how they were authenticated, and their permissions for a specific application or service.
Below is a prime example. As we have yet to meet a SAP deployment where the Identity Provider it uses is successfully configured to serve us functional online SAML 2.0 assertions, we must resort to offline assertions instead - essentially generate them ourselves.
The options are to generate long-lived assertions using SAP's tooling (which you of course get to build yourself), or build the generation logic into your application (SAP are kind enough to offer a sample in Java).
I don't like getting angry emails from lawyers (and SAP has a lot of them), so I'm not sharing the tooling or the samples on my blog directly. But they are available for download from the official KB.
... if you are able to log in with an SAP account with enough permissions to read the rest of the KB, that is.
I am not. I do have a partner account, but it doesn't have enough permissions to read this highly confidential KB article on how to authenticate against their APIs.
Security by obscurity in its finest. By obscuring the documentation for authenticating, no less!
As a side note, working through this endeavour was an eye-opening. It suddenly became clear to me, why SAP consultants are paid so well - dealing with this every day must be soul-crushing.
Should I use the SAP SuccessFactors SDK or roll my own?
What SDK? 😅
If you're doing this in .NET, you're on your own, so you roll your own.
Authenticating to SAP SuccessFactors OData using SAML 2.0 Bearer Assertions
Alright. Time to finally get started on the actual guide!
Everything below is based on my and my team's experiences building integrations with SAP SuccessFactors and having to deal with the different authentication configurations.
I'll concentrate on the requirements to generate the assertions yourself, because I find it unlikely you (or me) will ever get them generated for us by someone else.
And of course, this is a server-side implementation. If you're doing something purely on the front-end, maybe you will be able to configure something less unholy - but with the experience I have so far, I doubt that.
The guide assumes you have administrative access to your IdP or (more likely) can kindly request someone to generate signing certificate to register with SuccessFactors and share with you. Additionally, because this is 1990s, you will need a service account - called "API User" in SAP - with enough permissions to access all of the OData endpoints you're planning to call.
Unless you're able to use the SAP IdP to generate the assertion (which has been deprecated for like 10 years), these are hard requirements and you won't be able to proceed without.
Why this matters
Let's establish a few facts first:
- SuccessFactors requires OAuth access tokens for OData APIs. This is good.
- For server-to-server scenarios without user interaction, a SAML 2.0 Bearer Assertion is a secure, standards-based way to obtain tokens: you present a signed assertion to the tenant token endpoint, and the tenant returns an access token if everything validates
- Offline assertions can be long-lived and you can generate them yourself. They are obviously less secure than short-lived online assertions (generated dynamically by a trusted identity provider like Entra ID)
Then a couple of opinions:
- Online assertions will probably not work
- After you figure out the authentication, the rest of the work is (probably) smooth sailing
Conceptual overview
The flow in a nutshell:
- Produce or obtain a signed SAML 2.0 assertion that the SuccessFactors tenant can validate.
- POST the assertion to the tenant OAuth token endpoint with grant_type set to the SAML bearer grant.
- The tenant validates the assertion (signature, audience, timestamps) and issues an OAuth access token.
- Use Authorization: Bearer to call OData endpoints.
Sounds simple, right?
And it is, for the most part - we'll just need to get the prerequisites to actually generate the assertion!
Key properties of a correct assertion:
- Properly signed using a private key whose public certificate is registered/trusted by the tenant
- Audience/Recipient/tokenUrl values match tenant expectations (often the token endpoint URL)
- Meaningful validity window (expireInMinutes in SAMLAssertion.properties and in the resulting NotBefore/NotOnOrAfter) to limit exposure
How you should get a valid assertion:
- Using IdP (deprecated and not recommended)
- Establishing trust between Entra ID and SAP SuccessFactors IdP and generating the assertions for the trusted Entra ID service principal (I've yet to see a working configuration, and SAP themselves describe this "experimental" in some of their documentation)
- Using offline assertion (was supposed to be deprecated but SAP gave up and this is the way to go)
Sound ridiculous so far? Great! You haven't lost your sense yet.
If you're brave enough, let's continue.
Tenant and IdP prerequisites
From the SuccessFactors tenant side you'll need:
- Tenant token endpoint URL (usually https:///oauth/token, but as we know, can follow any format and SAP might change it seemingly randomly)
- Here's a non-exhaustive list of temporarily valid addresses that could change at any point: list of data center URLs for BizX OData destination configuration
- company_id and client_id values the tenant expects for your integration.
- You need to get these from the customer - company_id they already know, but client_id they'll need to register for you
- Upload or register the public X.509 certificate that SuccessFactors will use to verify the assertion signature.
- You'll need the private key to generate the assertion. Public key is used by SuccessFactors to validate your assertion.
Token request — exact HTTP details you will send
- Endpoint
- Headers
- Content-Type: application/x-www-form-urlencoded
- Form fields
- company_id: (string) configured in tenant
- client_id: (string) registered client id
- assertion: the SAML assertion (raw XML or base64-encoded depending on tenant)
- grant_type: urn:ietf:params:oauth:grant-type:saml2-bearer
- Sample curl (raw assertion):
-
curl -v -X POST "https://your-tenant.example.com/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "company_id=YOUR_COMPANY&client_id=YOUR_CLIENT&assertion=PASTE_ASSERTION_XML_HERE&grant_type=urn:ietf:params:oauth:grant-type:saml2-bearer"
-
- Response (success)
- JSON containing at minimum: access_token, token_type (bearer), expires_in (seconds).
Note: Confirm whether your tenant wants the raw XML or base64 encoded assertion. This varies. You'll figure it out by trial and error, because it won't be documented anywhere.
How to generate the offline assertion
Just a reminder - online assertion would be better, but chances are the environment is not properly configured to generate assertions using a 3rd-party IdP. Using SAP IdP for online assertions is deprecated, unsafe, dangerous, not available anymore, and would probably have worked like a charm.
And that's all I have to say about that. On to offline assertions we go!
Method 1: Programmatically create and sign the assertion in your backend
High-level steps:
- Construct a SAML assertion with required elements: Issuer, Subject, Conditions (NotBefore/NotOnOrAfter), AudienceRestriction (audience == token endpoint or specific URI), and optionally SubjectConfirmation.
- Sign the assertion using an X509 private key (X509SigningCredentials or SignedXml).
- Serialize the signed XML and pass it to the token endpoint in the assertion parameter.
.NET-specific notes
- Libraries: System.IdentityModel.Tokens.Saml2 (part of Windows Identity Foundation components) or a third-party SAML library.
- Signing: use X509Certificate2 (PFX) loaded from a secure store and use it to sign the assertion.
- Canonicalization/signature details: XML signature placement, namespaces and canonicalization affect validation. Test until you get it right.
Best practices:
- Use a short NotBefore/NotOnOrAfter window (1–5 minutes), account for clock skew.
- Store private keys in Azure Key Vault (or similar), never in source control.
Example .NET architecture and implementation tips
Some ideas on how to implement this in your code.
- SAPSecureSettings.cs (POCO) - holds tenant host, company_id, client_id, API endpoint and a pointer to assertion or signing key info.
public class SAPSecureSettings
{
public string TenantHost { get; set; }
public string CompanyId { get; set; }
public string ClientId { get; set; }
public string ApiEndpoint { get; set; }
public string SigningKeyPath { get; set; } // Path to PFX or key identifier
}
- ISettingsService — abstracts access to configuration and secrets (read non-sensitive settings from appsettings, secrets from Key Vault or environment).
public interface ISettingsService
{
Task<SAPSecureSettings> GetSAPSettingsAsync();
}
public class SettingsService : ISettingsService
{
private readonly IConfiguration _config;
// Assume Key Vault or secret store integration here
public SettingsService(IConfiguration config)
{
_config = config;
}
public Task<SAPSecureSettings> GetSAPSettingsAsync()
{
return Task.FromResult(new SAPSecureSettings
{
TenantHost = _config["SAP:TenantHost"],
CompanyId = _config["SAP:CompanyId"],
ClientId = _config["SAP:ClientId"],
ApiEndpoint = _config["SAP:ApiEndpoint"],
SigningKeyPath = _config["SAP:SigningKeyPath"]
});
}
}
- ISapTokenService / SapTokenService — core responsibilities:
- Obtain or generate a SAML assertion as needed.
- POST to token endpoint and parse JSON response.
- Cache token using IMemoryCache or a distributed cache with expiry set to expires_in minus a safety window (e.g., 300s).
- Provide DoWithAccessTokenAsync(Func<string, Task
> action) that handles token acquisition, calls the action, and on Unauthorized forces refresh and retries once.
public interface ISapTokenService
{
Task<string> GetAccessTokenAsync(bool forceRefresh = false);
Task<T> DoWithAccessTokenAsync<T>(Func<string, Task<T>> action);
}
public class SapTokenService : ISapTokenService
{
private readonly IMemoryCache _cache;
private readonly ISettingsService _settings;
private readonly HttpClient _httpClient;
public SapTokenService(IMemoryCache cache, ISettingsService settings, HttpClient httpClient)
{
_cache = cache;
_settings = settings;
_httpClient = httpClient;
}
public async Task<string> GetAccessTokenAsync(bool forceRefresh = false)
{
var settings = await _settings.GetSAPSettingsAsync();
var cacheKey = $"{settings.CompanyId}_{settings.ClientId}";
if (!forceRefresh && _cache.TryGetValue(cacheKey, out string token))
{
return token;
}
// Generate SAML assertion (simplified; implement full signing logic)
var assertion = GenerateSamlAssertion(settings);
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("company_id", settings.CompanyId),
new KeyValuePair<string, string>("client_id", settings.ClientId),
new KeyValuePair<string, string>("assertion", assertion),
new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:saml2-bearer")
});
var response = await _httpClient.PostAsync($"https://{settings.TenantHost}/oauth/token", content);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(json);
var expiry = TimeSpan.FromSeconds(tokenResponse.ExpiresIn - 300);
_cache.Set(cacheKey, tokenResponse.AccessToken, expiry);
return tokenResponse.AccessToken;
}
public async Task<T> DoWithAccessTokenAsync<T>(Func<string, Task<T>> action)
{
try
{
var token = await GetAccessTokenAsync();
return await action(token);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
var token = await GetAccessTokenAsync(forceRefresh: true);
return await action(token);
}
}
private string GenerateSamlAssertion(SAPSecureSettings settings)
{
// Implement SAML assertion generation and signing using System.IdentityModel.Tokens.Saml2 or similar
// Load X509Certificate2 from secure store, construct assertion with proper Issuer, Subject, Audience, etc.
// Sign and serialize to XML string
throw new NotImplementedException("Implement SAML assertion generation and signing.");
}
}
public class TokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
}
Method 2: Pre-generated assertions with rotation
This works, but is not very safe:
- Generate assertions offline, store them securely, and rotate them frequently.
- Use when runtime signing is impossible but still requires careful rotation setup.
Generate SAML assertions using SAP-provided Offline tool
- Download Maven and make sure it's configured to your PATH variable: Download Maven
- Download and extract this: SAP note attachment
- cd to the extracted directory
- Modify "SAMLAssertion.properties" file:
- tokenUrl = the auth endpoint of your tenant
- clientId = application identifier you get from the customer
- userId = API user account
- userName = leave it empty
- privateKey = private key for the certificate that is registered with SuccessFactors
- expireInMinutes = Pretty self-explanatory, I suppose
- run this in terminal:
mvn compile exec:java -Dexec.args="SAMLAssertion.properties"
- Copy the generated, base64-encoded assertion
... and you're done! Prepare to rotate the assertion regularly.
Troubleshooting checklist
An overarching theme is that you will fail on many steps. First you'll fail yourself into eventual triumph in development, and then after a while stuff will randomly break in production.
But what to do?
If token request fails or APIs return 401:
- Examine token endpoint response for error details (but do not log secrets! And don't ask me why I'm writing it out explicitly.)
- Confirm the assertion's Audience / Recipient matches tenant expectations.
- Check NotBefore / NotOnOrAfter values and server clocks.
- Confirm company_id and client_id values are correct and active.
- Confirm whether tenant expects raw XML or base64 for the assertion parameter (by trying both - that's the only way).
- Verify HTTP Content-Type is application/x-www-form-urlencoded.
The most typical ways to mess up:
- XML signature canonicalization or namespace issues.
- Wrong certificate uploaded to tenant.
- Time window misconfiguration leading to NotBefore/NotOnOrAfter rejection.
- Offline assertion expiration (check the possibly base64-encoded assertion for NotOnOrAfter value - and verify your expireInMinutes in SAMLAssertion.properties)
Final notes and next steps
Start with a manual curl/Postman test using a validated assertion. Once you get a token, implement a SapTokenService that requests and caches tokens. Add retry logic for 401 and a safety window for token expiration.
Comments
No comments yet.