diff --git a/authlib/consts.py b/authlib/consts.py index ed67bccf..1fc26196 100644 --- a/authlib/consts.py +++ b/authlib/consts.py @@ -1,5 +1,5 @@ name = "Authlib" -version = "1.6.9" +version = "1.6.10" author = "Hsiaoming Yang " homepage = "https://authlib.org" default_user_agent = f"{name}/{version} (+{homepage})" diff --git a/authlib/oauth2/rfc6749/authorization_server.py b/authlib/oauth2/rfc6749/authorization_server.py index 928251dc..c484aa6c 100644 --- a/authlib/oauth2/rfc6749/authorization_server.py +++ b/authlib/oauth2/rfc6749/authorization_server.py @@ -241,10 +241,21 @@ def get_authorization_grant(self, request): if grant_cls.check_authorization_endpoint(request): return _create_grant(grant_cls, extensions, request, self) + # Per RFC 6749 ยง4.1.2.1, only redirect with the error if the client + # exists and the redirect_uri has been validated against it. + redirect_uri = None + if client_id := request.payload.client_id: + if client := self.query_client(client_id): + if requested_uri := request.payload.redirect_uri: + if client.check_redirect_uri(requested_uri): + redirect_uri = requested_uri + else: + redirect_uri = client.get_default_redirect_uri() + raise UnsupportedResponseTypeError( f"The response type '{request.payload.response_type}' is not supported by the server.", request.payload.response_type, - redirect_uri=request.payload.redirect_uri, + redirect_uri=redirect_uri, ) def get_consent_grant(self, request=None, end_user=None): diff --git a/docs/changelog.rst b/docs/changelog.rst index 1e557f58..2cdab361 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,37 @@ Changelog Here you can see the full list of changes between each Authlib release. +Version 1.6.10 +-------------- + +**Released on Apr 13, 2026** + +- Fix redirecting to unvalidated ``redirect_uri`` on ``UnsupportedResponseTypeError``. + +Version 1.6.9 +------------- + +**Released on Mar 2, 2026** + +- Not using header's ``jwk`` automatically. +- Add ``ES256K`` into default jwt algorithms. +- Remove deprecated algorithm from default registry. +- Generate random ``cek`` when ``cek`` length doesn't match. + +Version 1.6.8 +------------- + +**Released on Feb 17, 2026** + +- Add ``EdDSA`` to default ``jwt`` instance. + +Version 1.6.7 +------------- + +**Released on Feb 6, 2026** + +- Set supported algorithms for the default ``jwt`` instance. + Version 1.6.6 ------------- diff --git a/tests/flask/test_oauth2/test_authorization_code_grant.py b/tests/flask/test_oauth2/test_authorization_code_grant.py index f8d77fc9..6d437e2c 100644 --- a/tests/flask/test_oauth2/test_authorization_code_grant.py +++ b/tests/flask/test_oauth2/test_authorization_code_grant.py @@ -352,3 +352,47 @@ def test_token_generator(app, test_client, client, server): resp = json.loads(rv.data) assert "access_token" in resp assert "c-authorization_code.1." in resp["access_token"] + + +def test_missing_scope_empty_default(test_client, client, monkeypatch): + """When client.get_allowed_scope() returns empty string for missing scope, + the authorization should proceed without a scope. + """ + + def get_allowed_scope_empty(scope): + if scope is None: + return "" + return scope + + monkeypatch.setattr(client, "get_allowed_scope", get_allowed_scope_empty) + + rv = test_client.post(authorize_url, data={"user_id": "1"}) + assert "code=" in rv.location + + params = dict(url_decode(urlparse.urlparse(rv.location).query)) + code = params["code"] + headers = create_basic_header("client-id", "client-secret") + rv = test_client.post( + "/oauth/token", + data={ + "grant_type": "authorization_code", + "code": code, + }, + headers=headers, + ) + resp = json.loads(rv.data) + assert "access_token" in resp + assert resp.get("scope", "") == "" + + +def test_unsupported_response_type_does_not_redirect(test_client): + """Regression test for open redirect via unsupported response_type.""" + url = ( + "/oauth/authorize" + "?response_type=totally-unsupported" + "&redirect_uri=https%3A%2F%2Fevil.example%2Flanding" + "&state=s1" + ) + rv = test_client.get(url) + assert rv.status_code == 400 + assert rv.headers.get("Location") is None