Skip to content

refactor!: move the SSO authenticators to fess-sso-* plugins - #3430

Merged
marevol merged 1 commit into
masterfrom
feat/extract-sso-authenticators
Sep 10, 2026
Merged

marevol merged 1 commit into
masterfrom
feat/extract-sso-authenticators

Conversation

@marevol

@marevol marevol commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Pairs with four new plugin repositories: fess-sso-saml, fess-sso-spnego, fess-sso-entraid, fess-sso-oidc. Reviewable now. Nothing here needs them to exist first — the SDKs are simply dropped — but no installation should upgrade to a 15.9 that has this without them published.

All four authenticators leave the distribution, the way #3428 moved the S3 storage backend. Together they were 15 jars, 5,827,073 bytes of third-party SDKs on four independent release cadences — java-saml with xmlsec and woodstox, msal4j, the Nimbus OAuth2/OIDC stack, the SPNEGO library — in a distribution where most installations configure none of them.

What core keeps, and why

Core keeps the mapping from an sso.type value to the component name <sso.type>Authenticator, so a type core has never heard of reaches a plugin that registers that name.

SsoAction stays: FessSearchAction references SsoAction.class, and the responses it serves are redirects and a metadata stream, not JSP.

The administration screens stay, and that is forced rather than chosen. FessWebResourceRoot.processWebInfLib() mounts a Fess-WebAppJar plugin at /WEB-INF/classes as a CLASSES_JAR and never adds a RESOURCE_JAR, so a plugin cannot supply a JSP. admin_general.jsp therefore keeps offering all four types, with the 47 form fields and the labels.

fess_sso.xml keeps ssoManager and stays as the ++ merge point. fess_sso++.xml is deleted, because there is no way to share the file: a component of the same name in the war and in a plugin makes getComponent throw TooManyRegistrationComponentException and runs @PostConstruct twice, and fess_sso+<component>.xml cannot override it either — RedefinableComponentTagHandler.redefine() refuses a base path containing +. A test asserts core ships the base file and no ++ file.

The four credential classes move with their authenticators, out of org.codelibs.fess.app.web.base.login into org.codelibs.fess.sso.<name>. Nothing resolved them by name: FessLoginAssist handles LocalUserCredential and delegates every other type to auth.resolveCredential(), and they implement only LoginCredential and FessCredential, which stay. Leaving them under org.codelibs.fess.app would have put plugin classes in the smart-deploy scan path for no benefit.

Reporting an sso.type nothing serves

getAuthenticator() answered null and logged nothing. Every caller turns that null into a redirect, so GET /sso/ answered 302 to /login/, and SsoAction.index() put errors.sso_login_error on that page — "SSO login process failed.", which names neither a cause nor a remedy. The only trace in the log was a debug line reading "SSO is available but no user found."; nothing at warning level or above, with a configuration that is complete and correct — what is missing is a plugin. That became the ordinary upgrade path with this change, so SsoManager now warns once per sso.type, naming the component it looked for and the plugin that provides it:

No SSO authenticator is registered as samlAuthenticator for sso.type=saml. Every authenticator ships as a fess-sso-* plugin: install fess-sso-saml for saml, fess-sso-spnego for spnego, fess-sso-entraid for entraid (or the legacy aad), or fess-sso-oidc for oic. Until then every request to /sso/ is redirected back to the login page.

Blank and none say nothing — /sso/ is reachable whether or not SSO is configured. It is also anonymous, so the warning is deduplicated per type to keep an unauthenticated client from filling the log. Warning rather than error, because an ERROR line is a notification trigger in Fess and this is an installation that has not finished. The wording follows the exception StorageClientFactory writes for the same situation.

The pairs are spelled out rather than derived, because sso.type=oic is served by fess-sso-oidc — the one place the plugin name and the type differ, since oic is an internal abbreviation and the documentation has always said OIDC.

Dependency measurement

mvn dependency:list -DincludeScope=runtime before and after: 256 runtime artifacts to 241. Exactly 15 removals, no additions, no version changes.

  • commons-text stays — java-saml is one of three requesters, with fess-crawler and handlebars.
  • woodstox-core and stax2-api do leave, reached only through xmlsec 4.0.1, and nothing left in the war needs a StAX implementation.
  • bcprov-jdk18on is unaffected: fess-crawler-lasta reaches it through bcjmail.
  • Checked across every repository in the workspace: msal4j and azure-json appear in fess-ds-microsoft365, but inside its own shade <includes> — it bundles them rather than taking them from the war. No other plugin pom names any of the 15, and no plugin source imports org.codelibs.saml2, org.codelibs.spnego, com.nimbusds, com.microsoft.aad.msal4j, com.ctc.wstx or org.apache.xml.security. deps.xml names none of them, so the child-process class paths under WEB-INF/env are untouched.

