Skip to content

Fix for when Grails 8 returns 500 error instead of 401 for an unauthenticated request - #16654

Merged
jdaugherty merged 2 commits into
apache:8.0.xfrom
ruthst00:fix/16632-unauthenticated-request
Oct 16, 2026
Merged

jdaugherty merged 2 commits into
apache:8.0.xfrom
ruthst00:fix/16632-unauthenticated-request

Conversation

@ruthst00

Copy link
Copy Markdown
Contributor

Description

Cause

The compatibility FilterSecurityInterceptor passed null authentication to authorization voters, causing an AssertionError instead of a handled authentication failure.

Fix

It now throws AuthenticationCredentialsNotFoundException before invoking the decision manager. This lets the configured exception-translation filter return 401 for API entry points, while preserving login redirects and public-request behavior.

Coverage

  • Reproduced the reported failure before applying the fix.
  • Added tests for missing authentication, HTTP 401/200/403 responses, public requests, access denial, and committed responses.
  • Updated filter-chain documentation.

Fixes #16632

Generated with GPT 6 Astra via Cline API Provider in IntelliJ IDEA 2025.3.3

Contributor Checklist

Please review the following checklist before submitting your pull request. Pull requests that do not meet these requirements may be closed without review.

Issue and Scope

  • This PR is linked to an existing issue that has been acknowledged or approved by the project team. If no approved issue exists, please give background on why this change is necessary. Tickets are preferred for release change log history.
  • This PR addresses the complete scope of the linked issue. Partial implementations or unfinished work should not be submitted for review.
  • This PR contains a single, focused change. Unrelated changes should be submitted as separate pull requests.
  • This PR targets the correct branch for the type of change:
    • Patch release branches (e.g., 7.0.x): Bug fixes only. No new features or API changes.
    • Minor release branches (e.g., 7.1.x): New features are welcome, but breaking existing APIs must be avoided.
    • Major release branches (e.g., 8.0.x): Reserved for major changes. Breaking API changes are permitted.

Code Quality

  • I have added or updated tests that cover the changes introduced in this PR. All code contributions are expected to include appropriate test coverage.
  • I have verified that all existing tests pass by running ./gradlew build --rerun-tasks.
  • My code follows the project's code style guidelines. I have run ./gradlew codeStyle and resolved any violations. See Code Style for details.
  • This PR does not include mass reformatting, style-only changes, or large-scale refactoring unless it was explicitly approved in the linked issue. Unsolicited reformatting will not be accepted.
  • If generative AI tooling was used in preparing this contribution, a quality model was used to ensure contributions are consistent with the project's quality standards.

Licensing and Attribution

Documentation

  • If this PR introduces user-facing changes, I have included or updated the relevant documentation.
  • If this PR adds a new feature, I have updated the What's New section of the Grails Guide.
  • If this PR introduces breaking changes or changes that require user action during an upgrade, I have updated the Upgrade Notes for the corresponding version in the Grails Guide.
  • The PR description clearly explains what was changed and why.

### Cause and fix
The compatibility `FilterSecurityInterceptor` passed null authentication to authorization voters, causing an `AssertionError` instead of a handled authentication failure.

It now throws `AuthenticationCredentialsNotFoundException` before invoking the decision manager. This lets the configured exception-translation filter return 401 for API entry points, while preserving login redirects and public-request behavior.

### Coverage
- Reproduced the reported failure before applying the fix.
- Added tests for missing authentication, HTTP 401/200/403 responses, public requests, access denial, and committed responses.
- Updated filter-chain documentation.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create a functional test for this issue instead of a targeted change. Can you please reproduce and then I'll review?

…16632)

A filter chain that drops anonymousAuthenticationFilter reaches the
filter security interceptor with no Authentication in the security
context. The core functional-test-app gains a noAnonymous TESTCONFIG
whose Basic Auth and form-login chains drop that filter, and a spec that
expects the configured entry point to answer such requests (401 with a
Basic challenge, or a redirect to the login page), that valid Basic Auth
credentials are still authorized or refused through the same chain, and
that a chain which keeps the anonymous filter still serves public URLs.

Against the previous compat FilterSecurityInterceptor the two
unauthenticated cases fail with a 500, which is the regression the issue
reports. The new configuration is added to the securityConfig matrix.
@ruthst00

Copy link
Copy Markdown
Contributor Author

@jdaugherty, thank you for adding the functional test. You beat me to it!

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard is the right shape. It is the check Spring's AbstractSecurityInterceptor.beforeInvocation performed and the one AuthorizationFilter still performs, and the compat MethodSecurityInterceptor already carries the identical check, so this brings the two shims in line.

Verified at b1012aa: :grails-spring-security:test (228 tests) and :grails-spring-security-compat:test (16) pass, as does codeStyle for both modules. With the 8.0.x version of FilterSecurityInterceptor.groovy swapped back in, the two "missing authentication" cases of the new spec fail with the AssertionError from the issue and the other seven pass.

I pushed the functional reproduction to this branch in f899432: a noAnonymous TESTCONFIG for the core functional-test-app whose Basic Auth and form-login chains drop anonymousAuthenticationFilter, with a spec that expects a 401 with the Basic challenge, a redirect to the login page, 200 and 403 for valid Basic credentials with and without the role, and 200 for a public URL on a chain that keeps the filter. Against the 8.0.x interceptor the two unauthenticated cases get a 500. The configuration is added to the securityConfig matrix in gradle.yml.

I will merge this once the tests finish locally.

}

def authentication = SecurityContextHolder.context?.authentication
if (authentication == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same guard and same message as the compat MethodSecurityInterceptor, which is the consistency we want between the two shims.

Probe: with the 8.0.x version of this file, the missing authentication row of the new chain spec fails with java.lang.AssertionError: authentication cannot be null out of WebExpressionVoter.vote, the exact stack from #16632. With this change it is a 401.

@jdaugherty
jdaugherty merged commit a2f8c0a into apache:8.0.x Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

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

2 participants