inspector/clients/web/src/components/groups/ConnectionInfoContent/oauthDetailsFromConnectionState.ts
cliffhall 07cafeedbc feat: surface and decode the OAuth id_token in Connection Info (#2019)
When an authorization server returns an `id_token` alongside the access
token, the Inspector already persisted it and never showed it. Wire it
through to Connection Info as a viewer.

- Generalize `OAuthAccessTokenField` into `OAuthTokenField` (label + token),
  so the ID Token row reuses the existing copy control and "Decode JWT"
  toggle rather than duplicating them. Access-token behavior is unchanged.
- `oauthDetailsFromConnectionState` now surfaces `tokens.id_token` as
  `idToken`, omitted entirely when absent so no empty row renders.
- Docs: update the two `specification/` references to the renamed field.

This is deliberately a viewer, not a flow: no OIDC login mode, no signature
verification, and the `id_token` is never treated as a credential. The EMA
path is untouched. `id_token` is already in `maskSecrets`'
`JSON_SENSITIVE_KEYS`, so it stays masked in the Network tab — no gap.

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-15 18:33:41 -04:00

33 lines
1.3 KiB
TypeScript

import type { OAuthConnectionState } from "@inspector/core/auth/types.js";
import type { OAuthDetails } from "./ConnectionInfoContent";
/** Map persisted connection auth state into Connection Info display props. */
export function oauthDetailsFromConnectionState(
state: OAuthConnectionState,
): OAuthDetails {
const scopeSource = state.grantedScope ?? state.configuredScope;
const scopes = scopeSource?.split(" ").filter(Boolean);
return {
protocol: state.protocol,
authorized: state.authorized,
...(state.client?.clientId && { clientId: state.client.clientId }),
...(state.client?.registrationKind && {
clientRegistrationKind: state.client.registrationKind,
}),
...(state.authorizationServerMetadata?.authorization_endpoint && {
authUrl: state.authorizationServerMetadata.authorization_endpoint,
}),
...(scopes && scopes.length > 0 && { scopes }),
...(state.tokens?.access_token && {
accessToken: state.tokens.access_token,
}),
// Surfaced for inspection when the AS returned one; omitted entirely
// otherwise so Connection Info renders no empty row (#2019).
...(state.tokens?.id_token && {
idToken: state.tokens.id_token,
}),
...(state.protocol === "ema" &&
state.ema?.idpSession && { idpSession: state.ema.idpSession }),
};
}