Skip to content

feat(oauth): add client_secret_basic and RFC errors#99148

Merged
dcramer merged 15 commits into
masterfrom
oauth-client-secret-basic
Oct 24, 2025
Merged

feat(oauth): add client_secret_basic and RFC errors#99148
dcramer merged 15 commits into
masterfrom
oauth-client-secret-basic

Conversation

@dcramer

@dcramer dcramer commented Sep 9, 2025

Copy link
Copy Markdown
Member
  • Prefer Authorization: Basic over body; both present → invalid_request (400). Missing/invalid creds → invalid_client (401) with WWW-Authenticate.
  • Standardize token_type to Bearer; enforce redirect_uri binding; missing grant_type → invalid_request (400); accept case-insensitive schemes.
  • Bound Basic base64 to 4096 bytes to prevent DoS
  • Fix existing bug with expires_in for the authorize response

Refs:
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3

@dcramer dcramer requested a review from a team as a code owner September 9, 2025 19:47
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Sep 9, 2025
Comment thread src/sentry/web/frontend/oauth_token.py Fixed
Comment thread src/sentry/web/frontend/oauth_token.py Outdated
@codecov

codecov Bot commented Sep 9, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.83673% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/sentry/web/frontend/oauth_token.py 91.48% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #99148      +/-   ##
==========================================
+ Coverage   77.04%   80.97%   +3.93%     
==========================================
  Files        8733     8734       +1     
  Lines      388558   388613      +55     
  Branches    24641    24641              
==========================================
+ Hits       299357   314696   +15339     
+ Misses      88841    73557   -15284     
  Partials      360      360              

Comment thread src/sentry/web/frontend/oauth_token.py Outdated
@dcramer dcramer changed the title feat(oauth): client_secret_basic, RFC errors, cache headers feat(oauth): add client_secret_basic and RFC errors Sep 10, 2025
@dcramer

dcramer commented Sep 10, 2025

Copy link
Copy Markdown
Member Author

worth noting small chance this breaks an oauth consumer, but the chances are very low as most folks wouldnt be confirming the token_type value, and our oauth provider isnt extremely wide spread

@dcramer

dcramer commented Sep 10, 2025

Copy link
Copy Markdown
Member Author

Gonna refactor this one a bit more as I'm still not happy w/ the abstraction, but the general functionality should be good

- Prefer Basic auth over body; conflict → invalid_request (400)
- Map missing/bad creds to invalid_client (401) + WWW-Authenticate
- Add Cache-Control: no-store, Pragma: no-cache on success/errors
- Standardize token_type to “Bearer” (token + authorize fragment)
- Enforce redirect_uri binding in token exchange
- Treat missing grant_type as invalid_request (400)
- Remove redirect_uri from error logs
- Accept case-insensitive “Basic”/“Bearer” schemes (input)
Validate the length of the Base64 segment in Authorization: Basic before decoding.
If it exceeds 4096 bytes, reject with invalid_client (401). This prevents
unbounded memory allocation from maliciously large headers. Add a test to
cover the oversized header case.
…ive input test

- Add redirect_uri to test_no_open_id_token to satisfy strict binding
- Move lowercase 'bearer' input test under UserAuthTokenAuthentication where it applies
- Remove misplaced case-insensitive test from OrgAuthToken auth class
…yping

Switch header injection from **dict to explicit HTTP_AUTHORIZATION kw to align with client.post type hints and fix mypy arg-type errors.
- Remove manual Cache-Control/Pragma headers; add spec-referenced comment.
- Apply never_cache at dispatch so all methods inherit headers; drop per-method decorator.

Refs:
https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
- Remove manual Cache-Control/Pragma headers; add spec-referenced comment.
- Apply never_cache at dispatch so all methods inherit headers; drop per-method decorator.

Refs:
https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
- Parse Authorization: Basic from request.META only; return (id, secret) with error response.

- Simplify token view; keep RFC semantics (invalid_client on malformed header).

Refs:

https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
…isms

