Skip to content

Vcsim tes token exchange - #4136

Draft
hickeng wants to merge 3 commits into
vmware:mainfrom
hickeng:vcsim-tes-token-exchange
Draft

hickeng wants to merge 3 commits into
vmware:mainfrom
hickeng:vcsim-tes-token-exchange

Conversation

@hickeng

@hickeng hickeng commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Description

Makes the vcsim token chain real enough for a client to go from a SAML token to a JWT that a Kubernetes apiserver can verify.

  1. STS signs and validates tokens. The STS simulator issues a new assertion per request, signed with its own RSA key (generated lazily). The subject is the UsernameToken user, the certificate CN for a solution user, the ActAs subject, or the renewed token's subject; HoK tokens confirm the request certificate. A new SessionManager.ValidToken hook (following ValidLogin) lets STS validate tokens: vAPI SIGN login checks the token signature and the HoK request signature and records the token subject as the session user (instead of "TODO"); SOAP LoginByToken validates the token (not the request signature). Without the STS simulator both stay lenient.
    • Behaviour change: hand-written tokens are rejected wherever the STS simulator is registered, including vcsim. The bats test and the rest example output are updated accordingly. Happy to gate this behind an option if reviewers prefer.
  2. Token Exchange Service (POST /rest/vcenter/tokenservice/token-exchange, com.vmware.vcenter.tokenservice.TokenExchange, released 1.18): exchanges an issued SAML token for an RS256 JWT (iss, sub, aud, group_names, exp, iat, jti, kid), requires a vAPI session, returns 500 with InvalidGrant for a bad subject token, and Handler.InjectInvalidGrant(n) allows fault injection.
  3. OIDC discovery and JWKS on vcsim's TLS listener: /openidconnect/<domain>/.well-known/openid-configuration and /openidconnect/jwks/<domain>.

No token is logged, and error bodies never echo the request. Handler.Minted() returns every issued assertion and JWT, for tests that scan logs for leaked credentials.

Closes: n/a (no upstream issue yet)

How Has This Been Tested?

  • TestTokenChain: SAML from STS, then TES, then JWT verification against the served JWKS trusting only vcsim's certificate; it also scans captured logs and error bodies for leaked tokens.
  • go test ./sts/... ./vapi/... ./session/... ./lookup/... ./ssoadmin/... ./simulator/ passes; govc and vcsim build; make lint reports 0 issues.
  • session.loginbytoken bats flow run by hand against the built govc and vcsim (bats not installed). session/keepalive ExampleHandlerREST failed once under parallel load (timing-based, basic auth), and passed 3 of 3 alone.

George Hicken and others added 3 commits September 29, 2026 10:04
The STS simulator returned one canned assertion, for
Administrator@VSPHERE.LOCAL, whose signature no key held by vcsim could
check. vAPI session login accepted any "SIGN" header and recorded the
session user as "TODO"; SOAP LoginByToken accepted any NameID.

The STS simulator now issues an assertion per request, signed with its
own key (generated on first use):

- the subject is the UsernameToken user, a solution user named by its
  certificate's CN, the ActAs token's subject, or, for Renew, the
  subject of the token being renewed; a name without a domain gets
  "@<domain>"
- a holder-of-key token confirms the request's certificate; a bearer
  token is issued when none is given, or KeyType is Bearer
- the lifetime is the one requested, 5m by default
- group membership is the default Administrator's, or Users and
  Everyone (and SolutionUsers for a solution user)
- a token presented to Renew, ActAs or the request header must be one
  it issued, or the request fails with a SOAP fault
- passwords are still not checked

The assertion is written in exclusive canonical form, so the digest is
taken over its bytes; the simulator only validates the form it writes.

The STS simulator sets SessionManager.ValidToken. When set:

- vAPI session login with a "SIGN" header requires a valid token, and a
  holder-of-key token requires a request signature by the confirmed
  key; the session user is the token's subject
- SOAP LoginByToken requires a valid token; its request signature is
  not checked

Without an STS simulator, both still accept any token, and vAPI records
the token's NameID rather than "TODO".

Handler.Minted returns every issued token, for tests that scan logs
for leaked credentials. No token is logged.

The session.loginbytoken bats test used hand-written tokens, which vcsim
now rejects; it uses a token issued by the STS simulator instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: George Hicken <george.hicken@broadcom.com>
Simulate POST /rest/vcenter/tokenservice/token-exchange, which
exchanges a SAML token issued by the STS simulator for a JWT, per
TokenExchange.exchange and the claims vCenter writes for the
vmware-tes:vc:vns:k8s audience:

- RS256, signed with the STS simulator's key; kid is the hex SHA-1
  thumbprint of its certificate
- iss is https://<host>/openidconnect/<domain>
- aud is the requested audience, in lower case
- sub, username and domain are the SAML subject, with the domain in
  lower case; group_names are its groups, as name@domain
- exp is 10h after iat; the response is issued_token_type id_token,
  token_type N_A and scope openid, wrapped in "value"

Errors follow the TES client in wcpsvc:

- a subject token that is not valid is a 500 whose body carries
  com.vmware.vcenter.tokenservice.exceptions.InvalidGrant
- an unknown audience, grant type or token type, a missing audience,
  or a subject token that is not base64 is a 400 invalid_request
- the request requires a vAPI session (401 otherwise), via the vapi
  simulator's handler

Only the vns audience and the SAML to id_token exchange are supported.

Handler.InjectInvalidGrant fails the next n exchanges with InvalidGrant,
as vCenter does when the session's own token is no longer valid, so a
client's session renewal can be tested. Minted JWTs are included in
Handler.Minted. Neither requests nor responses are logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: George Hicken <george.hicken@broadcom.com>
Serve the SSO server's OpenID Connect provider metadata and JWKS, so
that a JWT issued by the Token Exchange Service simulator can be
verified by, for example, a Kubernetes API server configured with the
issuer https://<host>/openidconnect/<domain>:

- GET /openidconnect/<domain>/.well-known/openid-configuration, and
  /openidconnect/.well-known/openid-configuration for the default
  domain; the issuer and endpoint URLs are laid out as vCenter's are
  (jwks_uri is /openidconnect/jwks/<domain>)
- GET /openidconnect/jwks/<domain> and /openidconnect/jwks: the STS
  simulator's RSA key, with its kid and certificate chain

Both are served on vcsim's listener, so over its TLS, and require no
session. The authorize, token and logout endpoints are advertised in
the metadata but not served.

TestTokenChain follows a holder-of-key SAML token through the JWT TES
issues for it to verification against the served discovery and JWKS,
trusting only vcsim's certificate, and checks that no minted token
appears in the simulator's log or in an error response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: George Hicken <george.hicken@broadcom.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant