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]