feat(oauth): add client_secret_basic and RFC errors#99148
Conversation
Codecov Report❌ Patch coverage is
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 |
ed682a6 to
09f63d2
Compare
|
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 |
|
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
- Remove Pragma checks on token responses; Cache-Control no-store remains. Refs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Pragma https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
5ae9f6c to
6f5e5c5
Compare
6f5e5c5 to
dedbfcd
Compare
There was a problem hiding this comment.
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_incalculation bug in authorize response (wastimezone.now() - token.expires_at, now correctlytoken.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") |
There was a problem hiding this comment.
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>').
BYK
left a comment
There was a problem hiding this comment.
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()), | |||
| if cred_error is not None: | ||
| return cred_error |
|
will merge tomorrow morning |
246de02 to
cdf423f
Compare
- 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
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