Skip to content

Commit 10b51c6

Browse files
committed
fix(oauth): improve oauth token validation and error handling
- add application status check in grant validation - better error handling for lock acquisition failures - add tests for concurrent grant usage
1 parent 555601a commit 10b51c6

File tree

2 files changed

+34
-2
lines changed

2 files changed

+34
-2
lines changed

src/sentry/web/frontend/oauth_token.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from sentry.models.apitoken import ApiToken
1919
from sentry.sentry_apps.token_exchange.util import GrantTypes
2020
from sentry.utils import json, metrics
21+
from sentry.utils.locking import UnableToAcquireLock
2122
from sentry.web.frontend.base import control_silo_view
2223
from sentry.web.frontend.openidtoken import OpenIDToken
2324

@@ -128,7 +129,9 @@ def post(self, request: Request) -> HttpResponse:
128129
def get_access_tokens(self, request: Request, application: ApiApplication) -> dict:
129130
code = request.POST.get("code")
130131
try:
131-
grant = ApiGrant.objects.get(application=application, code=code)
132+
grant = ApiGrant.objects.get(
133+
application=application, application__status=ApiApplicationStatus.active, code=code
134+
)
132135
except ApiGrant.DoesNotExist:
133136
return {"error": "invalid_grant", "reason": "invalid grant"}
134137

@@ -141,7 +144,12 @@ def get_access_tokens(self, request: Request, application: ApiApplication) -> di
141144
elif grant.redirect_uri != redirect_uri:
142145
return {"error": "invalid_grant", "reason": "invalid redirect URI"}
143146

144-
token_data = {"token": ApiToken.from_grant(grant=grant)}
147+
try:
148+
token_data = {"token": ApiToken.from_grant(grant=grant)}
149+
except UnableToAcquireLock:
150+
# TODO(mdtro): we should return a 409 status code here
151+
return {"error": "invalid_grant", "reason": "invalid grant"}
152+
145153
if grant.has_scope("openid") and options.get("codecov.signing_secret"):
146154
open_id_token = OpenIDToken(
147155
request.POST.get("client_id"),
@@ -150,6 +158,7 @@ def get_access_tokens(self, request: Request, application: ApiApplication) -> di
150158
nonce=request.POST.get("nonce"),
151159
)
152160
token_data["id_token"] = open_id_token.get_signed_id_token(grant=grant)
161+
153162
return token_data
154163

155164
def get_refresh_token(self, request: Request, application: ApiApplication) -> dict:

tests/sentry/web/frontend/test_oauth_token.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.utils import timezone
44

5+
from sentry.locks import locks
56
from sentry.models.apiapplication import ApiApplication
67
from sentry.models.apigrant import ApiGrant
78
from sentry.models.apitoken import ApiToken
@@ -203,6 +204,28 @@ def test_one_time_use_grant(self):
203204
)
204205
assert resp.status_code == 400
205206

207+
def test_grant_lock(self):
208+
self.login_as(self.user)
209+
210+
# Simulate a concurrent request by using an existing grant
211+
# that has its grant lock taken out.
212+
lock = locks.get(ApiGrant.get_lock_key(self.grant.id), duration=10, name="api_grant")
213+
lock.acquire()
214+
215+
# Attempt to create a token with the same grant
216+
# This should fail because the lock is held by the previous request
217+
resp = self.client.post(
218+
self.path,
219+
{
220+
"grant_type": "authorization_code",
221+
"code": self.grant.code,
222+
"client_id": self.application.client_id,
223+
"client_secret": self.client_secret,
224+
},
225+
)
226+
assert resp.status_code == 400
227+
assert resp.json() == {"error": "invalid_grant"}
228+
206229
def test_invalid_redirect_uri(self):
207230
self.login_as(self.user)
208231

0 commit comments

Comments
 (0)