Skip to content

fix(sso): correct SPNEGO logging, config fallback and dead settings - #3216

Open
marevol wants to merge 1 commit into
masterfrom
fix/spnego-cleanup
Open

fix(sso): correct SPNEGO logging, config fallback and dead settings#3216
marevol wants to merge 1 commit into
masterfrom
fix/spnego-cleanup

Conversation

@marevol

@marevol marevol commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Defects found while auditing org.codelibs.fess.sso.spnego against the underlying org.codelibs:spnego library. All of them are self-contained fixes in SpnegoAuthenticator; no behavior that a working SPNEGO deployment depends on is changed.

Changes

Credential prefix leaks through the Authorization-header mask

The mask kept the first 10 characters of the header. "Negotiate " is exactly 10 characters, so a Negotiate token was masked correctly — but "Basic dXNl" retains four base64 characters, which decode to the first three plain bytes of user:password, i.e. the first three characters of the user name. SsoAction#index logs this message at WARN.

Now masked to the scheme token only: Basic ***, Negotiate ***.

A blank spnego.* property is no longer passed through as ""

Properties#getProperty(key, default) returns "" for a key that is present but empty, not the default. AdminGeneralAction#doUpdate writes every spnego.* key on save, so clearing an input field in System → General persists an empty string. That empty string then reached the library, and for the file-path keys it surfaced as EmptyArgumentException swallowed into the generic "Failed to initialize SPNEGO." — losing the specific "configuration file not found" message.

SpnegoConfig#getProperty now treats blank as unset.

spnego.exclude.dirs is no longer advertised

excludeDirs is consumed only by SpnegoHttpFilter. Fess does not install that filter — it calls SpnegoAuthenticator#authenticate directly from getLoginCredential — so an operator listing /api/ got no exclusion and no warning. The constant and the mapping are removed; the docs are updated separately in codelibs/fess-docs.

spnego.logger.level "0" raised the level instead of silencing it

The library's setLogLevel switch has cases for 1/2/3/4/6/7 and default: Level.INFO — no case for 0. getInitParameter returned "0" when none of the Log4j levels were enabled, so the branch intended to silence the library set it to INFO. It now returns "7" (SEVERE), the quietest level the switch actually understands.

The 401 challenge no longer logs on the normal path

The library's SpnegoHttpServletResponse#setStatus(401, true) sets content-length 0 and flushes, so the response is already committed when RequestClientErrorException is thrown. LastaFlute's handleClientError sees beforeHandlingCommitted == true and logs *Cannot send error as '401 ...' because of already committed at INFO — on every SPNEGO handshake, i.e. on the normal login path.

Functionally harmless (LastaFlute guards isCommitted()), so this keeps the existing control flow and just marks the exception .asLogging(DelicateErrorLoggingLevel.DEBUG). Returning ActionResponse.undefined() instead would be cleaner still, but that changes the response path and is left as a follow-up.

Warn when the permissive settings are actually in effect

spnego.allow.localhost and spnego.allow.unsecure.basic were hardened to false, but a coded default only applies when the key is absent. FessProp#setSystemProperty removes a key only on null, and the admin screen always writes a concrete "true"/"false". An instance where an admin saved System → General before the hardening therefore has spnego.allow.localhost=true persisted in system.properties, and the new default never fires.

warnInsecureSettings now logs a WARN at initialization for both, so the condition is visible in the log instead of silent.

Lifecycle and message fixes

  • authenticator is volatile and destroy() is synchronized. getAuthenticator() holds the monitor, but @PreDestroy destroy() read and cleared the same field without it.
  • The two UnsupportedOperationException messages said "not supported in SpnegoFilterConfig"; the class that throws them is SpnegoConfig.

Tests

SpnegoAuthenticatorTest: Tests run: 17, Failures: 0, Errors: 0.

Two tautological tests are replaced (test_constantsExist and test_innerClassNaming asserted that SpnegoAuthenticator.class.getSimpleName() equals "SpnegoAuthenticator"). New coverage: the header mask across null / Negotiate / Basic / no-separator inputs, the blank-property fallback, the exclude.dirs mapping, and the exception messages.

Not included

  • Documentation corrections (default-value table, krb5.conf encryption types, spnego.allowed.realms) — codelibs/fess-docs.
  • Admin-screen validation and the spnego.allowed.realms field — separate PR.

Fixes several defects found while auditing the SPNEGO authenticator against the
underlying spnego library.

- Mask the Authorization header down to its scheme. The previous mask kept the
  first 10 characters, which for "Basic dXNl..." retains four base64 characters
  and therefore the first three plain bytes of "user:password". SsoAction logs
  this message at WARN.
- Treat a blank spnego.* system property as unset. The admin screen writes every
  key on save, so clearing an input stores an empty string; that empty string
  reached the library and turned a simple misconfiguration into an opaque
  "Failed to initialize SPNEGO." with the specific cause lost.
- Stop advertising spnego.exclude.dirs. Only SpnegoHttpFilter consumes it, and
  Fess calls SpnegoAuthenticator#authenticate directly instead of installing
  that filter, so the value never excluded anything.
- Return "7" (SEVERE) instead of "0" when no log level is enabled. The library's
  setLogLevel switch has no case for 0, so it fell through to INFO and the
  branch meant to silence the library actually raised its level.
- Log the 401 challenge at debug level. The library has already written and
  flushed the 401 with its WWW-Authenticate header, so LastaFlute reported
  "Cannot send error ... because of already committed" at INFO on every single
  SPNEGO handshake.
- Warn at initialization when spnego.allow.localhost or spnego.allow.unsecure.basic
  is enabled. The hardened defaults only apply when the key is absent, so an
  instance that stored the old permissive values keeps using them silently.
- Make the authenticator field volatile and synchronize destroy(), which read and
  cleared it outside the monitor held by getAuthenticator().
- Name SpnegoConfig, not SpnegoFilterConfig, in the two UnsupportedOperationException
  messages thrown by SpnegoConfig itself.

Replaces two tautological tests with coverage for the header mask, the blank
property fallback and the exclude.dirs mapping. Tests run: 17, Failures: 0.
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