google-oauth-client is kept, though only the OIDC authenticator used it. Removing the declaration removes no jar — google-api-client 2.7.2 requests it transitively at 1.36.0 and stays for fess-ds-gsuite — so the only effect would be to move the choice of version out of this pom. Its types extend google-http-client's, which is pinned here, and a split between them fails at run time rather than at compile time.

Tests

mvn test: 7770 → 7462, and the difference reconciles exactly.

  • −312 in the eight test classes that move to the plugins.
  • −1 from merging two FessLoginAssistTest cases. They used SamlCredential.SamlUser and OpenIdConnectCredential.OpenIdUser as two stand-ins for the same thing — any FessUser that is not the local document, which is all needsLoginSessionSyncCheck asks — so they are now one test with a local anonymous FessUser.
  • +5 in SsoManagerTest for the warning: the message content, once-per-type, the aad mapping naming fess-sso-entraid, silence for blank/none/null, and the classpath assertion above.

mvn javadoc:jar passes, which CI runs and which needed AdminSysteminfoAction's {@link} to EntraIdAuthenticator turned into {@code} plus where it went. Ten remaining mentions of the moved class names are prose that is still true, and naming the class is how a reader finds the behaviour.

Runtime verification

A LastaDi container was booted on a plain JVM with only fess_sso.xml, on a class path of this branch's WEB-INF/classes, this branch's runtime jars (no SSO SDK), and the fess-sso-entraid jar:

core fess_sso.xml   : .../wt-fess-sso/target/classes/fess_sso.xml
core fess_sso++.xml : jar:.../fess-sso-entraid-15.9.0-SNAPSHOT.jar!/fess_sso++.xml
container init OK
PASS  entraidAuthenticator resolves to the plugin class
PASS  entraidAuthenticator is an SsoAuthenticator
PASS  entraidAuthenticator is a singleton
PASS  @PostConstruct registered it with ssoManager  [authenticators=1]

fess_sso++.xml resolves from the plugin alone, which is the point of the deletion. Adding a second copy of that file declaring the same component — what a 15.8 war would do — reproduces the reason it cannot be shared:

container init OK
TooManyRegistrationComponentException
[Component Key] entraidAuthenticator
[Registered Components]
  componentName: entraidAuthenticator  definedDiXml: fess_sso.xml
  componentName: entraidAuthenticator  (from the plugin)

The container initializes and the failure lands on getComponent(), so a mismatched pair starts normally and returns 500 from /sso/. That is why the plugins are version-paired and say so in their READMEs.

One consequence beyond SSO: the StAX implementation

woodstox-core and stax2-api reach the war only through xmlsec 4.0.1, which only java-saml
asks for, so they leave with it. Nothing else in the distribution needs them, and nothing else
supplies a replacement: no jar among the remaining 241 runtime artifacts carries a
META-INF/services/javax.xml.stream.* entry
, so XMLInputFactory.newFactory() now resolves to
the JDK implementation instead of Woodstox. The JDK's is a complete implementation, so this is a
change of implementation rather than a loss, but it applies to every installation and not only to
the ones using SAML.

fess-sso-saml deliberately does not ship Woodstox back. Bundling it alone would be worse
than not having it: Woodstox registers itself through that same ServiceLoader entry while its
stax2-api companion is not shaded, so XMLInputFactory.newFactory() would then throw
ClassNotFoundException: org.codehaus.stax2.XMLInputFactory2 for the whole webapp -- reproduced
while building the plugin. Bundling both would instead make the webapp's XML parser depend on
whether an SSO plugin happens to be installed. Excluding it keeps one answer either way, and
java-saml never uses the StAX API: scanning every class in java-saml-3.1.2.jar and
java-saml-core-3.1.2.jar finds zero references to javax/xml/stream, org/codehaus/stax2,
com/ctc/wstx or org/apache/xml/security/stax -- it uses Santuario's DOM API.

The ten DI-only tunables

fess_sso++.xml carried ten commented <property> entries on samlAuthenticator and entraidAuthenticatormaxRequestIds, maxStates, the Graph timeouts, the group cache bounds and so on. They move to the plugins exactly as they were, and none is promoted to a configuration key: none appears anywhere in the seven documentation trees, so they are internal defaults rather than settings anyone was told about.

