Skip to content

[#1221] e2e characterization tests for /oauth2 in a non-root realm - #1233

Open
maximthomas wants to merge 6 commits into
OpenIdentityPlatform:masterfrom
maximthomas:features/1075-e2e-oauth2-realms
Open

maximthomas wants to merge 6 commits into
OpenIdentityPlatform:masterfrom
maximthomas:features/1075-e2e-oauth2-realms

Conversation

@maximthomas

@maximthomas maximthomas commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Why

#1221 (a step of #1075) pins the HTTP contract of /oauth2 with e2e tests that pass on master, where Restlet still serves /oauth2. A test that passes unchanged after the move to CHF is that PR's parity proof, and any expectation a move changes must be a divergence listed in that PR.

This part covers the non-root-realm deployment shape. No master spec puts a provider, a client and a user in a sub-realm. It adds no production code and no workflow change.

What

One new spec, e2e/oauth2/realms-test.spec.mjs, with 10 tests. It builds its own sub-realm per worker (provider, confidential client, user) and drives the four realm spellings (/oauth2/realms/root/realms/<sub>, /oauth2/realms/<sub>, /oauth2/<sub>, /oauth2?realm=<sub>) against:

  • discovery;
  • the token endpoint (client_credentials through ?realm=, client authentication on the path spellings, the password grant);
  • tokeninfo;
  • authorize (anonymous login redirect, code issuance for a sub-realm session, a root-realm session refused);
  • the failures: unknown realm, unrouted path, bad ?realm=, unknown Host, upper-cased realm segment.

The tests are the 10 of e2e/oauth2/realms-test.spec.mjs at commit b02024a of maximthomas/OpenAM, on the abandoned features/restlet-migration branch of the fork, which was written against the CHF port of /oauth2. Where the branch disagrees with master, the test follows master. Fixtures are not touched: the spec imports only REDIRECT_URI, basicAuth and sessionContext from oauth2-fixtures.mjs.

Realm and user per worker

The realm name and the user name carry a per-worker suffix. A realm created through /json/global-config/realms copies root's data-store config, so its users are written to root's directory, and deleting the realm keeps the user. A fixed name would answer 409 on a second run or from a parallel worker. afterAll deletes the user, then the realm, only if this worker created them. No 409 is tolerated.

Changed expectations

Run verbatim on master, the branch spec had 8 tests green and 2 red. The rewritten spec differs from the branch in these places:

Test Branch Master Reason
authorize redirects to the sub-realm login redirect_uri inside goto singly encoded (http://app.invalid/cb) goto is the request URL as sent (redirect_uri=http%3A%2F%2Fapp.invalid%2Fcb) branch recorded on CHF
unknown realm in the path (nested and flat) 404 {"error":"not_found","error_description":"Realm &quot;/nosuchsub&quot; not found"} (flat form without the slash) 404 {"code":404,"reason":"Not Found","message":"No mapping organization found for organization identifier: /nosuchsub"} for both forms branch recorded on CHF
unrouted path below a resolving realm 404 {"error":"not_found","error_description":"Not Found"} 404, body not pinned framework value reduced to status
bad ?realm= (bogus, <sub>/nosuch) 400 {"error":"invalid_request","error_description":"Invalid realm, <value>"} 404 {code,reason,message} naming the value as given branch recorded on CHF
unknown Host (legacy, nested, query) 400 invalid_request, two different descriptions by spelling 500 {"code":500,"reason":"Internal Server Error","message":"No mapping organization found for organization identifier: not-a-real-host.invalid"} on all three master defect, target named (see below)

The other tests, and every success answer, hold the branch's values with the exceptions under "tightened" below.

Assertions tightened beyond the branch

All values were recorded from master.

  • Content-Type is asserted exactly. Discovery answers application/json;charset=UTF-8; the token 200 answers bare application/json.
  • Error bodies are compared whole, including the {code,reason,message} 404 envelope of an unknown realm.
  • Member sets are compared whole: the discovery document, the token response, the tokeninfo body, the authorize redirect's query keys.
  • Tokeninfo carries one member more than predicted, named after the granted scope (profile): the scope validator adds each granted scope as a member valued from the client's attribute of that name. It is part of the pinned member set.
  • The unknown-realm check also covers the legacy spelling /oauth2/nosuchsub/..., which the branch never requested. Master answers it with the same 404 {code,reason,message} envelope naming /nosuchsub, here thrown by RestletRealmRouter (on the nested and flat spellings by RealmRoutingFactory's Restlet router).
  • The case-insensitive realm test covers the three path spellings, where the branch upper-cased only the legacy one: /oauth2/<SUB>, matched by RestletRealmRouter, and /oauth2/realms/ROOT/realms/<SUB> and /oauth2/realms/<SUB>, matched by RealmRoutingFactory's Restlet router, which the flip replaces. Each spelling also gains a realm-scoped leg: a client_credentials request gets a 200 token with the sub-realm's client, which exists only in the sub-realm. The branch's tokeninfo leg alone (401 invalid_token) would also pass if the segment fell back to root, because tokeninfo is not realm-scoped.

Not pinned: the Server: Restlet-Framework/2.4.4 header, and the bodies of unrouted 404 answers, which are framework values.

Defects pinned

An unknown Host under a sub-realm is a 500 on master, whatever the spelling, because the failed lookup of the Host's realm surfaces as an internal error. The target is 400 invalid_request, a client error. The test pins master's 500 and its comment names the target.

Evidence

All runs were local, on a container built FROM the published image with the WAR of master b9cd122c78 (no production change after #1219), embedded config and user store, two Playwright runs on one container.

Run Run 1 Run 2
branch spec verbatim on master 8 passed, 2 failed 8 passed, 2 failed
this spec, final version, inside the shared run below 10 passed 10 passed
shared server: every spec under e2e/oauth2, e2e/uma, e2e/xacml with this one, final version 91 passed, 0 failed 91 passed, 0 failed
spec mutant: one expectation changed in each test 10 failed 10 failed
production mutant M-realm-attr 2 failed, 8 passed 2 failed, 8 passed
production mutant M-realm-attr-rrf 2 failed, 8 passed 2 failed, 8 passed
production mutant M-realm-case-rrf 1 failed, 9 passed 1 failed, 9 passed
  • The shared run is 81 existing tests plus the 10 new ones; no master spec went red next to this one, and realms-test shows 10 passed in both runs.
  • The spec mutant and the M-realm-attr mutants ran before test 9 covered the nested and flat spellings; the other rows ran on the final version.
  • Spec mutant: each test failed at its own edited assertion.
  • Targeted mutants of the two legs added in review: each leg's expectation was flipped to what a fallback to root would answer, and the test was run alone. The legacy unknown-realm leg (expected 401 invalid_token) failed on 404; the upper-cased token leg (expected 401) failed on 200.
  • Production mutants (each removes the two httpRequest.setAttribute calls that hand the realm to the login):
    • M-realm-attr: RestletRealmRouter in openam-restlet. Tests 3 and 10 (client authentication on the path spellings, the password grant) are red on the legacy /<sub> leg with 401 invalid_client.
    • M-realm-attr-rrf: the Restlet inner router of RealmRoutingFactory in openam-rest. Tests 3 and 10 are red on the nested leg with 401 invalid_client.
    • In both, the ?realm= token test stays green through the query fallback.
  • Production mutant M-realm-case-rrf: the Restlet inner router of RealmRoutingFactory answers 404 for a realm segment that is not lower case, root still in any case. Test 9 is red on its nested leg (404 instead of 401 invalid_token), after its legacy legs passed. The test stops there; on the same container, for a probe realm, the upper-cased flat spelling answers the same 404, and the lower-case spellings still answer 401.
  • The raw requests (Host override, raw path bytes) follow the scheme of OPENAM_BASE_URL. Through a local TLS proxy in front of the master container, with its self-signed certificate in NODE_EXTRA_CA_CERTS, tests 8 and 9 passed on https://…:8443/openam and on https://…/openam (port 443). On :8443 the previous version failed both with socket hang up.
  • No realm oauth2realms* and no user realmuser* is left on the container after any run.

CI's build-docker runs this spec against an external OpenDJ user store; its result is not in this table.

Notes for the flip

The token endpoint's client and resource-owner logins in a path-spelled realm read the realm from the servlet request attribute realm. On the /realms/... spellings it is written by RealmRoutingFactory's Restlet router, on the legacy /{subrealm} spelling by RestletRealmRouter. A flip that resolves the realm elsewhere has to hand it to the login, or these logins fall back to the host's realm and answer 401 invalid_client (tests 3 and 10). ?realm= keeps working through the query fallback.

The realm segment matches in any case on all three path spellings, root included (test 9).

Part of #1221.

…2 in a non-root realm

Ports realms-test.spec.mjs from the restlet-migration branch and pins what master answers on Restlet:
discovery, the token endpoint (client_credentials and password grants), tokeninfo and authorize in a
sub-realm with its own provider, client and user, through the nested, legacy, flat and ?realm= spellings,
plus the unknown-realm, unrouted-path and unknown-Host answers. The unknown Host's 500 is pinned with
a comment naming the target, 400 invalid_request.
@maximthomas maximthomas added enhancement javascript Pull requests that update Javascript code oauth2 OAuth2 / OpenID Connect tests Test suite: coverage, fixtures, or test infrastructure labels Nov 12, 2026
…the scheme of OPENAM_BASE_URL

raw() always used node:http with a port-80 fallback. Against an https
OPENAM_BASE_URL it wrote plaintext to the TLS port, or connected to :80
when the URL had no port, while every Playwright request in the spec
followed the base URL.

It now picks node:https for an https base and defaults to port 443. The
TLS server name is set to the stand's host, because Node otherwise takes
it from the overridden Host header and the unknown-Host legs would fail
the certificate's name check. The certificate is still verified: a stand
with a self-signed one passes its CA in NODE_EXTRA_CA_CERTS.
…d and flat spellings too

The case-insensitivity test upper-cased only the legacy /oauth2/<SUB>
segment, which RestletRealmRouter resolves. The nested and flat spellings
go through RealmRoutingFactory's Restlet router, the one the move to CHF
replaces, so a case-sensitive match there left the test green.

Both legs, tokeninfo and the realm-scoped client_credentials grant, now
run on the three path spellings, the nested one with root upper-cased
too.
@maximthomas
maximthomas marked this pull request as ready for review November 12, 2026 11:43
@maximthomas
maximthomas requested a review from vharseko November 12, 2026 11:43

@vharseko vharseko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Assert that successful token responses contain usable tokens — realms-test.spec.mjs:166–168

expectToken checks that the token fields exist, but never checks their values. A 200 response containing "access_token": null passes all of these assertions; password-grant responses can likewise contain a null refresh_token. The path-spelling and password-grant tests do not consume those tokens afterward, so they would stay green even though the endpoint no longer issues usable credentials. The original tests asserted that the access token was non-empty. Please retain that assertion for each response, and check the refresh token on password-grant responses as well.

@maximthomas

Copy link
Copy Markdown
Contributor Author

Addressed the token-value assertion review in 461fe65afe.

expectToken now requires a non-empty string for access_token on every checked successful response and for refresh_token on password-grant responses. This covers all realm spellings, upper-cased realm paths, and both sub-realm and root-user password grants.

Validation: all 91 local OAuth2, UMA and XACML tests passed, including all 10 realm tests, against s6-openam:b2-r1-shared with an external OpenDJ user store. Focused helper checks accepted both valid response shapes and rejected all 21 malformed-token cases; syntax and git diff --check also passed.

@maximthomas
maximthomas requested a review from vharseko November 14, 2026 07:51

@vharseko vharseko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous finding is addressed. expectToken now requires a non-empty string for every checked access token and for refresh tokens in password-grant responses. I found no further issues in this fix.

CI checks are still queued.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement javascript Pull requests that update Javascript code oauth2 OAuth2 / OpenID Connect tests Test suite: coverage, fixtures, or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants