diff --git a/src/sentry/web/frontend/oauth_authorize.py b/src/sentry/web/frontend/oauth_authorize.py index 4e54e9489f1ef7..6e9d239753a719 100644 --- a/src/sentry/web/frontend/oauth_authorize.py +++ b/src/sentry/web/frontend/oauth_authorize.py @@ -110,7 +110,26 @@ def get(self, request: HttpRequest, **kwargs) -> HttpResponseBase: err_response="client_id", ) + # Spec references: + # - RFC 6749 §3.1.2.3 (Redirection Endpoint): redirect_uri must match a pre-registered value; if + # multiple redirect URIs are registered, the client MUST include redirect_uri in the request. + # https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.3 + # - RFC 8252 §8.4 (Native Apps): loopback redirect considerations (ephemeral ports). + # https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 if not redirect_uri: + # If multiple redirect URIs are registered, require the client to provide an + # exact redirect_uri. + # See RFC 6749 §3.1.2.3: https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.3 + uris = application.get_redirect_uris() + if len(uris) != 1: + return self.error( + request=request, + client_id=client_id, + response_type=response_type, + redirect_uri=redirect_uri, + name="invalid_request", + err_response="redirect_uri", + ) redirect_uri = application.get_default_redirect_uri() elif not application.is_valid_redirect_uri(redirect_uri): return self.error( diff --git a/tests/sentry/web/frontend/test_oauth_authorize.py b/tests/sentry/web/frontend/test_oauth_authorize.py index dd27857f4dd8b6..8e7574a15225ee 100644 --- a/tests/sentry/web/frontend/test_oauth_authorize.py +++ b/tests/sentry/web/frontend/test_oauth_authorize.py @@ -144,6 +144,21 @@ def test_rich_params(self) -> None: assert not ApiToken.objects.filter(user=self.user).exists() + def test_requires_redirect_uri_when_multiple_registered(self) -> None: + self.login_as(self.user) + # Update application to have multiple registered redirect URIs + self.application.redirect_uris = "https://example.com\nhttps://example.org/callback" + self.application.save() + + resp = self.client.get( + f"{self.path}?response_type=code&client_id={self.application.client_id}" + ) + + # Must require redirect_uri when multiple are registered (RFC 6749 §3.1.2.3) + assert resp.status_code == 400 + self.assertTemplateUsed("sentry/oauth-error.html") + assert resp.context["error"] == "Missing or invalid redirect_uri parameter." + def test_approve_flow_bypass_prompt(self) -> None: self.login_as(self.user)