Exports SAML trust config from SAP BTP Neo subaccount via REST API, classifies IdPs as IAS or third-party, generates CF migration notes, saves JSON report to .migration/neo-trust-config.json. For Neo-to-CF IdP migrations.
npx claudepluginhub sap-samples/btp-neo-java-app-migration --plugin sap-btp-neo-migrationThis skill is limited to using the following tools:
Export and analyze Neo subaccount trust configuration for Cloud Foundry migration.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
Export and analyze Neo subaccount trust configuration for Cloud Foundry migration.
This skill exports the complete SAML trust configuration from a Neo subaccount using the Trust Management REST API. It:
$MIGRATION_DIR/neo-trust-config.json for downstream skillsThis skill is read-only — it does NOT create trust in CF. A future subaccount-trust-import skill will consume the output.
Scope: Application IdPs only. This skill exports the identity providers used for end-user authentication to applications (the
applicationIdentityProviderssection of the Neo SAML trust config). These can be the default SAP identity provider (accounts.sap.com), an IAS tenant, or a third-party IdP (e.g. Microsoft Azure AD). This is distinct from the platform IdP, which governs access to platform tools (BTP cockpit, CLI) and is configured separately — that is out of scope for this skill.
Before running this skill, ensure:
Tools available on PATH:
curl (required)jq (recommended — install with choco install jq on Windows or brew install jq on macOS; if unavailable, the skill will parse JSON directly)Neo Platform API OAuth client created in the Neo subaccount cockpit with the hcp.readTrustSettings scope — OR a pre-issued Bearer token (valid for 25 minutes)
Neo subaccount technical name (not the display name — find it in Neo cockpit > Overview)
Neo region host (e.g., eu1.hana.ondemand.com, us1.hana.ondemand.com)
No dependency on other skills — this skill can run standalone.
0a. Determine the migration directory:
# If running inside an app migration copy, use its .migration/ folder.
# Otherwise use a temp directory — subaccount migration is not tied to any single app repo.
if [ -f .migration/cf-migration-config.json ]; then
MIGRATION_DIR="$(pwd)/.migration"
else
MIGRATION_DIR="${TMPDIR:-/tmp}/neo-subaccount-migration"
mkdir -p "$MIGRATION_DIR"
fi
echo "Migration directory: $MIGRATION_DIR"
0b. Check for existing config file:
if [ -f "$MIGRATION_DIR/neo-migration-config.json" ]; then
cat "$MIGRATION_DIR/neo-migration-config.json"
fi
Read the following fields if present:
neoSubaccount → subaccount technical nameneoRegionHost → region hostauth.method → "clientCredentials" or "bearerToken"auth.clientId + auth.clientSecret → if method is clientCredentialsauth.bearerToken → if method is bearerToken0c. Check the user's prompt for any values provided inline.
0d. If any required value is still missing, invoke the subaccount-migration-orchestrator skill to collect all inputs and write $MIGRATION_DIR/neo-migration-config.json, then return here and continue from Step 1.
Do not ask the user for individual values directly — the orchestrator owns input collection and ensures all three export skills share a consistent config file.
0e. If $MIGRATION_DIR is inside a git repo, ensure it is in .gitignore:
if [[ "$MIGRATION_DIR" != /tmp* ]]; then
if [ -f .gitignore ]; then
grep -q '^\\.migration' .gitignore 2>/dev/null || echo '.migration/' >> .gitignore
grep -q '^\.migration' .gitignore 2>/dev/null || echo '.migration/' >> .gitignore
echo '.migration/' > .gitignore
fi
fi
If using client credentials (auth.method = "clientCredentials"):
export MSYS_NO_PATHCONV=1
TOKEN_RESPONSE=$(curl -s -X POST \
"https://api.${REGION_HOST}/oauth2/apitoken/v1?grant_type=client_credentials" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "${CLIENT_ID}:${CLIENT_SECRET}")
Extract the token:
BEARER_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token // empty')
If jq is not available, read the response and extract access_token from the JSON manually.
Verify the token was obtained:
If BEARER_TOKEN is empty, check the response for error details:
| HTTP Status | Cause | Solution |
|---|---|---|
| 401 | Invalid client_id or client_secret | Verify Platform API OAuth client credentials in Neo cockpit |
| 400 | Bad request | Ensure the OAuth client is properly configured with hcp.readTrustSettings scope |
| Network error | Cannot reach API | Verify region host is correct (e.g., eu1.hana.ondemand.com) and network connectivity |
If the token fetch fails, show the error to the user and stop.
If using pre-issued token (auth.method = "bearerToken"):
Skip this step — use the token provided by the user. Warn: "Note: Your Bearer token expires 25 minutes after it was issued."
export MSYS_NO_PATHCONV=1
TRUST_RESPONSE=$(curl -s -w "\n%{http_code}" \
"https://apissecurity.${REGION_HOST}/trust/v2/accounts/${SUBACCOUNT}" \
-H "Authorization: Bearer ${BEARER_TOKEN}" \
-H "Accept: application/json")
# Separate body and HTTP status code
HTTP_STATUS=$(echo "$TRUST_RESPONSE" | tail -1)
TRUST_BODY=$(echo "$TRUST_RESPONSE" | sed '$d')
On Windows, if parsing fails due to \r\n line endings:
TRUST_BODY=$(echo "$TRUST_RESPONSE" | sed '$d' | tr -d '\r')
HTTP_STATUS=$(echo "$TRUST_RESPONSE" | tail -1 | tr -d '\r')
Verify the response:
| HTTP Status | Cause | Solution |
|---|---|---|
| 200 | Success | Proceed to parsing |
| 401 | Token expired or invalid | If using pre-issued token, ask user for a fresh one. If using client credentials, retry token fetch |
| 403 | Insufficient scopes | Ensure OAuth client has hcp.readTrustSettings scope |
| 404 | Subaccount not found | Use the technical name (not display name). Find it in Neo cockpit > subaccount Overview page |
| 500 | Server error | Wait a moment and retry. If persistent, check SAP system status |
If the status is not 200, show the error and response body to the user and stop.
Save the raw response for reference:
echo "$TRUST_BODY" > $MIGRATION_DIR/neo-trust-raw-response.json
Read the trust configuration response and classify each identity provider.
Classification logic:
For each IdP in applicationIdentityProviders.identityProviders[]:
type equals "SCI" → classify as IAS, use host field as iasHosthost contains .accounts.ondemand.com, .accounts.cloud.sap, or .accounts400.ondemand.com → classify as IAS, use host as iasHostssoUrl or sloUrl contains .accounts.ondemand.com, .accounts.cloud.sap, or .accounts400.ondemand.com → classify as IAS, extract the hostname from the URL as iasHost (e.g. https://mytenant.accounts.ondemand.com/saml2/idp/sso → mytenant.accounts.ondemand.com)iasHost to nullWhy URL-based detection matters: Some Neo subaccounts have IAS IdPs where the
typefield is absent or notSCI, and thehostfield may also be missing. In these cases the IAS tenant host can still be reliably extracted from the SSO or SLO URLs, which always contain the IAS tenant hostname as the domain.
Extract per-IdP data:
For each IdP, extract and normalize:
name, enabled (convert string "true"/"false" to boolean)ssoUrl, ssoBindingsignatureAlgorithmonlyForIdpInitiatedSSO, onlyForOAuthSAMLBearerFlow (convert to boolean)userIdSource (object with type and optional value)assertionBasedAttributes[] (keep full array)defaultAttributes[] (keep full array)assertionBasedGroups[] (keep full array including rules)defaultGroups[] (keep full array)signingCertificate — include only for ThirdParty IdPs (it is the IdP's public certificate, required to configure trust in CF); omit for IAS IdPs (connection is established automatically via btp create security/trust)SECURITY: Do NOT include signingKey or signingCertificate from localServiceProvider in the output (these are the SP's private key and certificate).
For each IdP, generate a cfMigrationNotes array of human-readable strings:
| Condition | Migration Note |
|---|---|
enabled is false | "Currently disabled in Neo — skip unless needed in CF" |
userIdSource.type is "Attribute" | "Custom user ID source (Attribute: {value}) — must be configured in IAS" |
assertionBasedAttributes is non-empty | "Has {N} assertion-based attribute mappings that need IAS configuration" |
defaultAttributes is non-empty | "Has {N} default attributes — configure in IAS" |
assertionBasedGroups is non-empty | "Has assertion-based group rules ({N} groups) that need manual IAS configuration" |
defaultGroups is non-empty | "Has {N} default groups — map to role collections in CF" |
onlyForIdpInitiatedSSO is true | "IdP-initiated SSO only — verify CF/IAS support for this flow" |
onlyForOAuthSAMLBearerFlow is true | "OAuth SAML Bearer flow only — configure in CF destination service" |
signatureAlgorithm is "SHA-1" | "Uses SHA-1 signature algorithm — consider upgrading to SHA-256" |
Type is ThirdParty | "Third-party IdP — must be manually registered in IAS or CF trust configuration" |
Type is IAS | "IAS tenant ({iasHost}) — can be connected to CF subaccount via btp CLI or XSUAA API" |
Construct the output JSON with the following structure and save using the Write tool:
{
"sourceSubaccount": "<subaccount technical name>",
"sourceRegion": "<region host>",
"exportTimestamp": "<ISO 8601 timestamp>",
"configurationType": "<from response, e.g. 'Custom'>",
"localServiceProvider": {
"name": "<SP name>",
"principalPropagationEnabled": "<true|false>",
"forceAuthenticationEnabled": "<true|false>",
"useCustomApplicationDomains": "<true|false>",
"defaultIdentityProviderName": "<default IdP name>"
},
"identityProviders": [
{
"name": "<idp-name>",
"type": "IAS|ThirdParty",
"iasHost": "<host or null>",
"enabled": true,
"ssoUrl": "<url>",
"ssoBinding": "HTTP-POST|HTTP-REDIRECT",
"signatureAlgorithm": "SHA-256|SHA-1",
"onlyForIdpInitiatedSSO": false,
"onlyForOAuthSAMLBearerFlow": false,
"userIdSource": {
"type": "Subject|Attribute",
"value": "<attribute-name or null>"
},
"signingCertificate": "<base64-DER — only present for ThirdParty IdPs, omitted for IAS>",
"hasAssertionBasedAttributes": true,
"assertionBasedAttributes": [],
"hasDefaultAttributes": false,
"defaultAttributes": [],
"hasAssertionBasedGroups": true,
"assertionBasedGroups": [],
"hasDefaultGroups": false,
"defaultGroups": [],
"cfMigrationNotes": []
}
],
"migrationSummary": {
"totalIdPs": 0,
"iasIdPs": 0,
"thirdPartyIdPs": 0,
"idpsWithCustomAttributes": 0,
"idpsWithGroupRules": 0,
"requiresManualSteps": false,
"manualSteps": []
}
}
Save the file:
# Directory should already exist from Step 0
# Use the Write tool to save to $MIGRATION_DIR/neo-trust-config.json
Build the migrationSummary:
requiresManualSteps = true if manualSteps is non-emptyAfter saving the output file, display a summary to the user:
Trust Configuration Export Complete
====================================
Source: <subaccount> (<region>)
Configuration Type: <type>
Default IdP: <name>
Principal Propagation: <enabled/disabled>
Identity Providers: <total>
IAS: <count>
Third-Party: <count>
[For each IdP:]
- <name> (<IAS|ThirdParty>) [ENABLED|DISABLED]
Migration notes:
- <note 1>
- <note 2>
Manual Steps Required: <yes/no>
[If yes, list steps]
Output saved to: $MIGRATION_DIR/neo-trust-config.json
| File | Location | Purpose |
|---|---|---|
neo-migration-config.json | $MIGRATION_DIR | Shared input config — Neo subaccount details and auth credentials |
neo-trust-config.json | $MIGRATION_DIR | Output — parsed trust config with IdP classifications and migration notes |
neo-trust-raw-response.json | $MIGRATION_DIR | Raw API response (for debugging) |
Verify output file exists:
ls -la $MIGRATION_DIR/neo-trust-config.json
Verify JSON is valid:
jq . $MIGRATION_DIR/neo-trust-config.json
Or if jq is unavailable:
python3 -c "import json; json.load(open('$MIGRATION_DIR/neo-trust-config.json'))"
Verify migration summary:
jq '.migrationSummary' $MIGRATION_DIR/neo-trust-config.json
List all IdPs with classification:
jq '.identityProviders[] | {name, type, enabled}' $MIGRATION_DIR/neo-trust-config.json
Cause: Invalid Platform API OAuth client credentials.
Solution: Verify the client ID and secret in Neo cockpit > OAuth. Ensure the OAuth client has the hcp.readTrustSettings scope assigned.
Cause: Wrong subaccount name — the API requires the technical name, not the display name. Solution: Find the technical name in Neo cockpit > subaccount Overview page. It is typically lowercase with no spaces.
Cause: Bearer token expired (25-minute validity) or invalid. Solution: If using a pre-issued token, obtain a fresh one. If using client credentials, verify them and retry.
Cause: OAuth client does not have hcp.readTrustSettings scope.
Solution: In Neo cockpit, edit the Platform API OAuth client and add the hcp.readTrustSettings scope.
Cause: Git Bash on Windows converts /paths to Windows paths in URLs.
Solution: Set export MSYS_NO_PATHCONV=1 before running curl commands (this skill does this automatically).
Cause: jq is not installed.
Solution: Install jq (choco install jq on Windows, brew install jq on macOS). The skill will still work by parsing JSON directly, but jq makes verification easier.
Cause: The subaccount uses the default SAP ID Service trust configuration with no custom IdPs.
Solution: This is valid — the output file will contain an empty identityProviders array. No further trust migration is needed.
After completing this skill:
$MIGRATION_DIR/neo-trust-config.json and creates SAML trust for IAS IdPs in the CF subaccount via the XSUAA apiaccess REST API