Skip to content

[#1215] Serve /.well-known (WebFinger) through CHF and delete its Restlet code - #1219

Merged
maximthomas merged 9 commits into
OpenIdentityPlatform:masterfrom
maximthomas:features/1075-webfinger-chf
Nov 6, 2026
Merged

maximthomas merged 9 commits into
OpenIdentityPlatform:masterfrom
maximthomas:features/1075-webfinger-chf

Conversation

@maximthomas

@maximthomas maximthomas commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is slice S5 of the sequential Restlet to CHF migration tracked in #1075. Closes #1215.

OpenID Connect issuer discovery (/.well-known/webfinger, RFC 7033) moves from a servlet of its own, which ran the Restlet WebFinger application, to the OpenAM servlet (CHF), next to /json and /xacml. Its Restlet code is deleted. The wire contract pinned by e2e/oauth2/webfinger-test.spec.mjs is kept, except for the divergences listed below.

Changes

  • openam-oauth2: new OAuth2HttpAccessAuditFilter, a CHF access audit filter with component OAuth that takes the realm from the RealmContext.
  • openam-oauth2: new WebFingerService, a CHF @Get endpoint. It calls the unchanged OpenIDConnectProviderDiscovery with the deployment URL of the request's realm.
    • Discovery errors keep master's answer: {"error":"bad_request","error_description":...} with Content-Type: application/json and no charset.
    • A repeated resource or rel parameter counts with its first value, as on master.
  • openam-oauth2: new WebFingerHttpRouteProvider and WebFingerRealms, registered through META-INF/services/org.forgerock.openam.http.HttpRouteProvider.
    • Realms resolve as the Restlet RestletRealmRouter resolved them: the realm of the host name, then one sub-realm per path segment before webfinger, then the first realm query value, which wins.
    • An unknown realm answers 404 {"error":"Not Found","error_description":"Realm \"<realm>\" not found"}.
    • A path segment that CHF decodes to a realm name the lookup rejects (%2F, or a DN special character such as %2C) answers the same 404, naming the full realm path. Master looked the raw segment up, found no realm and answered 404.
    • The /json spelling realms/root[/realms/<sub>] is added through RealmRoutingFactory.
    • RealmContextFilter is not used, because it answers 400 to an unknown realm parameter.
  • web.xml: the WebFinger servlet and its mapping are deleted, and /.well-known/* is mapped to the OpenAM servlet.
  • Three classes are deleted: org.forgerock.openidconnect.restlet.WebFinger, org.forgerock.openidconnect.restlet.OpenIDConnectDiscovery, and org.forgerock.oauth2.restlet.OAuth2StatusService (only WebFinger used it).
  • e2e: e2e/oauth2/webfinger-test.spec.mjs grows from 11 to 19 tests. The upgrade guide gets an [IMPORTANT] note, and the WebFinger sentence of the Use the upstream org.restlet.jee 2.4.4 artifacts instead of the Jakarta-transformed copies, and fix WebFinger answering 500 #1201 note now points to it.

Divergences produced by the CHF code

Request master branch
GET /.well-known/realms/root/webfinger?... 404 (Realm "/realms" not found) 200, the JRD of the bare path
GET /.well-known/realms/root/realms/<sub>/webfinger?... 404 200
GET /.well-known/realms/<sub>/webfinger?... 404 200 (RealmRoutingFactory looks <sub> up by name or alias, as under /json)

On a deployment with a top-level realm named realms, master answered /.well-known/realms/webfinger from that realm and /.well-known/realms/x/webfinger from /realms/x. The branch takes these paths as the realms/ spellings, as /json does: 404 for the first unless a realm or alias named webfinger exists, and the realm or alias x for the second. This is intended. WebFingerHttpRouteProviderTest#theRealmsKeywordWinsOverATopLevelRealmNamedRealms pins it, and the upgrade note names the two ways to reach such a realm: /.well-known/realms/root/realms/realms/webfinger and ?realm=/realms.

Unpinned changes

These were measured on a branch container, unless marked otherwise.

  • POST (any method but GET/HEAD): still 405 with Allow: GET. The body changes from {"error":"Method Not Allowed",...} to CREST {"code":405,"reason":"Method Not Allowed","message":"Method Not Allowed"}.
  • Unrouted child of /.well-known: still 404. The Restlet framework body is gone.
  • Unknown realm in a realms/ spelling: 404 with the CREST body that /json gives, {"code":404,"reason":"Not Found","message":"Realm &quot;/bogus&quot; not found"}. The quotes are HTML-escaped by RealmRoutingFactory, as under /json. An unknown realm in the realm parameter or in a /<realm>/webfinger path keeps master's OAuth2-shaped body.
  • No Server: Restlet-Framework/2.4.4 header.
  • A host name that resolves to no realm:
    • master answers 500. This was read from RestletRealmRouter, not measured.
    • The branch answers 404 in the OAuth2 shape on the bare and /<realm>/ spellings.
    • On the realms/ spellings it answers 400 CREST from RealmRoutingFactory's hostname filter, as /json does.
  • ?_api is answered by CHF's global ApiDescriptorFilter.
  • A malformed % escape in the query answers 400 before routing.
  • WebFinger requests no longer set the servlet request attributes realm and realmObject, which master's RestletRealmRouter set before the base URL was computed. Nothing in the repository reads them, and the CHF router of /json never set them. A custom Base URL Source extension or servlet filter that reads them would get null. The upgrade note says so. This was read from the code, not measured.
  • Access audit events no longer carry the user ID and session tracking ID that the Restlet OAuth2AccessAuditFilter read from a session cookie the request sent. They record component OAuth, the realm and the status.

Known edge, not fixed here

An encoded slash or an encoded DN special character in a realm segment of a realms/ spelling answers 500, for example /.well-known/realms/root/realms/a%2Fb/webfinger or /.well-known/realms/root/realms/a%2Cb/webfinger. RealmRoutingFactory's CHF realm router lets the IllegalArgumentException escape: Realm.of(Realm, "a/b") rejects the slash, and the DN parser of the realm lookup rejects the comma. The %2C case was traced through the code, not measured. /json and /xacml share that router and answer the same. Tomcat rejects %2F unless ALLOW_ENCODED_SLASH is set, but lets %2C through. The bare and /<realm>/ spellings (/.well-known/a%2Fb/webfinger, /.well-known/sub/a%2Cb/webfinger) answer 404, which unit tests pin. The fix belongs in RealmRoutingFactory, for all three endpoints.

Tests

Unit tests in openam-oauth2, all with 0 failures:

  • OAuth2HttpAccessAuditFilterTest 2, WebFingerServiceTest 8, WebFingerHttpRouteProviderTest 22.
  • Module run over **/oauth2/** and **/openidconnect/**: 951 tests, 0 failures, 0 errors. This includes OpenIDConnectProviderDiscoveryTest (10).
  • Four production mutants in one WebFingerHttpRouteProviderTest run: 8 of 22 tests failed, exactly the expected ones.
    • The host step looks up / instead of the host name: resolvesTheRealmOfTheHostName fails.
    • The last realm query value wins: the &realm=/sub&realm=/ row fails.
    • RealmRoutingFactory answers realms/<name> with the host realm: the /realms/sub/webfinger row and theRealmsKeywordWinsOverATopLevelRealmNamedRealms fail.
    • subRealmHandler without its IllegalArgumentException catch: the four %2C and %2F rows fail.

New or changed e2e tests (19 in the spec):

  • answers the realms/root path spelling like the bare path replaces answers 404 to the realms/root path spelling (divergence).
  • answers 404 for a path segment that names no realm, answers an empty realm parameter like the root realm, answers HEAD like GET without a body and uses the first value of a repeated parameter (parity: all pass on master).
  • /.well-known/webfinger in a sub-realm: a throwaway realm is created and deleted per run. It has four tests: ?realm= and the path segment (parity), realms/root/realms/<sub> and realms/<sub> (divergences).

Runtime evidence

Docker stand: the openidentityplatform/openam image with the WAR replaced and the embedded config store. Each spec set ran twice on one container.

The stand ran the WAR of 4c11483. The round-1 review fixes were not run on a stand. They change only the answer to a decoded %2F or %2C in a realm path segment, which no e2e test sends, plus tests and documentation. build-docker runs the e2e specs on the new head.

WAR Specs Result (both runs)
master (35b52a8; WebFinger and realm code unchanged up to 2a80c16) oauth2/webfinger-test 16 passed / 3 failed: exactly the three realms/ spellings, each 404 instead of 200
branch oauth2/webfinger-test 19 passed / 0 failed
branch oauth2/webfinger-test, oauth2/oauth2-test, uma, xacml 61 passed / 0 failed
branch + mutant M1: WebFingerService sets the error Content-Type before setEntity oauth2/webfinger-test 16 passed / 3 failed: the three 400 tests (the charset reappears)
branch + mutant M2: realmParameterFilter ignores the realm parameter oauth2/webfinger-test 18 passed / 1 failed: answers 404 for a realm that does not exist

answers a JRD for the sub-realm named by ?realm= stays green under M2, because the JRD is the same in every realm. The unit test WebFingerHttpRouteProviderTest#resolvesTheRealmOfEverySpelling pins the realm itself.

Startup on the branch container:

  • catalina.out and the debug logs hold no Guice ProvisionException, ConfigurationException or CreationException.
  • No RuntimeException caught from HttpFrameworkServlet.
  • /json/authenticate issued the admin token, and /.well-known/webfinger answered from CHF, so HttpRouterProvider built the new route provider and its injected types.

Upgrade note

New [IMPORTANT] block in openam-documentation/openam-doc-source/src/main/asciidoc/upgrade-guide/chap-upgrade-servers.adoc:

  • A customized web.xml must delete the WebFinger servlet and map /.well-known/* to the OpenAM servlet.
  • It covers the realms/ spellings (404 before) and the CREST 404 body for an unknown realm in them.
  • It covers the audit change and the removed classes.

The first paragraph of the #1201 note now refers to this block instead of telling operators to swap the servlet class.

@maximthomas maximthomas added enhancement javascript Pull requests that update Javascript code oauth2 OAuth2 / OpenID Connect documentation Documentation, README, or javadoc refactoring Code cleanup, refactor, dead-code or dependency removal tests Test suite: coverage, fixtures, or test infrastructure java Pull requests that update java code labels Nov 5, 2026
…rejects and pin realm resolution

CHF decodes the {subrealm} variable, so a %2C reached the realm lookup as a
DN special character and its IllegalArgumentException escaped as a 500;
master looked the raw segment up and answered 404. The handler now catches
it around the lookup only and names the full realm path, also for %2F.

New WebFingerHttpRouteProviderTest cases pin the host-name step, the
realms/<sub> spelling, the first of two realm parameters, the realms
keyword over a top-level realm named "realms", and the %2C/%2F answers.
RealmTestHelper gains mockMalformedRealm for a lookup that throws
IllegalArgumentException.
… named realms, realm request attributes

The note now says how to reach a top-level realm named "realms", which the
realms/ spellings shadow as under /json, and that WebFinger requests no
longer set the realm and realmObject servlet request attributes. The e2e
spec header lists the realms/<sub> spelling it pins.
@maximthomas
maximthomas marked this pull request as ready for review November 5, 2026 12:37
@maximthomas
maximthomas requested a review from vharseko November 5, 2026 12:37

@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.

Reviewed head 5ddcad9 and the descriptions and discussion history of this PR, #1215 and #1075. No new actionable findings. The documented compatibility changes and the acknowledged RealmRoutingFactory limitation are consistent with the agreed scope.

Validation: all 32 new unit-test cases passed with openam-http built from the same head and RealmTestHelper taken from this PR. The updated Playwright spec passes node --check; git diff --check is clean. I did not rerun the container-based e2e tests.

@maximthomas
maximthomas merged commit 8527b9b into OpenIdentityPlatform:master Nov 6, 2026
16 checks passed
@maximthomas
maximthomas deleted the features/1075-webfinger-chf branch November 6, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation, README, or javadoc enhancement java Pull requests that update java code javascript Pull requests that update Javascript code oauth2 OAuth2 / OpenID Connect refactoring Code cleanup, refactor, dead-code or dependency removal tests Test suite: coverage, fixtures, or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Serve /.well-known (WebFinger) through CHF and delete its Restlet code

2 participants