oauth2: add allow_failed_matcher for unauthenticated pass-through on auth failure#43027
Conversation
…auth failure This PR adds an allow_failed_matcher field to the OAuth2 filter configuration, enabling requests to proceed to upstream as unauthenticated when OAuth validation fails (missing, invalid, or expired credentials) and the request matches the configured matchers. This feature enables graceful degradation patterns where services can handle both authenticated and unauthenticated requests, similar to the JWT filter's allow_missing_or_failed functionality. Implementation details: - Added allow_failed_matcher field to OAuth2Config proto (field 27) - The matcher is evaluated in the request flow after pass_through_matcher (which skips validation entirely) and before deny_redirect_matcher - OAuth validation is always attempted (unlike pass_through_matcher which skips it) - When OAuth validation fails AND the request matches allow_failed_matcher, all OAuth cookies are stripped from the request before forwarding to upstream - Context headers added: x-envoy-oauth-status: failed and x-envoy-oauth-failure-reason - Added new statistic oauth_allow_failed_passthrough to track when this path is taken - Updated filter logic in filter.cc and oauth_client.cc to handle the new matcher - Added comprehensive unit tests in filter_test.cc and oauth_test.cc The implementation follows the same architectural pattern as other matchers in the OAuth2 filter and maintains backward compatibility (optional field with no behavior change when not configured). Signed-off-by: Mo Chen <carterchen.m@gmail.com>
|
Hi @mchen391, welcome and thank you for your contribution. We will try to review your Pull Request as quickly as possible. In the meantime, please take a look at the contribution guidelines if you have not done so already. |
|
CC @envoyproxy/api-shepherds: Your approval is needed for changes made to |
|
Hi @zhaohuabing, I saw you contributed a lot to this area in the past. Could we borrow your expertise to review this PR cc @wbpcode as code owner as well Thanks! |
Can't this be achieved with Custom Response? Here is an example using Envoy Gateway BackendTrafficPolicy. |
|
@zhaohuabing Unless the local reply is able to forward the request to upstream, no
unfortunately. Imagine the homepage of a website - it will render normally
for anonymous users, but will render tailored content for logged-in users
too. Or imagine a search engine API that gives anonymous users result
(likely from DB) but also gives tailored result to users with credentials.
As a result, we need the request that does not carry any oauth credentials
to still forward to upstream and let upstream handles the request.
|
|
I think we could redirect unauthenticated requests. Since the OAuth2 filter is already fairly complex, it might be better to handle this in a more generalized way rather than adding another knob to the OAuth2 filter. |
@zhaohuabing Sorry I still don't think this would solve the problem. In this case we do NOT want to show the anonymous user an error page, but instead want to show them a normal content but just not tailored for them since they are anonymous. Just imagine the google homepage - you will be able to load google and use its search engine without login completely fine, but can also do that in a logged-in state, with maybe some tailored search result sorting if you search in logged-in state. I don't see a clean way to achieve that with redirects. One thing I can think of is to make a duplicate set of all the APIs that are supposed to be optional auth (could work with or without auth), one require auth and one does not require, and make the former redirect to the latter if auth failed. You could imagine that there are going to tons of endpoints that need to be duplicated and maintained separately, which I don't think is ideal or maintenable. In fact I think the Envoy jwt_authn filter's |
|
Thanks for the clarification — I don’t think you necessarily need a full duplicate setup, but it's true that you’d still need some separate routing or handling logic for unauthenticated vs authenticated requests, which would require changes on the application side and isn’t ideal. In that case, having a pass-through option is really helpful. So this approach seems reasonable to me. |
|
/lgtm api |
This commit addresses a security vulnerability where malicious clients could inject x-envoy-oauth-status and x-envoy-oauth-failure-reason headers that might confuse downstream services about the authentication state of requests. Changes: - Sanitize OAuth status headers (x-envoy-oauth-status and x-envoy-oauth-failure-reason) for all incoming requests in decodeHeaders(), following the same pattern as Authorization header sanitization - Change from addCopy() to setCopy() when setting these headers in continueAsUnauthorized() to ensure headers are replaced rather than appended, providing defense-in-depth - Add test coverage for header injection scenarios in both authenticated and allow_failed code paths The sanitization ensures that only the OAuth2 filter itself can set these headers, preventing header injection attacks at system boundaries. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Mo Chen <carterchen.m@gmail.com>
# Conflicts: # changelogs/current.yaml Signed-off-by: Mo Chen <carterchen.m@gmail.com>
The previous merge incorrectly reordered changelog entries alphabetically, causing them to appear as additions in the PR diff. This commit fixes the merge by: - Keeping all main branch entries in their exact original order - Appending the oauth2 allow_failed_matcher entry at the end This minimizes diff noise and makes the PR review clearer. Signed-off-by: Mo Chen <carterchen.m@gmail.com>
|
@zhaohuabing, sorry for the ping, but I could use another look at the PR. Changes have been made to the header sanitization per the suggestions, though there are a few other comments I think are worth discussing. Thank you! |
|
Friendly bump on this in case anyone has had a chance to look since the last ping. |
|
friendly ping @zhaohuabing |
|
Hi folks, Sorry for the delay — I’ll review this sometime this week. |
Ok in fact when investigating this issue I found that not only I've pushed a fix which I think is the most straightforward and easy solution on top of my head. In short, Happy to learn and discuss if y'all have any better and cleaner solution to this. Thank you for bringing this up! |
This should never happen in practice 🤔 ? |
I'm not that familiar with the entire infra but there's actually a test around calling
Either way, I made |
This should be a test error. The onSuccess() should never be called synchronously |
wbpcode
left a comment
There was a problem hiding this comment.
LGTM overall to me now. Thanks for the update.
|
/wait |
handleOAuthFailureAsync on the filter was just handleOAuthFailure + continueDecoding(), identical in structure to handleRefreshTokenFailure. Remove it and have OAuth2ClientImpl::handleOAuthFailure call parent_->handleOAuthFailure directly, then either call continueDecoding() (async path) or set state_ (sync path) based on is_request_dispatched. Also simplify the cluster-not-found path in dispatchRequest to call handleOAuthFailure(false, ...) instead of duplicating the sync path logic. Signed-off-by: Mo Chen <carterchen.m@gmail.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@wbpcode thanks for another review, please let me know if there's anything I need to do to move forward! Thank you! |
|
/lgtm api |
wbpcode
left a comment
There was a problem hiding this comment.
LGTM. Thanks for this great feature.
|
/retest |
Add 5 new tests covering previously uncovered code paths in the allow_failed_matcher feature: - HandleOAuthFailureOnAllowedPath: exercises OAuth2Filter::handleOAuthFailure on a path matching allow_failed_matcher (the code path triggered by real OAuth client callbacks, unreachable via mocked client tests) - HandleOAuthFailureOnNonAllowedPathWithExtraDetails: exercises the deny branch of handleOAuthFailure and the non-empty extra_details formatting - AllowFailedBlockedForCallbackPath: verifies shouldAllowFailed returns false for the OAuth callback URL even when allow_failed_matcher matches all paths - AllowFailedWithRefreshTokenSyncFailureStop: covers FailureStop branch after synchronous asyncRefreshAccessToken failure - OAuthCallbackGetAccessTokenSyncContinue: covers FailureContinue branch after synchronous asyncGetAccessToken failure on the callback path Signed-off-by: Mo Chen <mochen@google.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Mo Chen <carterchen.m@gmail.com>
Head branch was pushed to by a user without write access
|
CI failed due to test coverage low. Added a few new tests in filter_test |
|
/retest |
…auth failure (envoyproxy#43027) Commit Message: This PR adds an allow_failed_matcher field to the OAuth2 filter configuration, enabling requests to proceed to upstream as unauthenticated when OAuth validation fails (missing, invalid, or expired credentials) and the request matches the configured matchers. This enables graceful degradation patterns where services can handle both authenticated and unauthenticated requests, similar to the JWT filter's allow_missing_or_failed functionality. When triggered: - All OAuth cookies are stripped from the request - Context headers x-envoy-oauth-status: failed and x-envoy-oauth-failure-reason are added - Request continues to upstream as unauthenticated Matcher evaluation order: pass_through_matcher > allow_failed_matcher > deny_redirect_matcher > default behavior. Signed-off-by: Mo Chen <carterchen.m@gmail.com> Additional Description: Use case: An API that returns personalized results for authenticated requests but falls back to generic results for unauthenticated requests. Example configuration: ```yaml allow_failed_matcher: - name: ":path" string_match: prefix: "/api/public" ``` Risk Level: Medium Testing: Added unit tests covering multiple scenarios (no cookies, invalid refresh token, async/sync failures, OAuth callback path, matcher precedence). All existing OAuth2 tests pass. Verified backward compatibility. Docs Changes: Updated proto documentation for allow_failed_matcher field. Added release notes to changelogs/current.yaml. Release Notes: Added to changelogs/current.yaml Platform Specific Features: N/A [Optional Runtime guard:] N/A - Config-guarded feature (optional proto field) [Optional Fixes #Issue] Fixes envoyproxy#36523 [Optional [API Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):] - New optional field allow_failed_matcher (field 27) to OAuth2Config proto - Backward compatible - no behavior change when not configured - Uses existing HeaderMatcher type, follows same pattern as other matchers in OAuth2Config --------- Signed-off-by: Mo Chen <carterchen.m@gmail.com> Signed-off-by: code <wbphub@gmail.com> Co-authored-by: code <wbphub@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Nick Shipilov <nick.shipilov.n@gmail.com>
…auth failure (envoyproxy#43027) Commit Message: This PR adds an allow_failed_matcher field to the OAuth2 filter configuration, enabling requests to proceed to upstream as unauthenticated when OAuth validation fails (missing, invalid, or expired credentials) and the request matches the configured matchers. This enables graceful degradation patterns where services can handle both authenticated and unauthenticated requests, similar to the JWT filter's allow_missing_or_failed functionality. When triggered: - All OAuth cookies are stripped from the request - Context headers x-envoy-oauth-status: failed and x-envoy-oauth-failure-reason are added - Request continues to upstream as unauthenticated Matcher evaluation order: pass_through_matcher > allow_failed_matcher > deny_redirect_matcher > default behavior. Signed-off-by: Mo Chen <carterchen.m@gmail.com> Additional Description: Use case: An API that returns personalized results for authenticated requests but falls back to generic results for unauthenticated requests. Example configuration: ```yaml allow_failed_matcher: - name: ":path" string_match: prefix: "/api/public" ``` Risk Level: Medium Testing: Added unit tests covering multiple scenarios (no cookies, invalid refresh token, async/sync failures, OAuth callback path, matcher precedence). All existing OAuth2 tests pass. Verified backward compatibility. Docs Changes: Updated proto documentation for allow_failed_matcher field. Added release notes to changelogs/current.yaml. Release Notes: Added to changelogs/current.yaml Platform Specific Features: N/A [Optional Runtime guard:] N/A - Config-guarded feature (optional proto field) [Optional Fixes #Issue] Fixes envoyproxy#36523 [Optional [API Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):] - New optional field allow_failed_matcher (field 27) to OAuth2Config proto - Backward compatible - no behavior change when not configured - Uses existing HeaderMatcher type, follows same pattern as other matchers in OAuth2Config --------- Signed-off-by: Mo Chen <carterchen.m@gmail.com> Signed-off-by: code <wbphub@gmail.com> Co-authored-by: code <wbphub@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Commit Message:
This PR adds an allow_failed_matcher field to the OAuth2 filter configuration, enabling requests to proceed to upstream as unauthenticated when OAuth validation fails (missing, invalid, or expired credentials) and the request matches the configured matchers.
This enables graceful degradation patterns where services can handle both authenticated and unauthenticated requests, similar to the JWT filter's allow_missing_or_failed functionality.
When triggered:
Matcher evaluation order: pass_through_matcher > allow_failed_matcher > deny_redirect_matcher > default behavior.
Signed-off-by: Mo Chen carterchen.m@gmail.com
Additional Description:
Use case: An API that returns personalized results for authenticated requests but falls back to generic results for unauthenticated requests.
Example configuration:
Risk Level: Medium
Testing:
Added unit tests covering multiple scenarios (no cookies, invalid refresh token, async/sync failures, OAuth callback path, matcher precedence). All existing OAuth2 tests pass. Verified backward compatibility.
Docs Changes: Updated proto documentation for allow_failed_matcher field. Added release notes to changelogs/current.yaml.
Release Notes: Added to changelogs/current.yaml
Platform Specific Features: N/A
[Optional Runtime guard:] N/A - Config-guarded feature (optional proto field)
[Optional Fixes #Issue] Fixes #36523
[Optional API Considerations:]