WebDAV digest authentication broken on Jmix 3.0.1 - works on Jmix 1.x and 2.x

Hello,

We have hit a WebDAV problem on Jmix 3.0.1 that we cannot work around ourselves, and
we would be grateful for your guidance.

When a user opens a WebDAV document from Microsoft Word, it never opens. The server
sends a Digest challenge, Word answers it, and Spring Security logs the authentication
as successful. But the authorization check that runs immediately afterwards - same
thread, same request, about a millisecond later - sees an AnonymousAuthenticationToken
instead of the user. AuthorizationFilter denies the request, ExceptionTranslationFilter
calls the entry point again, a new challenge goes out, Word answers that one too, and
the same thing happens again. It loops forever, and Word eventually shows “Access Denied”.

The important part is this: the same application code, the same configuration, the same
certificate and the same Microsoft Office installation work perfectly on Jmix 1.x and on
Jmix 2.x. Only Jmix 3.0.1 fails.

That comparison rules out most of the obvious explanations. Jmix 2 is already flowui, so
the UI framework is not the difference. Jmix 2 is already on Spring Security 6, where
SecurityContextHolderFilter and the explicit SecurityContextRepository save model were
introduced, and digest works there - so that change is not the difference either. What
is left between our last working version and the first broken one is the Spring Boot 3
to 4 / Spring Security 6 to 7 step, or a change in how the Jmix 3 WebDAV starter builds
its filter chain.

Here is what the log shows. First, authentication succeeding:

DEBUG [nio-8443-exec-4] o.s.s.w.a.w.DigestAuthenticationFilter
  : Digest Authorization header received from user agent:
    Digest username="admin",realm="webdav",...
DEBUG [nio-8443-exec-4] o.s.s.w.a.w.DigestAuthenticationFilter
  : Extracted username: 'admin'; realm: 'webdav'; ...
DEBUG [nio-8443-exec-4] o.s.s.w.a.w.DigestAuthenticationFilter
  : Authentication success for user: 'admin'
    with response: '9b624b9cf6af37eb3b391703a49f74c9'

Then, on the same thread and the same request:

TRACE [nio-8443-exec-4] RequestMatcherDelegatingAuthorizationManager
  : Authorizing OPTIONS /webdav/document/<document-id>/
TRACE [nio-8443-exec-4] RequestMatcherDelegatingAuthorizationManager
  : Checking authorization on OPTIONS /webdav/document/<document-id>/
    using AuthenticatedAuthorizationManager@2cb4e04