- Read Basic from request.META; set client_id/client_secret from header or body (not both).

- Return invalid_request on mixed header+body credentials; keep invalid_client for missing/invalid creds.

Refs:

https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13

Copilot AI 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.

Pull Request Overview

This PR enhances OAuth 2.0 compliance by implementing RFC 6749 requirements for client authentication and error handling. The changes standardize token responses to use "Bearer" (capitalized), add support for HTTP Basic authentication with proper conflict detection, enforce redirect URI binding for authorization code grants, and fix an existing bug where expires_in was calculated as a negative value.

Key Changes:

  • Add client_secret_basic authentication method with validation and conflict detection between header and body credentials
  • Standardize error codes: missing credentials → invalid_client (401), missing grant_type → invalid_request (400), with WWW-Authenticate headers
  • Fix expires_in calculation bug in authorize response (was timezone.now() - token.expires_at, now correctly token.expires_at - timezone.now())

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/sentry/web/frontend/oauth_token.py Core implementation of Basic auth extraction, error standardization, redirect_uri binding enforcement, and token_type capitalization
src/sentry/web/frontend/oauth_authorize.py Fix expires_in bug, standardize token_type to "Bearer", update logger name
tests/sentry/web/frontend/test_oauth_token.py Comprehensive test coverage for Basic auth scenarios, error code updates, redirect_uri binding
tests/sentry/web/frontend/test_oauth_authorize.py Update assertions for Bearer token_type and validate expires_in as positive integer
tests/sentry/api/test_authentication.py Add test for case-insensitive bearer scheme acceptance

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

status=401,
)
try:
decoded = base64.b64decode(b64.encode("ascii"), validate=True).decode("utf-8")

Copilot AI Oct 23, 2025

Copy link

Choose a reason for hiding this comment

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

The broad exception handler on line 240 catches all exceptions including encoding errors. Consider logging the specific exception type or message to aid debugging when Basic auth fails (e.g., 'Invalid Basic auth header: <exception_type>').

Copilot uses AI. Check for mistakes.
Comment thread tests/sentry/web/frontend/test_oauth_authorize.py Outdated

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good overall. Our invalid expires_in logic in the old code is mind blowing.

Given that we messed that up, I think we should add a test that ensure the value is correct now (didn't see that in the new tests).

Didn't scrutinize the test code much but they look fine overall.

@@ -439,9 +439,9 @@ def approve(
redirect_uri,
{
"access_token": token.token,
"expires_in": int((timezone.now() - token.expires_at).total_seconds()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤯

Comment on lines +116 to +117
if cred_error is not None:
return cred_error

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is this, golang? 🙄

Comment thread src/sentry/web/frontend/oauth_token.py
Comment thread src/sentry/web/frontend/oauth_token.py
Comment thread src/sentry/web/frontend/oauth_token.py Outdated
Comment thread src/sentry/web/frontend/oauth_token.py
Comment thread src/sentry/web/frontend/oauth_token.py
Comment thread src/sentry/web/frontend/oauth_token.py
Comment thread src/sentry/web/frontend/oauth_token.py Outdated
@dcramer

dcramer commented Oct 23, 2025

Copy link
Copy Markdown
Member Author

will merge tomorrow morning

Comment thread src/sentry/web/frontend/oauth_token.py Dismissed

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@dcramer dcramer merged commit 3c2bc4a into master Oct 24, 2025
69 checks passed
@dcramer dcramer deleted the oauth-client-secret-basic branch October 24, 2025 16:54
shashjar pushed a commit that referenced this pull request Nov 4, 2025
- Prefer Authorization: Basic over body; both present → invalid_request
(400). Missing/invalid creds → invalid_client (401) with
WWW-Authenticate.
- Standardize token_type to Bearer; enforce redirect_uri binding;
missing grant_type → invalid_request (400); accept case-insensitive
schemes.
- Bound Basic base64 to 4096 bytes to prevent DoS
- Fix existing bug with expires_in for the authorize response

Refs:
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 9, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants