Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Grails 8 returning 500 error instead of 401 for an unauthenticated request

オープン
#16,632 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@ruthst00 がすでに取り組んでいます。

2026年10月15日 から。

  • #16654 @ruthst00 による — オープン

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
55/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
groovy

調査の方向性

Start with the reproduction repo linked in the issue (tprebs/grails-8-security-401-regression) and run runGrails8.sh to see the 500. The relevant code is the re-implemented FilterSecurityInterceptor and AccessDecisionVoter path in grails-spring-security-compat, where WebExpressionVoter.vote asserts on a null Authentication. Done means an unauthenticated request to a @Secured action returns 401 with no AssertionError, and the Grails 7 behaviour is matched.

索引モデルが issue の本文から書いたものです。

説明

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:

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:

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

主要言語
Groovy
スター
2.9k
フォーク
976
平均マージ
21時間 14分
マージ済み PR(30日)
174

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

apache/grails-core のほかの issue

apache/grails-core の issue をすべて見る

似ている issue

Security の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。