TRACE [nio-8443-exec-4] o.s.s.w.a.ExceptionTranslationFilter
  : Sending AnonymousAuthenticationToken [Principal=anonymousUser,
    Credentials=[PROTECTED], Authenticated=true,
    Details=WebAuthenticationDetails [...],
    Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point
    since access is denied
org.springframework.security.authorization.AuthorizationDeniedException: Access Denied
    at org.springframework.security.web.access.intercept.AuthorizationFilter
       .doFilter(AuthorizationFilter.java:99) ~[spring-security-web-7.0.6.jar:7.0.6]

And then a fresh challenge, which starts the loop again:

DEBUG [nio-8443-exec-4] s.s.w.a.w.DigestAuthenticationEntryPoint
  : WWW-Authenticate header sent to user agent: Digest realm="webdav", ...

The filter order for that request, taken from the stack trace, outermost first, is:

SecurityContextHolderFilter -> ... -> LogoutFilter -> DigestAuthenticationFilter
  -> RequestCacheAwareFilter -> SecurityContextHolderAwareRequestFilter
  -> AnonymousAuthenticationFilter -> ExceptionTranslationFilter -> AuthorizationFilter

AnonymousAuthenticationFilter only populates the anonymous token when the context is
empty, so it looks as though the principal set by DigestAuthenticationFilter is not
visible to the filters that come after it.

Our configuration is:

jmix.webdav.url-prefix=https://<host>:8443/webdav
jmix.webdav.authentication-method=digest
jmix.webdav.send-document-events=true

We use jmix-webdav-starter, jmix-webdav-flowui-starter and jmix-webdav-rest-starter, on
JDK 21 with embedded Tomcat 11.0.22, HTTPS on port 8443, with a certificate whose CN and
SAN match the host in url-prefix. Passwords are {noop} for the seeded admin account and
BCrypt for users created through the UI - exactly as in our Jmix 1.x and 2.x applications
that work.

We have already ruled out the following. The URL and certificate are correct, and a
browser reaches the same URL and is challenged normally. The credentials are correct -
the log says “Authentication success”. Password storage is not the issue, since the same
storage works on 1.x and 2.x. Jmix roles are not the issue, because the denial comes from
AuthenticatedAuthorizationManager, which only asks whether anyone is authenticated at
all, before any resource role is consulted. And the client is not the issue, because the
same Office installation on the same workstation opens documents from our Jmix 1.x and
2.x applications.

We would like to ask three things.

First, is this a known regression in the Jmix 3 WebDAV starter, and is a fix planned?

Second, is there a supported way to make the authentication set by
DigestAuthenticationFilter survive to AuthorizationFilter - for example by contributing
our own SecurityFilterChain for /webdav/** with an explicit SecurityContextRepository?
If that is a sanctioned workaround we would much rather apply it than wait for a release.
But this application is a GxP-regulated quality management system, and we are not willing
to hand-roll security configuration without your confirmation that it is the right
approach.

Third, if digest is simply no longer supported on Jmix 3, could the add-on documentation
say so? Basic is not a usable alternative for us. Microsoft 365 Apps block Basic
authentication sign-in prompts by default, and the only remedy on the client side is a
per-workstation registry or Group Policy change, which our regulated customers will not
accept.

We are happy to send the full trace log, put together a minimal reproduction project, or
join a screen-share, whichever is most useful to you.

Thank you for your help.

Hello,

Thanks for reporting.

It’s looks like it’s not because of the Spring Security itself, but because of behavior change in Vaadin 25.

WebdavRestDigestAuthConfiguration creates DigestAuthenticationFilter via new, so it captures the static SecurityContextHolder strategy at construction time. All filters built by HttpSecurity resolve the strategy from the unique SecurityContextHolderStrategy bean instead (Vaadin’s VaadinAwareSecurityContextHolderStrategy, which has its own separate ThreadLocal).

  • Vaadin 24 (Jmix 2.x): the strategy bean’s factory method callsSecurityContextHolder.setContextHolderStrategy(...) immediately at bean creation (before security chains are built). The digest filter captures the same instance as the rest of the chain so everything works.
  • Vaadin 25 (Jmix 3.x): the static install is deferred to a SmartInitializingSingleton, so it waits until all SecurityFilterChain beans are built. The digest filter captures the default strategy and writes the authenticated context into a ThreadLocal the rest of the chain never reads. So AuthorizationFilter sees an anonymous token and causes this issue.

Fix - is basically setting a correct strategy.

You can add it as a WA on project side.
Create a custom WebdavRestDigestAuthConfiguration configuration and override filter creation:

@EnableWebSecurity
@Configuration
public class CustomWebdavRestDigestAuthConfiguration extends WebdavRestDigestAuthConfiguration {

    @Autowired
    protected ObjectProvider<SecurityContextHolderStrategy> securityContextHolderStrategyProvider;

    @Override
    public DigestAuthenticationFilter digestAuthenticationFilter(DigestAuthenticationEntryPoint entryPoint) {
        DigestAuthenticationFilter filter = super.digestAuthenticationFilter(entryPoint);
        filter.setSecurityContextHolderStrategy(
                securityContextHolderStrategyProvider.getIfUnique(SecurityContextHolder::getContextHolderStrategy));
        return filter;
    }
}

Most configurations provided by Jmix Framework are designed to be overridden if necessary on a project side, so such approach is correct.

Please test this WA on your side to make sure there are no “extra” issues.

Regards,
Ivan

Hi Ivan,

Confirmed working — thank you. Documents now open in Word from Jmix 3.0.1 with digest authentication, and the authenticated principal reaches AuthorizationFilter as it should.

Your diagnosis was exactly right, and the Vaadin 24 → 25 change in when the strategy is installed statically is not something we would have found on our own. We had been looking at the Spring Security 6 → 7 upgrade, which turned out to be the wrong suspect entirely.

We applied the workaround as a subclass in our own security package, with two small additions:

  • @Bean on the overridden method, to be explicit rather than relying on the annotation being inherited;

  • the same @ConditionalOnProperty(prefix = "jmix.webdav", name = "authentication-method", havingValue = "digest") that DefaultWebdavRestDigestAuthConfiguration carries — without it our configuration would install a digest chain even on a deployment configured for basic.

No side effects so far: upload, open, edit, save and check-in all behave normally, and the rest of the application’s security is unaffected.

Two questions, if you don’t mind:

  1. Is a fix planned in the add-on itself, so we can drop this override at that version? We would rather not carry a permanent copy of framework configuration in a validated system.

  2. Are other Jmix filters built with new rather than through HttpSecurity likely to hit the same Vaadin 25 timing issue? If so we would like to check them before they surface in production.

Thanks again for the quick and precise answer.

Best regards,
Thaer
Qtes Soft

Hi Thaer,

About your additions:

  • Explicit @Bean annotation of the overridden method.
    I don’t get it. The original method you override doesn’t have the @Bean annotation - it’s just “internal” method to create filter. And within overridden method you just add an extra setting.
    The original configuration has the @Bean annotation on SecurityFilterChain and DigestAuthenticationEntryPoint methods, but not the DigestAuthenticationFilter one.
  • Adding @ConditionalOnProperty is fine.

About your questions:

  1. Yes, fix will be done in Webdav add-on. Basically, we will do the same but in the original configuration.
  2. For now I can’t find any other filter with the same issue within the Jmix Framework.
    But if your project have some custom filters created by new - then the similar issue is theoretically possible. The main criteria of “vulnerability” - it caches strategy during construction (like ... = SecurityContextHolder.getContextHolderStrategy()) and created by new.

Regards,
Ivan

Hi Ivan,

You’re right, and thank you for catching it — I had assumed digestAuthenticationFilter was a bean method. I checked the class and it isn’t: only securityFilterChain and digestEntryPoint carry @Bean. Adding it would have published a DigestAuthenticationFilter bean, which Spring Boot auto-registers with the servlet container for every URL — putting digest authentication in front of the whole application rather than /webdav/**. It went unnoticed because requests without an Authorization header pass through untouched.

Removed. We now have only your override plus the @ConditionalOnProperty.

Good to hear the fix is coming to the add-on itself — we’ll drop our subclass at that version. Could you let us know the release number when it’s decided? This is a GxP-regulated system, so the override and its removal both need recording in our validation file, and pointing at a specific version makes that clean.

Thank you also for the “vulnerability” criteria — cached strategy at construction plus new. That’s a precise thing to grep for. We’ll audit our own filters against it.

Thanks again for the diagnosis. The Vaadin 24 → 25 timing change is not something we would have found ourselves; we had been looking at the Spring Security 6 → 7 upgrade, which was the wrong suspect entirely.

Best regards,

1 Like

Hi Thaer,

Fix will be delivered in the Jmix 3.0.2 this week.

Regards,
Ivan

1 Like