Left alone deliberately

admin_general.jsp still offers all four sso.type values with no hint that a plugin is required; the log line above is what reports it. An empty sso.type still makes available() return true — existing assertions pin that — and stays quiet. bcprov-jdk18on's direct declaration is untouched; whether its version pin existed for java-saml is unverified. fess-parent still defines java.saml.version, spnego.version, msal4j.version and oauth2.oidc.sdk.version, which this pom no longer references and the plugins now need.

All four authenticators leave the distribution: saml, spnego, entraid and oic
become fess-sso-saml, fess-sso-spnego, fess-sso-entraid and fess-sso-oidc.
Together they were 15 jars of third-party SDKs -- java-saml with xmlsec and
woodstox, msal4j, the Nimbus OAuth2/OIDC stack, and the SPNEGO library -- on
four independent release cadences, in a distribution where most installations
configure none of them.

What core keeps is the mapping from an sso.type value to the component name
<sso.type>Authenticator, so a type core has never heard of reaches a plugin
that registers that name. fess_sso.xml keeps ssoManager and the ++ merge point;
fess_sso++.xml is deleted, because a component of the same name in the war and
in a plugin makes getComponent throw TooManyRegistrationComponentException and
runs @PostConstruct twice, and fess_sso+<component>.xml cannot override it
either -- RedefinableComponentTagHandler.redefine() refuses a base path
containing '+'. A test asserts core ships the base file and no ++ file.

SsoAction stays: FessSearchAction references SsoAction.class, and the SAML
responses it serves are redirects and a metadata stream rather than JSP. The
administration screens stay too, because a plugin jar is mounted at
/WEB-INF/classes and cannot supply a JSP -- so admin_general.jsp keeps offering
all four types, and the labels and the 47 form fields stay with it.

The four credential classes move with their authenticators, out of
org.codelibs.fess.app.web.base.login and into org.codelibs.fess.sso.<name>.
Nothing resolved them by name: FessLoginAssist handles LocalUserCredential and
delegates every other type to auth.resolveCredential(), and they implement only
LoginCredential and FessCredential, which stay. Keeping them under
org.codelibs.fess.app would have put plugin classes in the smart-deploy scan
path for no benefit.

getAuthenticator() answered null and said nothing, at any log level. Every
caller turns that null into a redirect, so the entire symptom was a GET /sso/
answering 302 to /login/ with a configuration that is complete and correct --
what is missing is a plugin. That became the ordinary upgrade path with this
change, so SsoManager now warns once per sso.type, naming the component it
looked for and the plugin that provides it. Blank and none say nothing: /sso/
is reachable whether or not SSO is configured, and it is anonymous, so the
warning is deduplicated to keep an unauthenticated client from filling the log.

Warning, not error: an ERROR line is a notification trigger in Fess, and this
is an installation that has not finished rather than a fault. The wording
follows the exception StorageClientFactory writes for the same situation.

mvn dependency:list -DincludeScope=runtime before and after: 256 runtime
artifacts to 241. Exactly 15 removals, no additions, no version changes,
totalling 5,827,073 bytes.

commons-text stays -- java-saml is one of three requesters, with fess-crawler
and handlebars. woodstox-core and stax2-api do leave, reached only through
xmlsec 4.0.1, and no artifact left in the war needs a StAX implementation.
bcprov-jdk18on is unaffected: fess-crawler-lasta reaches it through bcjmail.
Checked across every repository in the workspace: msal4j and azure-json appear
in fess-ds-microsoft365, but inside its own shade includes -- it bundles them
rather than taking them from the war. No other plugin pom names any of the 15,
and no plugin source imports org.codelibs.saml2, org.codelibs.spnego,
com.nimbusds, com.microsoft.aad.msal4j, com.ctc.wstx or
org.apache.xml.security. deps.xml names none of them, so the child-process
class paths under WEB-INF/env are untouched.

google-oauth-client is kept, though only the OIDC authenticator used it.
Removing the declaration removes no jar -- google-api-client 2.7.2 requests it
transitively at 1.36.0 and stays for fess-ds-gsuite -- so the only effect would
be to move the choice of version out of this pom. Its types extend
google-http-client's, which is pinned here, and a split between them fails at
run time rather than at compile time.

