OAuth 2.0 Implementation Across ERPs: Salesforce, SAP, Oracle, D365, NetSuite, Workday
How do OAuth 2.0 implementations differ across ERPs - Salesforce, SAP, Oracle, Workday, Dynamics 365, NetSuite?
TL;DR
- Bottom line: Every major ERP supports OAuth 2.0 but implementations vary wildly -- Salesforce uses JWT bearer with connected apps, SAP uses communication arrangements via BTP, Oracle requires OCI IAM confidential apps, Dynamics 365 uses Entra ID app registrations, NetSuite recently added OAuth 2.0 alongside legacy TBA, and Workday requires ISU-linked API clients.
- Key limit: Token lifetimes range from 1 hour (Dynamics 365) to session-based (Salesforce, 2h default) to non-expiring refresh tokens (Workday) -- each requires different token management strategies.
- Watch out for: Certificate rotation deadlines -- SAP XSUAA managed certs default to 7 days validity, Dynamics 365 client secrets max 24 months, Salesforce connected app certs must be manually rotated.
- Best for: Integration architects comparing OAuth 2.0 grant types, token behavior, and implementation complexity across ERP platforms to choose the right auth flow for cross-system integrations.
- Authentication: Use JWT bearer (Salesforce), client credentials via communication arrangement (SAP), confidential app client credentials (Oracle), Entra ID client credentials (D365), authorization code (NetSuite), or refresh token flow with ISU (Workday) for server-to-server.
System Profile
This card compares OAuth 2.0 implementations across the six dominant cloud ERP platforms as of March 2026. Each vendor has taken a fundamentally different approach -- from Salesforce's self-contained connected app model to Microsoft's centralized Entra ID identity platform. This card focuses exclusively on OAuth 2.0 grant types, token mechanics, and implementation patterns.
| System | Role | OAuth 2.0 Provider | Primary Server-to-Server Flow |
|---|---|---|---|
| Salesforce | CRM/Platform | Built-in (Connected Apps) | JWT Bearer |
| SAP S/4HANA Cloud | ERP | SAP BTP / XSUAA | Client Credentials via Comm Arrangement |
| Oracle Fusion Cloud ERP | ERP | OCI IAM (ex-IDCS) | Client Credentials (confidential app) |
| NetSuite | ERP | Built-in | Authorization Code + Refresh |
| Dynamics 365 | ERP/CRM | Microsoft Entra ID | Client Credentials |
| Workday | HCM/Finance | Built-in (API Clients) | Refresh Token (with ISU) |
API Surfaces & Capabilities
Each ERP's OAuth 2.0 implementation determines which API surfaces you can access. Not all OAuth flows unlock all API surfaces.
| ERP | OAuth-Protected APIs | Non-OAuth APIs (legacy) | OAuth Required Since |
|---|---|---|---|
| Salesforce | REST, SOAP, Bulk, Streaming, Composite | Username-Password (deprecated) | Connected apps since 2013; JWT since 2016 |
| SAP S/4HANA Cloud | OData v4, SOAP, REST (via BTP) | Communication user + basic auth (deprecated) | 2020 (BTP mandatory for Public Cloud) |
| Oracle Fusion Cloud | REST, SOAP, BI Publisher, FBDI | Basic auth (deprecated for new integrations) | 2023 (OCI IAM mandatory) |
| NetSuite | REST, RESTlets, SuiteAnalytics Connect | TBA (OAuth 1.0) for SuiteTalk SOAP | Optional; TBA deprecated for new integrations 2027.1 |
| Dynamics 365 | Dataverse Web API (OData v4), custom APIs | S2S with Azure AD (same mechanism) | Always required (Azure AD since inception) |
| Workday | REST API, SOAP (WWS via OAuth) | ISU with basic auth (still supported) | Optional; recommended since v35.0+ |
Rate Limits & Quotas
Token Endpoint Limits
| ERP | Token Endpoint Rate Limit | Token Request Method | Notes |
|---|---|---|---|
| Salesforce | 1,800 token requests/hour per connected app | POST to /services/oauth2/token | Shared across all flows [src1] |
| SAP S/4HANA Cloud | Fair-use / throttled per subaccount | POST to XSUAA /oauth/token | No hard limit; excessive requests return 429 [src2] |
| Oracle Fusion Cloud | 60 token requests/minute per app | POST to OCI IAM /oauth2/v1/token | Hard limit per confidential app [src3] |
| NetSuite | Included in API concurrency limits | POST to /services/rest/auth/oauth2/v1/token | Shares request budget [src5] |
| Dynamics 365 | No published per-app token limit | POST to login.microsoftonline.com | Entra ID throttles at tenant level [src4] |
| Workday | Not published | POST to /ccx/oauth2/{tenant}/token | Governed by overall API throughput [src6] |
Rolling / Daily Limits
| ERP | Daily API Limit | Window | Edition Differences |
|---|---|---|---|
| Salesforce | 100,000 (Enterprise), 5M (Unlimited) | 24h rolling | Developer: 15,000 |
| SAP S/4HANA Cloud | Fair-use / throttled | Per-request | No edition tiering |
| Oracle Fusion Cloud | Throttled per service | Per-request | Burst limits vary by API surface |
| NetSuite | Concurrency-based (max 10-25) | Per-request | SuiteCloud Plus adds concurrency |
| Dynamics 365 | 6,000 req/5min/user, 60,000/5min/org | 5-min window | Same for all editions |
| Workday | Not published; throttled | Per-request | Monitored by Workday ops |
Authentication
OAuth 2.0 Grant Types by ERP
| Grant Type | Salesforce | SAP S/4HANA | Oracle Fusion | NetSuite | D365 | Workday |
|---|---|---|---|---|---|---|
| Authorization Code | Yes | Yes (via BTP) | Yes | Yes | Yes | Yes |
| Client Credentials | No (use JWT) | Yes | Yes | No | Yes | No (use refresh) |
| JWT Bearer | Yes (primary S2S) | Yes (X.509) | No | No | Yes (cert-based) | No |
| Refresh Token | Yes (web server) | No (client creds) | Yes (7d default) | Yes (7d rolling) | No (client creds) | Yes (primary S2S) |
| Device Flow | Yes | No | No | No | Yes | No |
| SAML Bearer | Yes | Yes | Yes | No | No | No |
| ROPC (Username-Password) | Deprecated | No | No | No | Yes (not recommended) | No |
Token Lifetime & Refresh Behavior
| ERP | Access Token Lifetime | Refresh Token Lifetime | Refresh Behavior | MFA Impact |
|---|---|---|---|---|
| Salesforce | Session timeout (default 2h, configurable) | Until revoked (web server flow) | New JWT per request for JWT bearer; refresh token for web server | MFA blocks ROPC; JWT/web server unaffected |
| SAP S/4HANA Cloud | 12 min (XSUAA default) | Not issued (client credentials) | Must request new token; short TTL by design | N/A -- communication user |
| Oracle Fusion Cloud | 1 hour (configurable) | 7 days (configurable up to 100 days) | Standard refresh; offline_access scope required | N/A for confidential apps |
| NetSuite | 60 min | 7 days (rolling) | Rolling -- new refresh token with each access token request | N/A for machine integrations |
| Dynamics 365 | 60-75 min | Not issued (client credentials) | Request new token using credentials; MSAL handles caching | N/A for app-only context |
| Workday | Short-lived (minutes) | Non-expiring (optional), or 14 days | Refresh returns new access + new refresh token | N/A -- ISU is system account |
Scopes Comparison
| ERP | Scope Model | Example Scope | Granularity |
|---|---|---|---|
| Salesforce | Predefined scope strings | api, refresh_token, full | Coarse -- per API surface |
| SAP S/4HANA Cloud | Communication scenario scopes | Defined per comm arrangement | Fine -- per business scenario |
| Oracle Fusion Cloud | OCI IAM scopes | urn:opc:idm:__myscopes__ | Medium -- per application |
| NetSuite | REST/SuiteAnalytics scope | rest_webservices | Coarse -- per API surface |
| Dynamics 365 | Entra ID .default scope | https://org.crm.dynamics.com/.default | Coarse -- all permissions |
| Workday | Domain security via ISU | Implicit from ISU permissions | Fine -- per security domain |
Authentication Gotchas
- Salesforce: JWT bearer issues a new access token per request; no token refresh. Connected app X.509 cert must be manually rotated -- no auto-rotation. Session timeout configurable by admin, never hardcode 2h. [src1]
- SAP: XSUAA managed certificates default to 7-day validity. Must explicitly set longer validity (up to 1 year). Token endpoint URL differs for mTLS:
https://{subdomain}.authentication.cert.{landscape}/oauth/token. [src2] - Oracle: IDCS merged into OCI IAM in 2023. Old docs reference IDCS URLs; new integrations must use OCI IAM domain URLs. Two confidential apps needed for Fusion Apps + OIC. [src3]
- NetSuite: OAuth 2.0 and TBA are different features. TBA deprecated for new integrations 2027.1. OAuth 2.0 does not require request signing (unlike TBA). [src5, src7]
- Dynamics 365:
.defaultscope grants all configured permissions -- cannot request subset at token time. Client secrets max 24 months. [src4] - Workday: Refresh tokens are ISU-scoped. If ISU terminated or security policies change, all tokens for that ISU become invalid immediately. [src6]
Constraints
- Salesforce JWT bearer flow cannot be used without a connected app. The connected app requires a digital certificate (self-signed for dev, CA-signed recommended for prod).
- SAP S/4HANA Cloud Public Edition restricts to communication arrangement scopes. Custom OAuth scopes not supported.
- Oracle Fusion Cloud basic auth deprecated for new integrations in 2023. All new integrations must use OAuth 2.0 via OCI IAM.
- NetSuite cannot use OAuth 2.0 for SuiteTalk SOAP. TBA (OAuth 1.0) remains the only option for SOAP integrations.
- Dynamics 365 client credentials flow does not issue refresh tokens. Must request new token when current expires (60-75 min).
- Workday does not support client credentials grant. Server-to-server uses refresh token flow linked to ISU. Without valid refresh token, no programmatic access.
Integration Pattern Decision Tree
START -- Choose OAuth 2.0 flow for ERP integration
|
+-- Is this server-to-server (no user interaction)?
| |
| +-- Salesforce? -> JWT Bearer Flow (connected app + X.509 cert)
| +-- SAP S/4HANA Cloud? -> Client Credentials via Communication Arrangement
| +-- Oracle Fusion Cloud? -> Client Credentials (confidential app in OCI IAM)
| +-- NetSuite? -> Auth Code + Refresh Token (REST only; SOAP needs TBA)
| +-- Dynamics 365? -> Client Credentials (Entra ID app registration)
| +-- Workday? -> Refresh Token Flow (API Client + ISU)
|
+-- Is this user-delegated (on behalf of user)?
| +-- Any ERP -> Authorization Code Flow
| +-- Salesforce -> Web Server Flow (with callback URL)
| +-- D365 -> Authorization Code + PKCE (public clients)
|
+-- Is this a CLI or device?
+-- Salesforce -> Device Flow
+-- Dynamics 365 -> Device Code Flow
+-- Others -> Not supported; use auth code with browser redirect
Quick Reference
| Capability | Salesforce | SAP S/4HANA | Oracle Fusion | NetSuite | Dynamics 365 | Workday |
|---|---|---|---|---|---|---|
| OAuth Provider | Built-in (Connected Apps) | SAP BTP / XSUAA | OCI IAM (ex-IDCS) | Built-in | Microsoft Entra ID | Built-in (API Clients) |
| Primary S2S Grant | JWT Bearer | Client Credentials | Client Credentials | Auth Code + Refresh | Client Credentials | Refresh Token |
| Certificate Auth | X.509 on connected app | X.509 via mTLS | X.509 assertion | Not supported | Certificate on app reg | Not supported |
| Token Lifetime | Session (~2h) | 12 min | 1h (configurable) | 60 min | 60-75 min | Minutes |
| Refresh Token | Web server only | No (client creds) | Yes (7d default) | Yes (7d rolling) | No (client creds) | Yes (non-expiring opt.) |
| Scope Model | Coarse (api, full) | Per-scenario | Per-app / custom | Per-API-surface | .default (all-or-nothing) | ISU security domains |
| Admin Portal | Setup > Connected Apps | BTP Cockpit | OCI Console > Identity | Setup > OAuth 2.0 | Entra Admin Center | Workday > API Client |
| SDK/Library | jsforce, simple-salesforce | SAP Cloud SDK | OCI SDK | Standard HTTP | MSAL | Standard HTTP |
| Setup Complexity | Medium | High | Medium | Low | Low | Medium |
| PKCE Support | Yes | No | No | Yes | Yes | No |
Step-by-Step Integration Guide
1. Salesforce: Obtain Token via JWT Bearer Flow
Create a connected app with a digital certificate, sign a JWT assertion, and exchange for an access token. [src1]
# Input: Connected app consumer key, X.509 private key PEM, Salesforce username
# Output: Access token and instance_url
import jwt, requests, time
CONSUMER_KEY = "3MVG9..."
PRIVATE_KEY = open("server.key", "r").read()
USERNAME = "[email protected]"
LOGIN_URL = "https://login.salesforce.com"
claim = {
"iss": CONSUMER_KEY, "sub": USERNAME,
"aud": LOGIN_URL, "exp": int(time.time()) + 300
}
assertion = jwt.encode(claim, PRIVATE_KEY, algorithm="RS256")
response = requests.post(f"{LOGIN_URL}/services/oauth2/token", data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion
})
token_data = response.json()
access_token = token_data["access_token"]
instance_url = token_data["instance_url"] # CRITICAL: use for all API calls
Verify: curl -H "Authorization: Bearer {token}" {instance_url}/services/data/v62.0/limits -> returns JSON with API limits
2. SAP S/4HANA: Obtain Token via Client Credentials
Request a token from the XSUAA endpoint using service key credentials. [src2]
# Input: XSUAA clientid, clientsecret, token URL from service key
# Output: Access token (12-minute TTL)
import requests
response = requests.post(TOKEN_URL, data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
})
access_token = response.json()["access_token"]
# Token expires in ~720 seconds (12 min) -- cache and refresh proactively
Verify: curl -H "Authorization: Bearer {token}" https://{host}/sap/opu/odata4/.../$metadata -> returns OData metadata
3. Oracle Fusion Cloud: Client Credentials via OCI IAM
Register a confidential application in OCI IAM, then request a token. [src3]
# Input: OCI IAM client_id, client_secret, token endpoint
# Output: Access token (1-hour default)
response = requests.post(TOKEN_URL, data={
"grant_type": "client_credentials",
"scope": "urn:opc:idm:__myscopes__"
}, auth=(CLIENT_ID, CLIENT_SECRET))
access_token = response.json()["access_token"]
Verify: curl -H "Authorization: Bearer {token}" https://{host}/fscmRestApi/resources/11.13.18.05/invoices?limit=1 -> returns invoice JSON
4. Dynamics 365: Entra ID Client Credentials
Register an app in Entra ID, grant D365 API permissions, request a token. [src4]
# Input: Entra tenant_id, client_id, client_secret, D365 org URL
# Output: Access token (60-75 minute lifetime)
response = requests.post(
f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": f"{D365_URL}/.default"
}
)
access_token = response.json()["access_token"]
# No refresh token -- request new token when expired
Verify: curl -H "Authorization: Bearer {token}" {D365_URL}/api/data/v9.2/WhoAmI -> returns system user ID
5. NetSuite: OAuth 2.0 Authorization Code + Refresh
Enable OAuth 2.0 in SuiteCloud, create integration record, use refresh tokens for ongoing access. [src5]
# Input: client_id, client_secret, refresh_token
# Output: New access + refresh token (rolling refresh)
response = requests.post(TOKEN_URL, data={
"grant_type": "refresh_token",
"refresh_token": REFRESH_TOKEN,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
})
data = response.json()
access_token = data["access_token"]
new_refresh_token = data["refresh_token"] # MUST store -- old token now invalid
Verify: curl -H "Authorization: Bearer {token}" https://{id}.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1 -> returns customer JSON
6. Workday: Refresh Token Flow with ISU
Register API client, link to ISU, generate non-expiring refresh token, exchange for access tokens. [src6]
# Input: client_id, client_secret, refresh_token, tenant
# Output: Access token + new refresh token
response = requests.post(
f"https://wd2-impl-services1.workday.com/ccx/oauth2/{TENANT}/token",
data={
"grant_type": "refresh_token",
"refresh_token": REFRESH_TOKEN,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
)
data = response.json()
access_token = data["access_token"]
new_refresh_token = data["refresh_token"] # Store for next exchange
Verify: curl -H "Authorization: Bearer {token}" https://wd2-impl-services1.workday.com/ccx/api/v1/{tenant}/workers?limit=1 -> returns worker data
Code Examples
cURL: Token Acquisition for Each ERP
# --- Salesforce JWT Bearer ---
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=${JWT_ASSERTION}"
# --- SAP S/4HANA Client Credentials ---
curl -X POST "${XSUAA_TOKEN_URL}" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=client_credentials"
# --- Oracle Fusion Client Credentials ---
curl -X POST "${OCI_IAM_TOKEN_URL}" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=client_credentials" \
-d "scope=urn:opc:idm:__myscopes__"
# --- Dynamics 365 Client Credentials ---
curl -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
-d "grant_type=client_credentials" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "scope=${D365_URL}/.default"
# --- NetSuite OAuth 2.0 Refresh ---
curl -X POST "https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token" \
-d "grant_type=refresh_token" \
-d "refresh_token=${REFRESH_TOKEN}" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}"
# --- Workday Refresh Token ---
curl -X POST "https://wd2-impl-services1.workday.com/ccx/oauth2/${TENANT}/token" \
-d "grant_type=refresh_token" \
-d "refresh_token=${REFRESH_TOKEN}" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}"
Error Handling & Failure Points
Common Error Codes
| Error Code | ERP(s) | Meaning | Resolution |
|---|---|---|---|
invalid_grant | All | Token/assertion rejected | Regenerate assertion or refresh token; check cert validity |
invalid_client | All | Client ID/secret wrong | Verify credentials match registered application |
unauthorized_client | Salesforce, D365 | App not authorized for this grant | Enable the specific OAuth flow in app config |
invalid_scope | Oracle, D365 | Scope not allowed | Add scope in OCI IAM or Entra admin center |
| 429 | SAP, Oracle | Rate limit on token endpoint | Cache tokens; exponential backoff |
INVALID_LOGIN | Salesforce | JWT subject user inactive | Verify integration user is active with API access |
expired_token | NetSuite, Workday | Refresh token expired | Re-authorize via authorization code flow |
Failure Points in Production
- Certificate expiry silently breaks integrations: Salesforce connected app certs and SAP XSUAA managed certs (7-day default!) expire without warning. Fix:
Implement certificate rotation monitoring with alerts at 30, 7, and 1 day before expiry. [src1, src2] - Refresh token rotation lost in transit: NetSuite and Workday issue new refresh tokens with each request. Crash between response and storage means permanent access loss. Fix:
Use transactional storage -- write new refresh token before using the access token. [src5, src6] - SAP 12-min token TTL breaks long-running jobs: Bulk migration jobs running 30+ minutes fail mid-execution. Fix:
Proactively refresh token before each batch in middleware. [src2] - D365 client secret silent expiry: Secrets expire after configured lifetime (max 24 months). Fix:
Use certificate auth for production, or automate rotation with Azure Key Vault. [src4] - Oracle IDCS-to-OCI IAM migration breaks URLs: Token endpoint format changed during merge. Fix:
Use OCI IAM domain URL format and test during migration window. [src3] - Workday ISU termination cascading failure: Terminated ISU invalidates all linked OAuth tokens immediately. Fix:
Never link ISUs to human employees; create dedicated service ISUs. [src6]
Anti-Patterns
Wrong: Storing OAuth tokens in plaintext config files
# BAD -- tokens and secrets in plaintext .env or config.json
config = {
"salesforce_token": "00D5e000000XXXX!AR...",
"sap_client_secret": "xsuaa-secret-value-here",
"d365_client_secret": "A1bC2dE3f..."
}
# Committed to Git, visible in CI logs, no rotation tracking
Correct: Use a secrets manager with rotation support
# GOOD -- secrets from vault with automatic rotation
import boto3 # or azure.keyvault, hashicorp vault
secrets_client = boto3.client("secretsmanager")
def get_secret(name):
return secrets_client.get_secret_value(SecretId=name)["SecretString"]
salesforce_key = get_secret("erp/salesforce/private_key")
sap_secret = get_secret("erp/sap/client_secret")
# Rotation policies enforced at vault level
Wrong: Requesting a new token for every API call
# BAD -- token request per API call wastes quota and adds latency
def get_data(query):
token = get_new_token() # ~200ms per call
return requests.get(url, headers={"Authorization": f"Bearer {token}"})
Correct: Cache tokens and refresh proactively
# GOOD -- cache token, refresh before expiry
class TokenCache:
def __init__(self):
self._token, self._expires_at = None, 0
def get_token(self):
if time.time() > (self._expires_at - 60):
self._token, self._expires_at = self._fetch_new_token()
return self._token
Wrong: Hardcoding ERP instance URLs
# BAD -- breaks across environments, pod migrations
API_URL = "https://na139.salesforce.com/services/data/v62.0/query"
Correct: Parse instance_url from token response
# GOOD -- dynamic instance URL from OAuth response
data = requests.post(login_url + "/services/oauth2/token", data={...}).json()
instance_url = data["instance_url"] # Salesforce tells you where to call
Common Pitfalls
- Assuming all ERPs support client_credentials: Salesforce and Workday do not. Salesforce uses JWT bearer; Workday uses refresh tokens with ISU. Fix:
Abstract token acquisition per ERP using adapter pattern. [src1, src6] - Not storing Salesforce instance_url: The URL can change during pod migrations. Fix:
Always use instance_url from each token response. [src1] - Ignoring SAP's 12-minute token TTL: Middleware caching tokens for 1 hour fails 80% of the time with SAP. Fix:
Always respect expires_in from token response. [src2] - Using D365 client secrets in production: They expire (max 24 months) with weaker security. Fix:
Use certificate-based auth (client_assertion) in production. [src4] - Losing NetSuite rolling refresh tokens: Each refresh invalidates the old token. Fix:
Persist new refresh token transactionally before making API calls. [src5, src7] - Using personal Workday accounts instead of ISU: Employee departure breaks all integrations. Fix:
Always create dedicated ISUs for each integration. [src6]
Diagnostic Commands
# --- Salesforce: Test JWT Bearer token ---
python3 -c "
import jwt, time, requests
claim = {'iss':'${KEY}','sub':'${USER}','aud':'https://login.salesforce.com','exp':int(time.time())+300}
a = jwt.encode(claim, open('server.key').read(), algorithm='RS256')
r = requests.post('https://login.salesforce.com/services/oauth2/token',
data={'grant_type':'urn:ietf:params:oauth:grant-type:jwt-bearer','assertion':a})
print(r.status_code, r.json().get('access_token','ERROR')[:20]+'...')
"
# --- SAP: Test XSUAA client credentials ---
curl -s -o /dev/null -w "%{http_code}" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=client_credentials" "${XSUAA_TOKEN_URL}"
# Expected: 200
# --- Oracle: Test OCI IAM token ---
curl -s -o /dev/null -w "%{http_code}" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=client_credentials&scope=urn:opc:idm:__myscopes__" \
"${OCI_IAM_TOKEN_URL}"
# Expected: 200
# --- Dynamics 365: Test Entra ID ---
curl -s -o /dev/null -w "%{http_code}" \
-d "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&scope=${D365_URL}/.default" \
"https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token"
# Expected: 200
# --- NetSuite: Test OAuth 2.0 refresh ---
curl -s -o /dev/null -w "%{http_code}" \
-d "grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \
"https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token"
# Expected: 200
# --- Workday: Test refresh token ---
curl -s -o /dev/null -w "%{http_code}" \
-d "grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \
"https://wd2-impl-services1.workday.com/ccx/oauth2/${TENANT}/token"
# Expected: 200
Version History & Compatibility
| ERP | OAuth Milestone | Date | Status | Notes |
|---|---|---|---|---|
| Salesforce | JWT Bearer flow GA | 2016 | Current | No breaking changes since introduction |
| Salesforce | Device flow GA | 2020 | Current | CLI/IoT devices |
| SAP S/4HANA Cloud | XSUAA OAuth mandatory (Public Cloud) | 2020 | Current | Communication arrangements required |
| SAP | X.509 mTLS for XSUAA | 2022 | Current | Alternative to client secret |
| Oracle Fusion | IDCS OAuth GA | 2018 | Migrating to OCI IAM | IDCS merged into OCI IAM 2023 |
| Oracle Fusion | Basic auth deprecated (new integrations) | 2023 | Current | Existing basic auth still works |
| NetSuite | OAuth 2.0 GA | 2021 | Current | REST APIs only |
| NetSuite | TBA deprecated for new integrations | 2027.1 | Planned | Existing TBA continues |
| Dynamics 365 | Entra ID rebrand (from Azure AD) | 2023 | Current | No protocol changes |
| Dynamics 365 | Federated identity credentials | 2024 | Current | Cross-cloud workloads (GCP, AWS) |
| Workday | OAuth 2.0 API clients GA | 2020 | Current | Requires ISU linkage |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Comparing OAuth 2.0 flows across multiple ERPs | Need detailed single-ERP OAuth walkthrough | System-specific auth card |
| Deciding which grant type for server-to-server ERP integration | Need non-OAuth auth comparison (SAML, certs, basic) | erp-authentication-comparison |
| Building a universal integration adapter for multiple ERPs | Need rate limits or API capabilities (not auth) | System-specific API capability cards |
| Planning certificate/secret rotation across ERPs | Need SSO/SAML for end-user login | Vendor SSO documentation |
Cross-System Comparison
| Capability | Salesforce | SAP S/4HANA | Oracle Fusion | NetSuite | Dynamics 365 | Workday | Notes |
|---|---|---|---|---|---|---|---|
| OAuth Provider | Built-in | SAP BTP/XSUAA | OCI IAM | Built-in | Entra ID | Built-in | SF, NS, WD self-contained |
| S2S Grant | JWT Bearer | Client Credentials | Client Credentials | Auth Code+Refresh | Client Credentials | Refresh Token | No universal grant |
| Token TTL | ~2h (session) | 12 min | 1h | 60 min | 60-75 min | Minutes | SAP shortest by far |
| Refresh Token | Web server only | N/A | 7d default | 7d rolling | N/A | Non-expiring opt. | Rolling (NS/WD) vs static |
| Certificate Auth | X.509 on app | X.509 mTLS | X.509 assertion | No | Certificate on app | No | SAP requires mTLS endpoint |
| Setup Complexity | Medium | High | Medium | Low | Low | Medium | SAP highest (BTP dependency) |
| SDK Support | jsforce | SAP Cloud SDK | OCI SDK | HTTP only | MSAL | HTTP only | MSAL best abstraction |
| Admin Consent | Profile pre-auth | Comm arrangement | IAM admin grant | Admin consent | Entra admin consent | ISU policies | Each model different |
| Multi-tenant | Per-org app | Per-subaccount | Per-IAM-domain | Per-account-id | Per-Entra-tenant | Per-tenant | URL patterns all differ |
| PKCE Support | Yes | No | No | Yes | Yes | No | Required for public clients |
Important Caveats
- Token lifetimes and rate limits are subject to change with each ERP release. Values reflect March 2026 configurations. Always verify against current vendor docs.
- SAP S/4HANA on-premise uses a completely different OAuth implementation (via SAP Gateway) vs cloud (BTP/XSUAA). This card covers cloud only.
- Oracle's IDCS-to-OCI IAM migration is ongoing. Some tenants still use IDCS endpoints; check your specific tenant.
- NetSuite OAuth 2.0 requires SuiteCloud feature enabled by admin. Not enabled by default on all accounts.
- Dynamics 365 Entra ID (formerly Azure AD) rebranding changed no protocol behavior, but old docs may reference Azure AD URLs.
- Workday token endpoint URLs vary by data center (wd2, wd3, wd5) and environment type (impl vs prod). Always use the exact URL from your tenant config.