IAS Setup Guide β BTP Subaccount Authentication Options β
This guide covers authentication configuration for the CAP tutorials platform on a BTP subaccount. It presents two approaches β Option A (simple, recommended) and Option B (IAS-managed) β and explains when each is appropriate.
Architecture Options β
Option A: Default SAP ID Service Trust (Recommended for Dev/POC) β
BTP Subaccount β trusts β SAP ID Service (default, automatic)
β User authenticates at accounts.sap.com (existing browser session = transparent SSO)
β XSUAA issues JWT with sub from SAP ID ServicePros: Zero configuration, transparent SSO for anyone with an SAP account, no IAS tenant needed. Cons: No centralized identity management, no conditional auth, no social logins.
Option B: IAS as Trust Point (Production with Corporate IDP) β
BTP Subaccount β trusts β IAS (custom trust)
β IAS authenticates user directly (SAP Universal ID = same credentials)
β OR IAS delegates to a Corporate IDP you control (Azure AD, Okta, ADFS)
β XSUAA issues JWT with sub from IASPros: Centralized identity, MFA, conditional auth, social logins, corporate IDP delegation. Cons: Requires IAS tenant configuration, identity federation setup for user data migration.
Important: IAS does NOT proxy to SAP ID Service (accounts.sap.com) as a SAML 2.0 corporate IDP. SAP ID Service does not accept arbitrary SP registrations, so the SAML handshake fails. Users authenticate directly against IAS using the same SAP Universal ID credentials β the user store is shared within the global account.
Option A: Default SAP ID Service (Quick Setup) β
This is the simplest path. Every new BTP subaccount has SAP ID Service configured as the default trust automatically.
Verify Trust Configuration β
- BTP Cockpit β Navigate to your subaccount
- Security β Trust Configuration
- Confirm SAP ID Service (Default identity provider) shows Available for User Logon: Yes
That's it. Users accessing your application are redirected to accounts.sap.com. If they have an active browser session (most SAP developers do), authentication is transparent β no login form.
Deploy and Test β
- Deploy the MTA (see
CLAUDE.mdfor deploy commands) - Access the approuter URL β you should be silently authenticated
- If prompted, log in with your SAP account β subsequent visits will be transparent
Assign Role Collections β
- BTP Cockpit β Subaccount β Security β Role Collections
- Assign "Tutorials Admin" (or other role collections from xs-security.json) to your user
- Use the user's email or SAP ID as the identifier
Option B: IAS as Trust Point (Full Setup) β
Use this when you need centralized identity management, corporate IDP delegation, conditional authentication, MFA enforcement, or social logins.
Prerequisites β
- BTP Global Account with entitlement to SAP Cloud Identity Services
- Admin access to IAS admin console
- Understanding that users authenticate directly against IAS (not proxied through SAP ID Service)
Step 1: Obtain Your IAS Tenant β
Most BTP global accounts include one IAS tenant at no additional cost.
- BTP Cockpit β Global Account β Security β Trust Configuration
- Check if an IAS tenant is already listed under "Custom Identity Provider for Applications"
- If not, go to Entitlements β search for "Cloud Identity Services" β add to a subaccount
- Create a service instance of plan
defaultoradditional_tenant - Note the IAS admin console URL:
https://<tenant-id>.accounts.ondemand.com/admin
Note: A single IAS tenant can serve multiple subaccounts.
Step 2: Establish Trust from Subaccount to IAS β
- BTP Cockpit β Navigate to your subaccount
- Security β Trust Configuration
- Click Establish Trust
- Select your IAS tenant from the dropdown (auto-discovered within the global account)
- Leave defaults:
- Name: Auto-populated
- Available for User Logon: Yes
- Click Save
At this point, both SAP ID Service (default) and IAS (custom) are active. Users will see an IDP picker.
Step 3: Decide on IDP Availability β
| Goal | Configuration |
|---|---|
| IAS only (no picker) | Set Default SAP ID Service β "Available for User Logon: No" |
| Both active (picker shown) | Leave both enabled |
| Default SAP ID + IAS as backup | Keep default as primary, IAS as option |
Recommendation for production: Set SAP ID Service to "Not Available for User Logon" so users go directly to IAS without a picker. But only do this after verifying login works through IAS (Step 5).
Step 4: Configure Corporate IDP in IAS (If Applicable) β
This step is only for corporate IDPs you control (Azure AD, Okta, ADFS, PingFederate). Do NOT use this for SAP ID Service β it won't work.
If your users authenticate via a corporate IDP:
- IAS Admin Console β Identity Providers β Corporate Identity Providers
- Click Create
- Enter a display name (e.g., "Company Azure AD")
- Select type: SAML 2.0 Compliant or OpenID Connect
- Upload/enter the corporate IDP's metadata
- On the corporate IDP side: Register your IAS tenant as a trusted SP/relying party
- IAS SP metadata:
https://<tenant-id>.accounts.ondemand.com/saml2/metadata
- IAS SP metadata:
- Configure Identity Federation:
- Set to "Use Identity Provider user store" if you want pass-through identity
- Set to "Use Identity Authentication user store" for email-based matching
Why SAP ID Service doesn't work here: The "SAML 2.0 Compliant" type requires you to register IAS as a service provider on the remote IDP. SAP ID Service (accounts.sap.com) is a public IDP that does not accept SP registrations from arbitrary tenants. The SAML AuthnRequest from IAS gets rejected.
Step 5: Verify Login Through IAS β
- Access your application URL
- You should be redirected to your IAS tenant's login page
- Log in with your SAP Universal ID credentials (same email/password as accounts.sap.com)
- Verify you reach the application successfully
SAP Universal ID and IAS: Within the same global account, the IAS user directory includes all users who have SAP Universal ID accounts. The same credentials work β this is not "proxying" to SAP ID Service; it's the same underlying identity directory.
Step 6: Configure Subject Name Identifier (For User Data Migration) β
If migrating user data from a system that used SAP ID Service directly, ensure the sub claim matches:
- IAS Admin Console β Applications & Resources β Applications
- Select the application representing your BTP subaccount (auto-registered by Step 2)
- Go to Trust β Subject Name Identifier
- Configure:
- Source: Identity Directory
- Primary Attribute: User UUID (or Login Name, depending on what the legacy system used)
Why this matters:
| Setting | sub claim | Impact |
|---|---|---|
| User UUID | IAS-generated UUID | May differ from SAP ID Service sub |
| Login Name | User's email/login | Matches if legacy used email as sub |
The CAP platform uses Users.uuid = JWT sub for lookups. Test with a known user before migrating data.
Step 7: Verify Identity Continuity β
# Decode the JWT from a login through IAS, then check:
npx cds bind --exec -- node -e "
const cds = require('@sap/cds');
cds.connect.to('db').then(async db => {
const { Users } = cds.entities('com.sap.developers.ims');
const user = await SELECT.one.from(Users).where({ uuid: '<sub-from-jwt>' });
console.log(user ? 'MATCH: ' + user.email : 'NO MATCH - check Subject Name Identifier config');
process.exit(0);
});
"Step 8: Social / Corporate Identity Providers (the live production setup) β
This is the configuration that ships today (DEV + PROD,
GITHUB_OIDC_ENABLED: "true"indeploy/dev.mtaext/deploy/prod.mtaext). The themed/signinpage (hugo/layouts/signin.html) presents five sign-in buttons:
Button sap_idproutingIAS registration Continue with SAP ID sap.defaultNone β direct XSUAA trust to SAP ID Service Continue with GitHub sap.custom,GitHubOIDC-Compliant Corporate IdP, backed by our GitHub-OIDC shim Continue with Google sap.custom,GoogleOIDC-Compliant Corporate IdP (Google is OIDC-native) Continue with LinkedIn sap.custom,LinkedInOIDC-Compliant Corporate IdP (LinkedIn is OIDC-native) Continue with Hugging Face sap.custom,Hugging FaceOIDC-Compliant Corporate IdP (Hugging Face is OIDC-native) Not "Social IdPs" in IAS. Despite the user-facing framing, none of these are configured under IAS's Social Identity Providers screen. All four non-SAP providers are registered as Corporate Identity Providers β OpenID Connect Compliant. This is deliberate: the chain-form
sap_idp=sap.custom,<Name>deep-link (see authentication.md) routes XSUAA through thesap.customIAS trust and then has IAS applyidp=<Name>to pick the specific provider β a mechanism that only works for Corporate IdPs that carry a stable trust Name.The IdP trust Name is load-bearing. The
<Name>in each button'ssap_idp=sap.custom,<Name>must exactly match the Corporate Identity Provider Display Name you set in IAS (GitHub,Hugging Face). Rename a provider in IAS and the matching/signinbutton breaks (falls through to the default IdP). If you change a Name, updatehugo/layouts/signin.htmlin lockstep.
Step 8a β Google (OIDC-native, simplest) β
- Google Cloud Console β create OAuth 2.0 credentials (Web application)
- Authorized redirect URI:
https://<tenant-id>.accounts.ondemand.com/oauth2/callback
- Authorized redirect URI:
- IAS Admin Console β Identity Providers β Corporate Identity Providers β Create
- Display Name:
Google(must match the/signinbutton'ssap_idp=sap.custom,Google) - Type: OpenID Connect Compliant
- Display Name:
- OpenID Connect Configuration:
- Discovery URL:
https://accounts.google.com/.well-known/openid-configuration - Client ID / Client Secret: from step 1
- Scopes:
openid profile email
- Discovery URL:
- Identity Federation β "Use Identity Authentication user store" (email-based matching, so social users merge onto their existing SAP-ID row by email β see identity continuity).
- Save, then Enable the IdP for the tutorials application under Applications & Resources β Applications β
<your app>β Identity Provider.
Step 8b β LinkedIn and Hugging Face (OIDC-native, same recipe as Google) β
Both expose a standard OIDC discovery document, so they follow the Google recipe exactly β only the provider-side app registration and discovery URL differ:
| Provider | Register app at | Discovery URL | IAS Display Name |
|---|---|---|---|
| LinkedIn Developers β "Sign In with LinkedIn using OpenID Connect" product | https://www.linkedin.com/oauth/.well-known/openid-configuration | LinkedIn | |
| Hugging Face | HF OAuth app settings | https://huggingface.co/.well-known/openid-configuration | Hugging Face |
For each: redirect URI = https://<tenant-id>.accounts.ondemand.com/oauth2/callback, scopes openid profile email, Type = OpenID Connect Compliant, and the Display Name must match the /signin button Name exactly (note the space in Hugging Face).
Apple is intentionally omitted β "Sign in with Apple" requires a paid Apple Developer account. If you add it later, register it as another OIDC Corporate IdP and add a matching button to
signin.html.
Step 8c β GitHub (needs the GitHub-OIDC shim) β
GitHub is OAuth 2.0 only β it has no OIDC discovery document, no id_token, and no JWKS endpoint, so IAS cannot federate it as an OpenID Connect Corporate IdP directly. We bridge the gap with a small OIDC-compliant shim that runs in the approuter (approuter/lib/github-oidc-shim.js), wrapping GitHub's OAuth2 so IAS sees a normal OIDC provider.
The shim mounts at /github-oidc/* on the approuter and exposes the full OIDC surface IAS needs:
| Endpoint | Purpose |
|---|---|
GET /github-oidc/.well-known/openid-configuration | OIDC discovery document |
GET /github-oidc/authorize | Validates the IAS request, 302 β GitHub |
GET /github-oidc/callback | Exchanges the GitHub code, fetches the profile, 302 β IAS |
POST /github-oidc/token | Verifies PKCE, mints the RS256 id_token |
GET /github-oidc/jwks | Public key for IAS to verify the id_token |
Flag: the shim is gated by GITHUB_OIDC_ENABLED (approuter env, read at call time). Currently "true" on both DEV and PROD. Off β the handler is a no-op pass-through (GitHub button then falls through to the default IdP).
Prerequisites β five credstore secrets (BTP Credential Store, resolved via approuter/lib/credstore-secret.js; never in .mtaext or source). Rotate via /admin-ui/#secrets:
| Alias | What it is |
|---|---|
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET | The GitHub OAuth app credentials (the shim is GitHub's client) |
GITHUB_OIDC_IAS_CLIENT_ID / GITHUB_OIDC_IAS_CLIENT_SECRET | The client credentials IAS uses to call the shim's /token back-channel (IAS is the shim's client) |
GITHUB_OIDC_SIGNING_KEY | RSA private key (PKCS#8 PEM) the shim signs id_tokens with; the public JWK is derived from it on demand |
GITHUB_OIDC_STATE_SECRET | Any sufficiently-random string; seals the stateless authorizeβtoken correlation (A256GCM) |
GitHub side β create an OAuth app at GitHub Developer Settings:
- Authorization callback URL:
https://<approuter-host>/github-oidc/callback - Copy the Client ID/Secret into the
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRETcredstore aliases.
IAS admin steps β register the shim as a Corporate IdP:
- IAS Admin Console β Identity Providers β Corporate Identity Providers β Create
- Display Name:
GitHub(must matchsap_idp=sap.custom,GitHub) - Type: OpenID Connect Compliant
- Display Name:
- OpenID Connect Configuration:
- Discovery URL:
https://<approuter-host>/github-oidc/.well-known/openid-configurationβ IAS reads the shim's discovery, authorize, token, and JWKS endpoints from here automatically. - Client ID: the value you put in
GITHUB_OIDC_IAS_CLIENT_ID - Client Secret: the value you put in
GITHUB_OIDC_IAS_CLIENT_SECRET - Scopes:
openid profile email - PKCE: S256 (the shim requires it)
- Discovery URL:
- Identity Federation β "Use Identity Authentication user store" (email-based matching). Caveat: GitHub emails are advisory β the shim keys identity on the GitHub numeric user id (OIDC
sub), never on email, because/user.emailis frequently null and noreply synthetics are not unique. A GitHub user whose verified primary email is private, or whose SAP-ID row has no email yet, will land on a separateUsersrow until an explicit account merge (the known #2552 boundary β see authentication.md). - Save, then Enable
GitHubfor the tutorials application (same place as Step 8a #5).
Verify the shim is reachable before registering in IAS:
# Discovery doc should return JSON with issuer = https://<approuter-host>
curl -s https://<approuter-host>/github-oidc/.well-known/openid-configuration | jq .issuer
# JWKS should return one RS256 key
curl -s https://<approuter-host>/github-oidc/jwks | jq '.keys[0].kty, .keys[0].alg'If either returns a 503 server_error, a credstore secret is missing (GITHUB_OIDC_SIGNING_KEY for jwks, GITHUB_CLIENT_ID for authorize). If they 404, GITHUB_OIDC_ENABLED is not "true" on the approuter.
Troubleshooting β
Users See IDP Picker β
Cause: Both SAP ID Service and IAS trust are set to "Available for User Logon". Fix: Disable one β set "Available for User Logon: No" for the one you don't want.
IAS Login Fails with "Identity Provider could not process the authentication request" β
Cause: IAS is configured to delegate to a "SAML 2.0 Compliant" corporate IDP that doesn't recognize IAS as a trusted SP (e.g., SAP ID Service / accounts.sap.com). Fix: Remove the corporate IDP configuration. Users should authenticate directly against IAS with their SAP Universal ID credentials. If you need corporate IDP delegation, use an IDP you control (Azure AD, Okta) and register IAS as a trusted SP on that IDP's side.
Transparent SSO Not Working (Login Form Appears) β
Cause: Unlike SAP ID Service which many developers have persistent sessions for, IAS may not have an active session in the browser. Fix: This is expected behavior with IAS. After first login, IAS maintains its own session. For truly transparent SSO, either:
- Use Option A (SAP ID Service default trust) β most SAP developers have active sessions
- Configure SSO session duration in IAS β Application β Authentication β Session Management
User Gets New Identity (Progress Data Mismatch) β
Cause: Subject Name Identifier in IAS doesn't match what the legacy system produced. Fix: Update IAS application config (Step 6). The sub claim format depends on the source system.
Application Returns 401 β
Fix: Verify:
- XSUAA service instance exists and is bound to the app
- At least one trust is established and active
xs-security.jsonredirect URIs match:https://*.cfapps.*.hana.ondemand.com/**- Role collections are assigned to the user
Machine-to-Machine (client_credentials) on the shared IAS app β
The /a2a (agent-to-agent) and /mcp-auth/* surfaces accept IAS client-credentials (M2M) bearer tokens minted from the shared tutorials-identity IAS application instance. Two things must both be true for M2M to work in a space; they are independent and were each a separate root cause during the #2593β#2649 rollout.
1. The IAS instance must permit the client_credentials grant β
The MTA adopts tutorials-identity as an existing-service and passes no provisioning parameters, so the OAuth2 config lives on the live instance, not in the descriptor. By default the application plan permits only interactive grants (authorization_code, authorization_code_pkce_s256, refresh_token) and a token request with grant_type=client_credentials returns HTTP 400 unsupported_grant_type.
Add client_credentials additively (do not drop the interactive grants or the redirect URIs β the human login flow needs them) with cf update-service. This is the canonical M2M config for every space:
cf update-service tutorials-identity -c '{
"oauth2-configuration": {
"grant-types": [
"authorization_code",
"authorization_code_pkce_s256",
"refresh_token",
"client_credentials"
],
"public-client": true,
"redirect-uris": [
"http://localhost:*/oauth/callback",
"http://localhost:*/callback",
"http://localhost:*/mcp-callback",
"http://127.0.0.1:*/oauth/callback",
"http://127.0.0.1:*/callback",
"http://127.0.0.1:*/mcp-callback"
]
}
}'Notes:
public-client: truedoes not blockclient_credentialsβ the broker still issues acredential-type: SECRETbinding, so a service key / binding carries a usableclientsecret.- An IAS M2M token is a bare application token: its claims are
app_tid, aud, azp, azpacr, exp, iat, iss, jti, sap_id_type, subβ there is noscope/scopesclaim (IAS M2M does not carry OAuth scopes the way XSUAA does)./a2aand/mcp-auththerefore gate on token validity β non-anonymous user, not on a scope string.
2. The /a2a route-scoped middleware must resolve its bindings per-space β
srv/lib/a2a/ias-auth-mw.js validates the IAS bearer (with an optional XSUAA fallback) only on /a2a. It resolves:
- IAS by instance name
tutorials-identity(identical across spaces) β required. - XSUAA by service label
xsuaaβ optional fallback. The XSUAA instance name differs per space (tutorials-xsuaaon dev,xsuaa-imsprodon prod), so it must not be resolved by a hardcoded instance name. (#2649: an earlier version looked XSUAA up by name in the samegetServices()call as IAS; the prod name miss threw and degraded/a2ato a full anonymous pass-through β valid IAS M2M tokens got 401 on prod while/mcp-authin the separate MCP MTA accepted them.)
A missing/renamed XSUAA binding degrades to IAS-only (M2M still works); only a missing IAS binding degrades /a2a closed (anonymous β 401).
Verify M2M end-to-end (per space) β
Mint a token from a short-lived service key and smoke both surfaces. Never print the token β assert on HTTP status and response shape only.
cf create-service-key tutorials-identity sk-smoke
# strip the "Getting key..." header line so the JSON parses
cf service-key tutorials-identity sk-smoke | sed -n '/{/,$p' > sk.json
CID=$(jq -r .credentials.clientid sk.json)
CSEC=$(jq -r .credentials.clientsecret sk.json)
URL=$(jq -r .credentials.url sk.json)
TOK=$(curl -s -u "$CID:$CSEC" -d grant_type=client_credentials "$URL/oauth2/token" | jq -r .access_token)
H=https://developers.sap.com # dev: https://tutorial-system-dev-tutorials-approuter.cfapps.eu10-005.hana.ondemand.com
A2A='{"jsonrpc":"2.0","id":"s","method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"ping"}],"messageId":"m1"}}}'
MCP='{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
curl -s -o /dev/null -w "a2a anon %{http_code}\n" -d "$A2A" "$H/a2a" # expect 401
curl -s -o /dev/null -w "a2a bearer %{http_code}\n" -H "authorization: Bearer $TOK" -d "$A2A" "$H/a2a" # expect 200
curl -s -o /dev/null -w "mcp bearer %{http_code}\n" -X POST -H "authorization: Bearer $TOK" -H 'accept: application/json' -d "$MCP" "$H/mcp-auth/api" # expect 200, 9 tools
cf delete-service-key tutorials-identity sk-smoke -f
rm -f sk.jsonCF target drift:
cf targetcan drift devβprod mid-session. Re-check the space before every mutatingcf update-service/ service-key command, and re-target explicitly before a prod smoke.
Rollback β
- From Option B back to Option A: Re-enable SAP ID Service default trust ("Available for User Logon: Yes"). Instant fix, no restart needed.
- From Option A to Option B: Establish IAS trust (Step 2), then optionally disable default trust (Step 3).