Why OAuth 2.0, Not Basic Auth?
Traditional Basic Authentication sends your username and password (base64 encoded, trivially decodable) with every single request. If that password leaks, an attacker has full account access forever. You'd have to reset your password system-wide.
OAuth 2.0 solves this by introducing an intermediate token:
- Stateless: The server doesn't store session state. The token itself (a JWT, internally) proves your identity and permissions.
- Scoped: You can issue a token that grants only read access (im.read) or write access (im.write), not both. You follow the principle of least privilege — your nightly credit integration doesn't need permission to delete participants.
- Time-limited: The token expires in ~30 minutes. Even if it leaks, the window of exposure is small.
- Revokable: You can revoke a token from the admin console without changing any passwords.
- Auditable: SAP logs which API client made which requests, so you can trace the origin of any data change.
This is why every modern API (Google Cloud, AWS, Salesforce, Stripe, SAP SuccessFactors IM) uses OAuth 2.0 or a similar token-based system.
The Client Credentials Grant Type
OAuth 2.0 has multiple "grant types" — different ways to obtain a token depending on your use case. For server-to-server integrations (CRM to IM, ERP to IM, no human involved), you use the client credentials grant type.
Why? Because there's no user. A nightly credit push from your CRM to IM happens while the office is closed. No one is sitting at a desk clicking "approve." Your integration is the principal — it has its own identity (client_id), its own secret (client_secret), and its own permissions (scopes).
Grant Type Comparison
For reference, here are the main OAuth grant types and when to use them:
| Grant Type | Use Case | Involves User? | When to Use |
|---|---|---|---|
| Client Credentials | Server-to-server integration | No — the client itself is the principal | Nightly CRM → IM credit push, HR sync, BI extract. Your integration runs on a schedule, no user login needed. |
| Authorization Code | Web app with user login | Yes — user grants permission | A SaaS app where users log in with their company credentials. The app needs to act on their behalf. |
| Implicit | Single-page app (deprecated) | Yes — user grants permission | Not recommended anymore. Was used for browser-based apps; unsafe. |
| Refresh Token | Long-lived access | Not directly; extends existing session | When your access_token expires, use the refresh_token to get a new one without the user re-authenticating. You don't need this for client credentials (just fetch a new token when expired). |
For your ICM integrations, you will always use client credentials. No users, no authorization code flow, no implicit. Just client_id + client_secret → token → API calls.
Registering an API Client
Before you can authenticate, you need to register your integration as an "API client" in the SAP SuccessFactors IM admin console. This is a one-time setup. You'll receive credentials that your code will use forever (until you rotate them for security).
Steps to Register an API Client
- Log in to SAP SuccessFactors IM as a tenant admin. You need admin-level permissions to access the API registration section.
- Navigate to Admin → API Management or API Clients. The exact path varies by SAP SuccessFactors IM version, but it's always in the admin area. Look for "API" or "Integrations."
- Click "Create New API Client" or "Register Application."
- Fill in the form:
- Client Name: A human-readable name. Example: "CRM-to-IM-Nightly-Sync" or "Salesforce-Credits-Integration". This is for your reference; it shows up in audit logs.
- Client Type: Select "Confidential" (your integration has a secret) or "Public" (rarely used). For server-to-server, always "Confidential."
- Scopes: Check the permissions this client needs. For a credit push: check "im.write". For reading results for payroll: check "im.read". Never check both unless you truly need both — principle of least privilege.
- Redirect URI: Leave blank for client credentials. This is only used for the auth code flow (user login scenarios). You don't need it.
- Save/Create. SAP generates and displays your credentials once. Copy them immediately.
What You Receive
After registration, you receive:
Client ID: client_abc123def456 Client Secret: s3cr3tP@ssw0rd_xyz789_DO_NOT_SHARE Scopes: im.read, im.write Tenant ID: tenant-us-east-1
Store these securely. The client_secret is as sensitive as a password — treat it like one. Never commit it to version control. Use environment variables.
The Token Request
Now that you have credentials, here's how to get an access token. You POST to the OAuth endpoint with your client_id, client_secret, and the grant_type.
The HTTP Request
curl -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=client_abc123def456&client_secret=s3cr3tP@ssw0rd_xyz789&scope=im.read im.write" \ "https://api.sap.com/oauth/token"
Key points:
- HTTP Method: POST
- Content-Type: application/x-www-form-urlencoded (form data, not JSON)
- Body parameters:
- grant_type: Always "client_credentials" for server-to-server
- client_id: Your API client ID from registration
- client_secret: Your API client secret from registration
- scope: Space-separated list of scopes you need (e.g., "im.read" or "im.read im.write")
The Token Response
SAP responds with a JSON object:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjbGllbnRfYWJjMTIzZGVmNDU2IiwiaWF0IjoxNzEyOTk5OTk5fQ.abc123xyz789",
"token_type": "Bearer",
"expires_in": 1800,
"scope": "im.read im.write"
}Fields:
| Field | Meaning | How to Use It |
|---|---|---|
| access_token | Your bearer token | Attach to the Authorization header of every API request: Authorization: Bearer {this value} |
| token_type | Always "Bearer" | Tells you how to use the token — prefix it with "Bearer " in the Authorization header |
| expires_in | Seconds until token expires | For SAP SuccessFactors IM, typically 1800 (30 minutes). Store current time + expires_in to know when to refresh. |
| scope | Confirmed scopes | Which permissions this token has. Should match what you requested. |
Using the Bearer Token
Now that you have an access_token, attach it to every API request in the Authorization header:
curl -X GET \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Accept: application/json" \ "https://api.sap.com/successfactors/im/participants?$filter=status eq 'ACTIVE'"
The header format is:
Authorization: Bearer {access_token}Important: Always include "Bearer " (with the space). "Bearer" is the token type; it tells the API how to interpret the token.
Token Caching: The Critical Pattern
Here's a common mistake: fetching a new token for every API request. If you do that in a nightly credit push with 10,000 records:
- 10,000 POST requests to /oauth/token (separate from the 10,000 credit POSTs)
- Each token request takes ~500ms — that's 5,000 seconds (1.4 hours) just for tokens
- Your rate limit quota is exhausted by token requests alone
- The API throttles you for making too many requests too fast
Instead, cache the token in memory. Request a new token only when the current one is about to expire.
Simple Caching Logic
- On startup: Fetch a token, store it in a variable, and also store the expiry timestamp (current_time + expires_in seconds).
- Before each API call: Check if current_time >= (expiry_timestamp - 60 seconds). The 60-second buffer is a safety margin — refresh a bit early to avoid race conditions.
- If expired: Fetch a new token, update the cached token and expiry timestamp, and proceed with the API call.
- If not expired: Use the cached token.
Python Implementation: Token Manager Class
Here's a production-ready token manager that handles caching and refresh:
import requests import time import os class TokenManager: """Manages OAuth 2.0 tokens with automatic refresh.""" def __init__(self, client_id, client_secret, scope): self.client_id = client_id self.client_secret = client_secret self.scope = scope self.oauth_endpoint = "https://api.sap.com/oauth/token" # Cache state self.access_token = None self.expires_at = 0 # Unix timestamp when token expires def _fetch_new_token(self): """Request a new access token from OAuth endpoint.""" data = { "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret, "scope": self.scope, } resp = requests.post(self.oauth_endpoint, data=data) resp.raise_for_status() result = resp.json() self.access_token = result["access_token"] self.expires_at = time.time() + result["expires_in"] return self.access_token def get_token(self): """Get a valid access token, refreshing if necessary.""" current_time = time.time() # Refresh if expired or within 60 seconds of expiry if current_time >= (self.expires_at - 60): return self._fetch_new_token() return self.access_token # Usage: token_mgr = TokenManager( client_id=os.getenv("ICM_CLIENT_ID"), client_secret=os.getenv("ICM_CLIENT_SECRET"), scope="im.read im.write" ) # First call: fetches a new token token = token_mgr.get_token() print(f"Got token: {token[:20]}...") # Second call (within 30 min): returns cached token token = token_mgr.get_token() print(f"Reused cached token: {token[:20]}...") # Later, when used in an API call: headers = {"Authorization": f"Bearer {token_mgr.get_token()}"} resp = requests.get("https://api.sap.com/successfactors/im/participants", headers=headers)
Scopes: Principle of Least Privilege
When you register your API client, you specify which scopes (permissions) it needs. SAP SuccessFactors IM typically has two main scopes:
| Scope | What It Allows | Use Case |
|---|---|---|
| im.read | Read-only access. Fetch participants, plans, results, quotas, pipeline run history. | BI tools pulling data for dashboards. Payroll systems reading results. Analytics integrations. |
| im.write | Write access. Create/update credits, quotas, trigger pipeline runs, update participants. | CRM pushing daily sales credits. Planning tools updating quotas. Workflow automation. |
Follow the principle of least privilege: Your nightly credit push integration only needs "im.write". Do not give it "im.read" unless you're actually reading data. If a token leaks, the attacker can only write credits, not read participant data or results.
Similarly, a BI reporting integration only needs "im.read". It has no business writing anything.
Security Best Practices
Never Hardcode Credentials
Do not put client_id or client_secret in your code. Ever. Use environment variables:
import os client_id = os.getenv("ICM_CLIENT_ID") client_secret = os.getenv("ICM_CLIENT_SECRET") if not client_id or not client_secret: raise ValueError("Missing ICM_CLIENT_ID or ICM_CLIENT_SECRET environment variables")
Set environment variables on your server before the script runs (in systemd, cron, Kubernetes secrets, AWS Lambda env vars, etc.).
Never Log Access Tokens
Do not log the token itself. If your logs are accidentally exposed (email, Slack, GitHub issues), the token is now public and usable by an attacker.
# BAD — logs the full token token = token_mgr.get_token() print(f"Using token: {token}") # NEVER DO THIS # GOOD — logs only metadata, no token token = token_mgr.get_token() print(f"Retrieved access token (expires in ~30min)") # Safe
Rotate Credentials Periodically
Even with the best practices, credentials can leak. Rotate your client_secret every 90 days (or per your security policy). SAP allows you to create a new secret, update your code to use it, and then delete the old secret. No downtime required.
Monitor Token Usage
Check your API audit logs periodically. Look for:
- Unusual request rates (millions of requests from one client, while normal is thousands)
- Unexpected endpoints (your read integration shouldn't be writing credits)
- Failed authentication attempts (401 errors spike = possible credential breach)
Error Handling: Common Token Errors
Here's how to handle common authentication errors:
| Error / Status Code | Cause | Fix |
|---|---|---|
| 400 Bad Request (on token endpoint) | Malformed request. Missing grant_type, client_id, etc., or typo in field names. | Check the body format. Look at the error message in the response body for which field is wrong. |
| 401 Unauthorized (on token endpoint) | Invalid client_id or client_secret. Credentials are wrong or the API client was deleted. | Verify your credentials. Regenerate in the admin console if needed. Check env var is set correctly. |
| 403 Forbidden (on token endpoint) | Client is registered but not granted the requested scope. | Edit the API client in the admin console to add the missing scope. |
| 401 Unauthorized (on API endpoint) | Token expired mid-job or was revoked. | Clear the token cache, fetch a new token, and retry. |
| 429 Too Many Requests (on token endpoint) | You're fetching tokens too fast (likely bug — not caching). | Implement caching. Only fetch a token when the current one is about to expire. |
Complete Example: Token Manager with Error Handling
Here's a production-grade token manager that includes retry logic and error handling:
import requests import time import os import logging logger = logging.getLogger(__name__) class TokenManager: """OAuth token manager with caching, refresh, and retry logic.""" def __init__(self, client_id, client_secret, scope, max_retries=3): self.client_id = client_id self.client_secret = client_secret self.scope = scope self.oauth_endpoint = "https://api.sap.com/oauth/token" self.max_retries = max_retries self.access_token = None self.expires_at = 0 def _fetch_new_token(self): """Fetch token with retry logic.""" data = { "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret, "scope": self.scope, } for attempt in range(self.max_retries): try: resp = requests.post(self.oauth_endpoint, data=data, timeout=10) # 401, 403 are not retriable — credential issues if resp.status_code in [401, 403]: logger.error(f"Auth failed: {resp.status_code} - {resp.text}") raise Exception(f"OAuth auth failed: {resp.status_code}") resp.raise_for_status() result = resp.json() self.access_token = result["access_token"] self.expires_at = time.time() + result["expires_in"] logger.info("Token refreshed successfully") return self.access_token except (requests.RequestException, Exception) as e: if attempt < self.max_retries - 1: wait_time = 2 ** attempt # Exponential backoff: 1s, 2s, 4s logger.warning(f"Token fetch failed (attempt {attempt+1}), retrying in {wait_time}s: {e}") time.sleep(wait_time) else: logger.error(f"Token fetch failed after {self.max_retries} attempts: {e}") raise def get_token(self): """Get token, refreshing if necessary.""" current_time = time.time() if current_time >= (self.expires_at - 60): self._fetch_new_token() return self.access_token
Testing Your OAuth Setup
Before you use the token in production, test the flow manually:
# Step 1: Get a token curl -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_SECRET&scope=im.read" \ "https://api.sap.com/oauth/token" \ | python3 -m json.tool # Copy the access_token from the response, then: # Step 2: Use the token in an API call curl -X GET \ -H "Authorization: Bearer YOUR_TOKEN_HERE" \ "https://api.sap.com/successfactors/im/participants?$top=1" \ | python3 -m json.tool
If Step 2 returns a 200 with participant data, your OAuth setup works.
Next Steps
You now understand OAuth 2.0 and have a token manager that handles caching and refresh. In Lesson 3, you'll learn how to use that token to read data from the API — GET requests, pagination, filtering, and performance optimization.