mvn test: 7770 tests to 7462. The difference reconciles exactly: 312 in the
eight test classes that moved to the plugins, one from merging two
FessLoginAssistTest cases that used SamlCredential.SamlUser and
OpenIdConnectCredential.OpenIdUser as two stand-ins for the same thing -- any
FessUser that is not the local document -- and five added to SsoManagerTest for
the warning. mvn javadoc:jar passes, which needed AdminSysteminfoAction's
{@link} to EntraIdAuthenticator turned into {@code} plus where it went.

The ten DI-only tunables fess_sso++.xml carried, on samlAuthenticator and
entraidAuthenticator, move to the plugins as commented properties exactly as
they were. None is promoted to a configuration key: none appears anywhere in
the seven documentation trees, so they are internal defaults rather than
settings anyone was told about.
@marevol
marevol force-pushed the feat/extract-sso-authenticators branch from 2b3d8d1 to 2e19f79 Compare September 10, 2026 04:25
@marevol
marevol merged commit 105e876 into master Sep 10, 2026
2 checks passed
marevol added a commit to codelibs/fess-workspace that referenced this pull request Sep 10, 2026
…sitories (#35)

15.9 moves the S3 and GCS storage clients and the four SSO authenticators out of
the Fess distribution, so sets/all.yaml now clones and builds the six repos that
carry them: fess-storage-s3, fess-storage-gcs, fess-sso-saml, fess-sso-spnego,
fess-sso-entraid and fess-sso-oidc. All six default to main. That takes the set
from 48 repositories to 54 -- fess-storage-gcs had never been added, so this
covers the GCS split as well as the ones just extracted.

README.md gains a table per group, with the sso.type / storage.type value each
plugin serves. fess-sso-oidc is the one place the repo name and the type differ,
since the type stays oic. The prose says what core keeps: SsoManager resolving
<sso.type>Authenticator, StorageClientFactory resolving
<storage.type>StorageClient, with the implementation and the SDK in the plugin,
and the s3:/gcs: crawler client registered by the plugin around a class that
stays in fess-crawler. plugins.yaml is left alone: it covers data stores only,
which the README now says outright rather than leaving a reader to infer it.

CLAUDE.md records where the code went, so a grep of repos/fess that comes up
empty is not a surprise: the storage half is already on master, the SSO half is
codelibs/fess#3430. It also records the version-pairing constraint -- an SSO
plugin from a different minor than the war registers the same component name
twice, because core ships its own fess_sso++.xml up to 15.8 and LastaDi merges
every ++ file on the class path, so container.init() succeeds and getComponent()
then throws TooManyRegistrationComponentException. Storage has no counterpart
before 15.9: fess_storage.xml and the <storage.type>StorageClient lookup are
both new. And a plugin repo's prefix is also its PluginHelper.ArtifactType,
where STORAGE and SSO were added in 15.9.

Skills:
- fess-doc-review/refs/source-mapping.md: the four SSO rows and three new
  storage rows point at the plugin repos, each naming what stayed in core --
  sso.type and the entraid keys with their aad fallback in FessProp,
  StorageClientFactory's lookup, StorageType's endpoint detection -- plus a note
  on where the same classes live when reviewing 15.8 or earlier docs. The OIDC
  credential also changed package, out of org.codelibs.fess.app.web.base.login.
- fess-version-bump: fess-sso-* and fess-storage-* belong to wave 5. The script
  derives its list from all.yaml, so only the documented wave needed an edit.
- fess-tech-doc: both plugin families added to the "where to look" list.

Verified by parsing sets/all.yaml with the workspace's own yaml_parser.sh (54
repositories, the six resolving to main), cloning all six, and running
status.sh --short.
marevol added a commit to codelibs/fess-storage-gcs that referenced this pull request Sep 15, 2026
Building a Cloud Storage client fails on current Fess main with
NoClassDefFoundError: com/google/protobuf/MessageOrBuilder, raised from
DefaultStorageRetryStrategy.<clinit>. Both GcsStorageClient and
fess-crawler's GcsClient build through it, so the storage page, uploads
and gcs: crawling all break once the plugin is installed.

protobuf-java reached the war through org.opensearch:opensearch, which
codelibs/fess#3438 and codelibs/fess#3439 took out. Stop excluding it so
the SDK's protobuf-java 4.33.6 is shaded.

The comment claimed every excluded artifact is still in the war. That is
no longer true of woodstox-core and stax2-api either; they left with
codelibs/fess#3430. They stay excluded because only jackson-dataformat-xml
uses them and neither GcsStorageClient nor the gcs: crawler client reaches
it, and the comment now says so.
@marevol
marevol deleted the feat/extract-sso-authenticators branch September 16, 2026 13:36
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