Files
arena-plm-fiori/tools/adt-reentrance-login/README.md
T
alexzandClaude Opus 5 ffd33f4ec0 ADR-004: back out of on-stack RAP; retarget to starter customizing tenant
Both tenants belong to the same starter system: my429480 is its
customizing tenant (client 100, key-user extensibility only, no ABAP)
and my431353 is its development tenant (client 080). This package had
recorded my429480 as "productive" and ADR-002 had assumed it offered
developer extensibility; both were wrong.

- ADR-004 withdraws the on-stack RAP route and moves the application to
  a side-by-side runtime against released OData APIs. ADR-002 marked
  superseded.
- Development tenant my431353 parked: all 19 ZARENA_PLM objects left
  active, sources kept in docs/handover/rap/ as reference, so the route
  stays reversible.
- Replacement host left open as OQ-11 (key-user extensibility vs CAP
  side-by-side); recommendation recorded as CAP. No build starts until
  OQ-11 is closed.
- RAP-dependent items V-2, V-3, OQ-5, OQ-8, OQ-9, OQ-10 parked rather
  than closed.
- Tenant references corrected across CLAUDE.md, KNOWLEDGE.md,
  app/README.md, the RAP handover README, the rap-developer agent and
  the reentrance-login helper README.

No SAP tenant was modified.

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

247 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
> **Parked 2026-09-29.** `my431353` is the starter system's **development**
> tenant (client 080). I parked that tenant on 2026-09-29 together with the
> on-stack RAP route, leaving all of its objects active in place. I keep this
> helper only in case that route is revived — nothing in the current
> side-by-side architecture calls it. Every `my431353` reference below is the
> default this tool was built against, not a live target. The working target
> is the starter system's customizing tenant `my429480` (client 100), which
> supports key-user extensibility only and needs no ADT logon at all.
---
## 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) — that tenant being the
development tenant parked on 2026-09-29, kept here as the default this tool
shipped with.
### 1. Start the helper on this host
```bash
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:
```bash
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
```
That host is the parked development tenant (client 080, parked 2026-09-29);
point `--tenant` elsewhere if the revived route ever lands on another tenant.
(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 `cat`s 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. The default is the development tenant parked on 2026-09-29. |
| `--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>`. |