Skip to content

oauth2: add allow_failed_matcher for unauthenticated pass-through on auth failure#43027

Merged
wbpcode merged 17 commits intoenvoyproxy:mainfrom
mchen391:oauth2-allow-failed-matcher
Apr 7, 2026
Merged

oauth2: add allow_failed_matcher for unauthenticated pass-through on auth failure#43027
wbpcode merged 17 commits intoenvoyproxy:mainfrom
mchen391:oauth2-allow-failed-matcher

Conversation

@mchen391
Copy link
Copy Markdown
Contributor

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:

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 #36523
[Optional API Considerations:]

  • 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

…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>
@repokitteh-read-only
Copy link
Copy Markdown

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.

🐱

Caused by: #43027 was opened by mchen391.

see: more, trace.

@repokitteh-read-only
Copy link
Copy Markdown

CC @envoyproxy/api-shepherds: Your approval is needed for changes made to (api/envoy/|docs/root/api-docs/).
envoyproxy/api-shepherds assignee is @markdroth
CC @envoyproxy/api-watchers: FYI only for changes made to (api/envoy/|docs/root/api-docs/).

🐱

Caused by: #43027 was opened by mchen391.

see: more, trace.

Comment thread source/extensions/filters/http/oauth2/oauth_client.cc Outdated
@tyxia
Copy link
Copy Markdown
Member

tyxia commented Jan 16, 2026

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!

@zhaohuabing
Copy link
Copy Markdown
Member

zhaohuabing commented Jan 19, 2026

Use case: An API that returns personalized results for authenticated requests but falls back to generic results for unauthenticated requests.

Can't this be achieved with Custom Response?

Here is an example using Envoy Gateway BackendTrafficPolicy.

@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Jan 19, 2026 via email

@zhaohuabing
Copy link
Copy Markdown
Member

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.

https://github.com/zhaohuabing/playground/blob/main/gateway/oidc-keycloak/response-override-redirect.yaml

@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Jan 27, 2026

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.

https://github.com/zhaohuabing/playground/blob/main/gateway/oidc-keycloak/response-override-redirect.yaml

@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 allow_missing_or_failed option is also designed with this requirement in mind. https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto#envoy-v3-api-field-extensions-filters-http-jwt-authn-v3-jwtrequirement-allow-missing-or-failed

@zhaohuabing
Copy link
Copy Markdown
Member

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.

@markdroth
Copy link
Copy Markdown
Contributor

/lgtm api

Comment thread api/envoy/extensions/filters/http/oauth2/v3/oauth.proto
Comment thread source/extensions/filters/http/oauth2/filter.cc Outdated
Comment thread source/extensions/filters/http/oauth2/filter.cc Outdated
Comment thread source/extensions/filters/http/oauth2/filter.cc
Comment thread source/extensions/filters/http/oauth2/filter.cc Outdated
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>
@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Feb 4, 2026

@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!

@theon73
Copy link
Copy Markdown

theon73 commented Feb 11, 2026

Friendly bump on this in case anyone has had a chance to look since the last ping.
I’m helping cover while @mchen391 is away. Feedback appreciated, thanks.

@botengyao
Copy link
Copy Markdown
Member

friendly ping @zhaohuabing

@zhaohuabing
Copy link
Copy Markdown
Member

Hi folks, Sorry for the delay — I’ll review this sometime this week.

Comment thread source/extensions/filters/http/oauth2/oauth_client.cc
@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Mar 21, 2026

Sorry. I think we need to re-evaluate the whole implementation. Note, the onFailure may be called directly in sometimes. Then, call the continueDecoding() in the failure callback will result in unexpected behavior. We must to take it into account.

cc @zhaohuabing

Ok in fact when investigating this issue I found that not only onFailure but also onSuccess can be called directly inside send(), so calling continueDecoding in onSuccess is not safe either.

I've pushed a fix which I think is the most straightforward and easy solution on top of my head. In short, onSuccess and onFailure will now check if it is called before the async request is dispatched (should not call continueDecoding), or after it is dispatched (should call continueDecoding). Please refer to the two comments I left on the code for more details.

Happy to learn and discuss if y'all have any better and cleaner solution to this. Thank you for bringing this up!

cc @zhaohuabing @wbpcode

@mchen391 mchen391 requested a review from wbpcode March 21, 2026 09:04
@wbpcode
Copy link
Copy Markdown
Member

wbpcode commented Mar 25, 2026

Ok in fact when investigating this issue I found that not only onFailure but also onSuccess can be called directly inside send(), so calling continueDecoding in onSuccess is not safe either.

This should never happen in practice 🤔 ?

@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Mar 25, 2026

Ok in fact when investigating this issue I found that not only onFailure but also onSuccess can be called directly inside send(), so calling continueDecoding in onSuccess is not safe either.

This should never happen in practice 🤔 ?

I'm not that familiar with the entire infra but there's actually a test around calling onSuccess() for unhealthy upstream (here). Based on my investigation (with claude):

  • send() will call onSuccess() with response status being 5xx for unhealthy upstream
  • send() will call onFailure() if network is completely unreachable

Either way, I made onSuccess() safe to call in both sync/async context as well as onFailure() just in case it ever hits that pattern.

@wbpcode
Copy link
Copy Markdown
Member

wbpcode commented Mar 25, 2026

I'm not that familiar with the entire infra but there's actually a test around calling onSuccess() for unhealthy upstream (here). Based on my investigation (with claude):

This should be a test error. The onSuccess() should never be called synchronously

Copy link
Copy Markdown
Member

@wbpcode wbpcode left a comment

Choose a reason for hiding this comment

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

LGTM overall to me now. Thanks for the update.

Comment thread source/extensions/filters/http/oauth2/oauth_client.cc
@wbpcode
Copy link
Copy Markdown
Member

wbpcode commented Mar 25, 2026

/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>
@mchen391 mchen391 temporarily deployed to external-contributors March 25, 2026 18:12 — with GitHub Actions Inactive
@mchen391 mchen391 requested a review from wbpcode March 26, 2026 17:46
@mchen391
Copy link
Copy Markdown
Contributor Author

@wbpcode thanks for another review, please let me know if there's anything I need to do to move forward! Thank you!

@markdroth
Copy link
Copy Markdown
Contributor

/lgtm api

@repokitteh-read-only repokitteh-read-only Bot removed the api label Apr 3, 2026
wbpcode
wbpcode previously approved these changes Apr 4, 2026
Copy link
Copy Markdown
Member

@wbpcode wbpcode left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for this great feature.

@wbpcode wbpcode enabled auto-merge (squash) April 4, 2026 14:20
@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Apr 4, 2026

/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>
auto-merge was automatically disabled April 4, 2026 16:38

Head branch was pushed to by a user without write access

@mchen391 mchen391 had a problem deploying to external-contributors April 4, 2026 16:38 — with GitHub Actions Error
@mchen391 mchen391 temporarily deployed to external-contributors April 4, 2026 16:38 — with GitHub Actions Inactive
@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Apr 4, 2026

CI failed due to test coverage low. Added a few new tests in filter_test

@mchen391
Copy link
Copy Markdown
Contributor Author

mchen391 commented Apr 6, 2026

/retest

@wbpcode wbpcode merged commit fc849d5 into envoyproxy:main Apr 7, 2026
31 of 33 checks passed
nshipilov pushed a commit to nshipilov/envoy that referenced this pull request Apr 13, 2026
…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>
krinkinmu pushed a commit to grnmeira/envoy that referenced this pull request Apr 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

oauth2 filter: option to allow errors and pass through

7 participants