https://bz.apache.org/bugzilla/show_bug.cgi?id=70203
Bug ID: 70203
Summary: Jakarta Authentication provider is ignored after a
context restart, because
`AuthConfigFactoryImpl.getConfigProvider()` discards
the `RegistrationListener` when no registration exists
Product: Tomcat 11
Version: 11.0.22
Hardware: PC
Status: NEW
Severity: normal
Priority: P2
Component: Catalina
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: -------
**Tomcat version:** 11.0.22
**Java:** Eclipse Adoptium JDK 21.0.12.1+1-LTS
**Connector:** `http-nio-8080`, no fronting web server
### What happens
A web application registers an `AuthConfigProvider` from a
`ServletContextListener`
and removes the registration on `contextDestroyed`. This works on first
deployment.
Stop the context and start it again in the same running Tomcat, and the
provider is
never consulted again. The application registers successfully —
`registerConfigProvider`
returns an id — but no `ServerAuthModule` method is ever invoked, and requests
are
handled as though no Jakarta Authentication provider existed.
### Why
`AuthenticatorBase` caches the provider in `Optional<AuthConfigProvider>
jaspicProvider`
and relies on a `RegistrationListener` to invalidate that cache. The listener
is
lost, so the cache is never invalidated:
1. `AuthenticatorBase.findJaspicProvider()` calls
`factory.getConfigProvider("HttpServlet", jaspicAppContextID, this)`,
passing
itself as the listener.
2. On `contextDestroyed` the application calls `removeRegistration(id)`.
`AuthConfigFactoryImpl.removeRegistration` removes the
`RegistrationContextImpl`
and notifies the listeners **held inside it**, which go away with it.
3. That notification makes `AuthenticatorBase.notify()` re-run
`findJaspicProvider()`. There is now no registration, so
`AuthConfigFactoryImpl.getConfigProvider(layer, appContext, listener)`
returns
`null` **without attaching the listener**:
```java
public AuthConfigProvider getConfigProvider(String layer, String appContext,
RegistrationListener listener) {
RegistrationContextImpl registrationContext =
findRegistrationContextImpl(layer, appContext);
if (registrationContext != null) {
if (listener != null) {
RegistrationListenerWrapper wrapper =
new RegistrationListenerWrapper(layer, appContext,
listener);
registrationContext.addListener(wrapper);
}
return registrationContext.getProvider();
}
return null; // <-- listener discarded
}
```
4. `AuthenticatorBase` stores the result, so `jaspicProvider` is now
`Optional.empty()`. `getJaspicProvider()` tests the field for `null`, and an
empty `Optional` is not `null`, so it never looks the provider up again.
5. Neither `AuthenticatorBase.stopInternal()` (which only clears `sso`) nor
`startInternal()` (which only recomputes `jaspicAppContextID`) clears that
cache, and the valve survives a context stop/start.
6. On restart the application registers a new provider into a factory that now
has
no listener for that application context. Nothing notifies the
authenticator,
and the cached empty result stands for the life of the JVM.
### Where this contradicts the specification
The javadoc of `AuthConfigFactory.getConfigProvider(String, String,
RegistrationListener)`
states, without qualification:
> **"An argument listener is attached even if the return value is null."**
Jakarta Authentication 3.1, §2.1.1.1, describes the mechanism this is the
foundation of:
> "A runtime may continue to reuse a provider for as long as it wishes.
> However, a
> runtime that wishes to be notified of changes to the factory that would cause
> the
> factory to return a different provider for the `layer` and `appContext`
> arguments
> should include a (non-null) `RegistrationListener` as an argument in the call
> used
> to acquire the provider."
> "When a listener argument is included in the call to acquire a provider, the
> factory will invoke the `notify` method of the listener when the
> correspondence
> between the provider and the layer and application context for which it had
> been
> acquired is no longer in effect."
So the caching in `AuthenticatorBase` is explicitly permitted, and the listener
is
the specified means of invalidating it. Discarding the listener removes the
only
mechanism the specification provides for a runtime to learn that a provider has
appeared.
### Still present on main
`java/org/apache/catalina/authenticator/jaspic/AuthConfigFactoryImpl.java` on
`main`
has the same branch. No 11.0.x changelog entry mentions `getConfigProvider`,
`RegistrationListener` or `AuthConfigFactory`.
---
## Steps to reproduce
On a clean Tomcat 11 installation, with a minimal web application:
1. A `ServerAuthModule` whose `validateRequest` logs a line and returns
`AuthStatus.SUCCESS` after setting a caller principal via
`CallerPrincipalCallback`.
2. An `AuthConfigProvider` returning a `ServerAuthConfig` / `ServerAuthContext`
for
that module.
3. A `ServletContextListener`:
- `contextInitialized`:
```java
String appContext = ctx.getVirtualServerName() + " " +
ctx.getContextPath();
registrationId = AuthConfigFactory.getFactory()
.registerConfigProvider(provider, "HttpServlet", appContext,
"test");
```
- `contextDestroyed`:
```java
AuthConfigFactory.getFactory().removeRegistration(registrationId);
```
4. A `<security-constraint>` over some path, so an authenticator valve is
installed.
5. A servlet at any unconstrained path that prints
`request.getUserPrincipal()`.
Then:
- deploy and request the servlet — **the module logs and a principal is set**;
- stop the context (Manager `/stop`, or the equivalent in an IDE) and start it
again,
**without restarting Tomcat**;
- request the servlet again.
**Expected:** the module is invoked, as on first deployment.
**Actual:** the module is never invoked and no principal is set. The
application's
own logging shows the new `registerConfigProvider` succeeding.
The registration and removal are symmetric and correct on the application side;
the
same application works on every fresh Tomcat start and fails on every context
restart.
---
## Suggested direction, rather than a patch
The obvious edit — attach the listener before the `null` return — has nowhere
to
attach it to, because listeners are currently held inside
`RegistrationContextImpl`
and there is no registration in this case. A correct fix needs listeners to be
held
independently of registrations: a collection keyed by (layer, appContext) for
listeners registered while no provider exists, consulted and notified by
`doRegisterConfigProvider` when a matching registration is later added.
The shape of that belongs to whoever maintains this class, so no patch is
attached.
Two smaller changes would each make the failure survivable, and may be worth
having
regardless:
- **`AuthenticatorBase.stopInternal()` could clear `jaspicProvider`.** The
valve
outlives a context restart and there is no reason for a cached lookup to.
With a
compliant factory this would be redundant; without one it turns a permanent
failure into a transient one.
- **`AuthenticatorBase` could distinguish "not looked up" from "looked up and
absent"**,
rather than treating a cached `Optional.empty()` as a final answer.
---
## Is this a security issue?
I believe **no**, and the reasoning should be checked before this is filed
publicly.
The authenticator valve still runs and still enforces `<security-constraint>`.
Without a Jakarta Authentication provider it falls back to whatever
authenticator
`ContextConfig` installed from `<login-config>` — with no `<login-config>` that
is
`NonLoginAuthenticator`, which authenticates nobody. So a constrained resource
should return **403**, not be served unauthenticated: the failure denies rather
than permits.
**Confirm that before filing.** After a context restart, request a constrained
URL
and check it is refused:
```bash
curl -i http://localhost:8080/contextPath/someSecurityConstraintPath
```
A 403 confirms the analysis and Bugzilla is the right channel. A 200 would mean
a
constrained resource is being served without authentication — that is a
vulnerability, must not be posted publicly, and goes to the address on
https://tomcat.apache.org/security.html instead.
Worth noting the case that is greyer and worth a sentence in the report if you
think it applies: an application that declares a `<login-config>` as well as
using
Jakarta Authentication would silently fall back to that other mechanism rather
than
failing closed.
--
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]