Files
arena-plm-fiori/tools/adt-reentrance-login/README.md
T
alexzandClaude Opus 5 468729eb4e Sync current sources: clean RAP handover names, app rebind, tooling
Working-tree state as of 2026-09-29, uncommitted since the initial
"upload current sources" commit:

- docs/handover/rap/: renamed the 11 generator-suffixed sources
  (zz*000) to the clean object names built on the tenant, plus the
  A2X leg and parameter-entity edits.
- app/: manifest, annotations, i18n, mock data and local/mock ui5
  configs updated to the live service shape.
- PRD.md, CHANGELOG.md, KNOWLEDGE.md: build record through
  2026-09-19, including the rejected reentrance-ticket path.
- tools/adt-reentrance-login/: retained login helper (new).
- docs/v2/zui_arena_change_appr_o4.metadata.xml: captured live
  $metadata (new).
- Dropped a stray 0.3GB-class Eclipse installer that the initial
  commit had tracked under a URL-mangled filename, and the
  machine-specific 0M7DEJI.code-workspace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 10:22:52 -07:00

9.6 KiB
Raw Blame History

adt-reentrance-login

Capture an SAP ADT reentrance ticket through a localhost redirect, so abap-adt-mcp can log on to the S/4HANA Cloud development tenant from this headless / SSH-only host — without a browser window on the server's desktop.

Node 24, standard library only, no dependencies, no install step.


Why this exists

abap-adt-mcp 2.1.0 can do browser SSO, but only by launching a server-side Chrome window (headless: false) and harvesting cookies over the DevTools protocol; there is no localhost redirect in that path, so over multi-hop SSH neither the desktop nor a DevTools relay is reachable in practice [KNOWLEDGE.md, 2026-09-16].

ADT for VS Code does it differently. It opens

https://<tenant>/sap/bc/adt/core/http/reentranceticket?redirect-url=http://localhost:<port>/adt/redirect

and reads the ticket back out of the redirect

http://localhost:<port>/adt/redirect?...&reentrance-ticket=<ticket>

(observed 2026-09-16). That half is portable: your browser does the SSO, the tenant redirects to localhost, and a forwarded TCP port puts "localhost" back on this host. This script is that listener.


Run it

Three steps, three terminals-worth of context. Values below use the defaults (tenant my431353, port 42010, 300 s timeout).

1. Start the helper on this host

node /home/alexz/projects/sap-integrations-mcl/arena-plm/tools/adt-reentrance-login/login.mjs

It prints (to stderr) the port to forward and the URL to open, then waits up to 300 seconds for exactly one redirect.

2. Forward the port to the machine running your browser

VS Code: open the Ports panel (Ctrl+Shift+P → Ports: Focus on Ports View) → Forward a Port → enter 42010. VS Code forwards it to your local localhost:42010.

Plain SSH, from your laptop:

ssh -L 42010:127.0.0.1:42010 <this-host>

The browser must reach this host's localhost. Without the forward the tenant redirects your browser to its own machine and nothing is captured.

3. Open the URL in that browser and complete SSO

https://my431353.s4hana.cloud.sap/sap/bc/adt/core/http/reentranceticket?redirect-url=http://localhost:42010/adt/redirect

(Or capture it programmatically: node login.mjs --print-url-only.)

After SSO the tab lands on "Ticket captured", the helper writes ~/.abap-adt-mcp/ticket.json (mode 0600) and exits 0.

4. Make the MCP server pick it up

~/.abap-adt-mcp/ticket-provider.sh already cats that file, and abap-adt-mcp caches the login per destination — it only re-runs the provider on an explicit login call or a reauthenticate (dist/index.js:268-292, 615-625, read 2026-09-18). So after capturing a fresh ticket, call the MCP login tool for the destination.


Options

Flag Default Meaning
--tenant <url> https://my431353.s4hana.cloud.sap Tenant base URL. The UI host, not the -api host.
--port <n> 42010 Loopback port. Bound to 127.0.0.1 only; there is no flag to change the bind address.
--timeout <seconds> 300 Give up and write nothing.
--out <path> ~/.abap-adt-mcp/ticket.json Output file, mode 0600 (directory created 0700).
--client <nnn> not sent Adds sap-client=<nnn>. See "Which client?" below.
--allow-host <name> — Additionally accept this Host header (repeatable), for tunnelled forwards that are not localhost.
--probe off After capture, test cookie vs header transport (below).
--print-url-only off Print the logon URL to stdout and exit; start no listener.
--help — Usage.

Exit codes: 0 captured · 2 usage · 3 timeout · 4 cannot bind · 5 redirect arrived without a usable ticket.

Which client?

Nothing is hard-coded. The client the browser session uses is unconfirmed; the published service URL shows sap-client=080, but that is the API host's service URL, not proof about the UI logon. Run without --client first; only add --client 080 if the tenant needs it.


Probe mode (--probe)

