tprebs opened a new issue, #16632:
URL: https://github.com/apache/grails-core/issues/16632

   ### Expected Behavior
   
   An unauthenticated request to a `@Secured` controller should be answered 
with a `401 Unauthorized`. When no authentication is provided grails 8 is 
responding with a 500 error and an page. Previous versions of grails returned 
`401 Unauthorized`
   
   ### Actual Behaviour
   
   On 8.0.0 the same request is answered with `500 Internal Server Error` and 
an HTML error page, and the server log carries an `AssertionError`.
   
   This happens when the filter chain legitimately contains no anonymous 
authentication filter, so the request reaches the authorisation interceptor 
with a null `Authentication`. That is ordinary, documented
   configuration, the Spring Security Core plugin's `filters.adoc` describes 
composing a chain with
   `JOINED_FILTERS` and filter names prefixed with `-`, and removing 
`anonymousAuthenticationFilter` is a
   deliberate choice for a stateless API that must not grant anonymous 
identities:
   
   ```groovy
   grails.plugin.springsecurity.filterChain.chainMap = [
           [
                   pattern: '/**',
                   filters: 
'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,' +
                           
'-authenticationProcessingFilter,-securityContextPersistenceFilter,' +
                           '-rememberMeAuthenticationFilter'
           ],
   ]
   ```
   
   Grails builds with no anonymous filter, as configured:
   
   ```
   Creating filter chain: Ant [pattern='/**'],
     [SecurityRequestHolderFilter, MutableLogoutFilter, 
RestAuthenticationFilter,
      SecurityContextHolderAwareRequestFilter, RestTokenValidationFilter,
      ExceptionTranslationFilter, FilterSecurityInterceptor]
   ```
   
   An unauthenticated `GET /secret` then produces:
   
   ```
   ERROR o.a.c.c.C.[.[.[.[dispatcherServletRegistration] : Servlet.service() 
for servlet
     [dispatcherServletRegistration] threw exception [Filter execution threw an 
exception] with root cause
   
   java.lang.AssertionError: authentication cannot be null. Expression: 
authentication. Values: authentication = null
       at 
grails.plugin.springsecurity.web.access.expression.WebExpressionVoter.vote(WebExpressionVoter.groovy:47)
       at 
grails.plugin.springsecurity.access.vote.AuthenticatedVetoableDecisionManager.decide(...)
       at 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.groovy:54)
   
   ERROR o.a.c.c.C.[Tomcat].[localhost] : Exception Processing 
[ErrorPage[errorCode=500, location=/error]]
   ```
   ### Cause
   
   Spring Security 7.1.1 removed `FilterSecurityInterceptor` and the 
`AccessDecisionManager`/`AccessDecisionVoter`
   API. Grails 8 ships `org.apache.grails:grails-spring-security-compat` to 
re-implement those classes in Spring
   Security's own package namespace, which is why the stack frame above reads 
`FilterSecurityInterceptor.groovy`
   rather than Spring's Java original.
   
   The re-implementation omits the null-`Authentication` guard. Spring 
Security's
   `AbstractSecurityInterceptor.authenticateIfRequired()` begins:
   
   ```java
   Authentication authentication = 
SecurityContextHolder.getContext().getAuthentication();
   if (authentication == null) {
       throw new AuthenticationCredentialsNotFoundException(
           "An Authentication object was not found in the SecurityContext");
   }
   ```
   
   `AuthenticationCredentialsNotFoundException` extends 
`AuthenticationException`, so
   `ExceptionTranslationFilter` catches it and the entry point returns 401. 
That is the path that works on
   Grails 7.
   
   In `grails-spring-security-compat:8.0.0` there is no null guard, and the 
null `Authentication` is passed to the decision manager and reaches 
`WebExpressionVoter`, whose Groovy `assert` throws an `AssertionError`. That is 
an `Error`, not an `AuthenticationException`, so `ExceptionTranslationFilter` 
does not catch it and the request fails as a 500.
   
   ### Steps To Reproduce
   
   1. Create a Grails 8.0.0 application with `grails-spring-security` and 
`grails-spring-security-rest`.
   2. Add one controller with `@Secured(['ROLE_USER'])`.
   3. Set the catch-all chain in `grails-app/conf/application.groovy`:
      `[pattern: '/**', filters: 
'JOINED_FILTERS,-anonymousAuthenticationFilter']` (the other exclusions above
      are not required to trigger it; removing the anonymous filter is).
   4. Request the secured action with no `Authorization` header: `curl -i 
http://localhost:8080/secret`.
   5. Observe `HTTP 500` and an HTML error page. Expected `401`.
   6. Check the server log for `java.lang.AssertionError: authentication cannot 
be null` originating in
      `WebExpressionVoter.vote`.
   7. Run the same application on Grails 7.2.3 and the request is answered with 
`401`.
   
   recreation available here 
https://github.com/tprebs/grails-8-security-401-regression
   
   A minimal Grails application with one `@Secured` controller and two scripts:
   
   ```
   ./runGrails7.sh     # passes - answered with 401
   ./runGrails8.sh     # fails  - answered with 500
   ```
   
   
   ### Environment Information
   
   - Grails 8.0.0
   - Gradle 9.8.0, 
   - Java 21
   -  macOS
   
   ### Example Application
   
   https://github.com/tprebs/grails-8-security-401-regression
   
   ### Version
   
   8.0.0


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to