Open question this settles: SAP documents a logon ticket in the MYSAPSSO2 cookie and an assertion ticket in a mysapsso2 header [source: sap-docs sap-help loio 3354605668 and 3361378221, verified 2026-09-18]. Which one a reentrance ticket behaves as is unknown, and it decides whether abap-adt-mcp's sso2 mode can work at all — that mode sends the value strictly as a cookie (dist/lib/sso2TicketProvider.js, read 2026-09-18).

With --probe, after capture the helper makes two GETs to <tenant>/sap/bc/adt/core/discovery with Accept: application/atomsvc+xml and no other credentials:

  • (a) Cookie: MYSAPSSO2=<ticket>
  • (b) MYSAPSSO2: <ticket>

and reports only the HTTP status of each, whether a set-cookie with SAP_SESSIONID came back, and which succeeded. Bodies are discarded unread. A redirect is reported by host only (a SAML Location can carry request material).

Read the verdict as:

  • cookie worked → abap-adt-mcp's sso2 mode works as-is.
  • only the header worked → sso2 mode cannot use this ticket; the MCP server would need a header transport it does not have.
  • neither → expired ticket, wrong client, or wrong mechanism entirely.

Ticket lifetime

  • The lifetime of a reentrance ticket is not verified here. Treat it as short-lived and re-run the helper whenever logon fails. Do not assume the 8-hour default of a classic SAP logon ticket applies.
  • Whether a reentrance ticket is single-use is also unverified. If a captured ticket authenticates once and then stops, assume single-use and re-capture.
  • Expiry surfaces in the ADT MCP only as "Your user was logged off." abap_list_destinations and get_object_type_details still answer, so neither proves a live session; abap_creation-run_validation is the cheapest read-only liveness check [KNOWLEDGE.md, 2026-09-16].

Security rules

These are contracts, not preferences. The ticket is a live logon credential for the tenant.

  1. The ticket is never printed, logged, or put in an error message, argv, or an environment variable. The only place it is written is --out, mode 0600. Validation failures report the reason and the parameter names present — never values, never offending characters or positions.
  2. stdout stays empty. Every human-readable message goes to stderr, so nothing sensitive can be captured by a $(...) around the helper. The single exception is --print-url-only, which prints the (secret-free) logon URL.
  3. The listener binds 127.0.0.1 only and is one-shot. There is no flag to bind a wider interface. A Host header that is not a loopback name is refused (cheap DNS-rebinding guard); allow a specific tunnel name with --allow-host.
  4. Never paste the redirect URL into a chat, issue, log, or file — it contains the ticket. Note that the URL also lands in your browser's history; clear it on a shared machine.
  5. The output file is written with permissions set before any ticket byte reaches the disk (open + fchmod + write), so a pre-existing world-readable file cannot briefly expose it.
  6. Ticket values are validated against the RFC 6265 cookie-octet set copied verbatim from abap-adt-mcp — a ticket the MCP server would reject is rejected here instead of being written to a file that cannot work.

Percent-decoding, and why not URLSearchParams

The ticket is URL-decoded exactly once with decodeURIComponent over the raw query pair, and never re-encoded.

URLSearchParams is deliberately not used: it applies application/x-www-form-urlencoded rules and turns a literal + into a space. Measured on Node v24.15.0, 2026-09-18:

'?reentrance-ticket=AB%2BCD+EF%3d'
  searchParams.get(...)            -> 'AB+CD EF='      <-- space, corrupt
  decodeURIComponent(raw pair)     -> 'AB+CD+EF='      <-- correct

A space is not a legal cookie-octet, so a base64 ticket carrying + would be silently corrupted and then rejected.


Configuration (handled by the repo owner, documented here for reference)

This script does not touch ~/.abap-adt-mcp/systems.json or .mcp.json. For the record, abap-adt-mcp 2.1.0 requires for authType: "sso2" (dist/lib/systems.js:73-149, read 2026-09-18):

  • sso2.command must be an absolute executable path (sso2.args optional, sso2.timeoutMs 1000–300000);
  • the system url must be HTTPS ("so the ticket is never sent in plaintext");
  • insecureTls is refused in combination with sso2;
  • authType: "ticket" is accepted as an alias for sso2;
  • the server warns at startup that every remote caller of an sso2 destination shares the SAP identity of the user running it — one server instance per person.

Troubleshooting

Symptom Cause
Browser cannot reach localhost:42010 Port not forwarded, or forwarded to the wrong host. Re-check step 2.
port ... is already in use (exit 4) Another listener. Use --port <n> and forward that port.
Timed out ... Nothing was written. (exit 3) SSO never completed, or the redirect went to the wrong machine.
the redirect carried no reentrance-ticket parameter (exit 5) The tenant redirected without a ticket — usually a failed or cancelled SSO. The parameter names it did send are listed.
ticket contains characters outside the RFC 6265 cookie-octet set (exit 5) The value is not usable as a MYSAPSSO2 cookie; abap-adt-mcp would reject it too.
Refused a redirect with Host: ... Forward is not a plain localhost forward. Re-run with --allow-host <that-host>.