Repository: Colin-b/requests_auth

Issue:
Handle OAuth2 Browser query rejection properly
Same as #15 but for the browser queries

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe23f32..25a73e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 - OAuth2ClientCredentials username parameter is now client_id
 - OAuth2ClientCredentials password parameter is now client_secret
-- requests_auth.InvalidGrantRequest is now raised instead of requests.HTTPError in case a grant request was invalid. This exception contains more information.
+- requests_auth.InvalidGrantRequest is now raised instead of requests.HTTPError in case a grant request was invalid.
+- requests_auth.InvalidGrantRequest is now raised instead of requests_auth.GrantNotProvided in case a browser grant request was invalid.
+- There is no info logging anymore. If you want to have those information (browser opening on a specific URL, requests received by the OAUth2 server), you will have to put requests_auth logger to DEBUG.
 
 ### Removed
 - Support for Python < 3.6
diff --git a/requests_auth/authentication.py b/requests_auth/authentication.py
index 6925592..27aac84 100644
--- a/requests_auth/authentication.py
+++ b/requests_auth/authentication.py
@@ -3,6 +3,7 @@ import os
 import uuid
 from hashlib import sha256, sha512
 from urllib.parse import parse_qs, urlsplit, urlunsplit, urlencode
+from typing import Optional
 
 import requests
 import requests.auth
@@ -32,7 +33,7 @@ def _add_parameters(initial_url: str, extra_parameters: dict) -> str:
     return urlunsplit((scheme, netloc, path, new_query_string, fragment))
 
 
-def _pop_parameter(url: str, query_parameter_name: str) -> tuple:
+def _pop_parameter(url: str, query_parameter_name: str) -> (str, Optional[str]):
     """
     Remove and return parameter of an URL.
 
@@ -51,7 +52,7 @@ def _pop_parameter(url: str, query_parameter_name: str) -> tuple:
     )
 
 
-def _get_query_parameter(url: str, param_name: str) -> str:
+def _get_query_parameter(url: str, param_name: str) -> Optional[str]:
     scheme, netloc, path, query_string, fragment = urlsplit(url)
     query_params = parse_qs(query_string)
     all_values = query_params.get(param_name)
@@ -60,7 +61,7 @@ def _get_query_parameter(url: str, param_name: str) -> str:
 
 def request_new_grant_with_post(
     url: str, data, grant_name: str, timeout: float, auth=None
-) -> tuple:
+) -> (str, int):
     response = requests.post(url, data=data, timeout=timeout, auth=auth)
     if not response:
         # As described in https://tools.ietf.org/html/rfc6749#section-5.2
diff --git a/requests_auth/errors.py b/requests_auth/errors.py
index 330a0a2..c2e30ce 100644
--- a/requests_auth/errors.py
+++ b/requests_auth/errors.py
@@ -1,4 +1,5 @@
 from json import JSONDecodeError
+from typing import Union
 
 from requests import Response
 
@@ -40,7 +41,8 @@ class InvalidGrantRequest(Exception):
     If the request failed client authentication or is invalid, the authorization server returns an error response as described in https://tools.ietf.org/html/rfc6749#section-5.2
     """
 
-    errors = {
+    # https://tools.ietf.org/html/rfc6749#section-5.2
+    request_errors = {
         "invalid_request": "The request is missing a required parameter, includes an unsupported parameter value (other than grant type), repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.",
         "invalid_client": 'Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).  The authorization server MAY return an HTTP 401 (Unauthorized) status code to indicate which HTTP authentication schemes are supported.  If the client attempted to authenticate via the "Authorization" request header field, the authorization server MUST respond with an HTTP 401 (Unauthorized) status code and include the "WWW-Authenticate" response header field matching the authentication scheme used by the client.',
         "invalid_grant": "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.",
@@ -49,32 +51,66 @@ class InvalidGrantRequest(Exception):
         "invalid_scope": "The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.",
     }
 
-    def __init__(self, response: Response):
+    # https://tools.ietf.org/html/rfc6749#section-4.2.2.1
+    # https://tools.ietf.org/html/rfc6749#section-4.1.2.1
+    browser_errors = {
+        "invalid_request": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.",
+        "unauthorized_client": "The client is not authorized to request an authorization code or an access token using this method.",
+        "access_denied": "The resource owner or authorization server denied the request.",
+        "unsupported_response_type": "The authorization server does not support obtaining an authorization code or an access token using this method.",
+        "invalid_scope": "The requested scope is invalid, unknown, or malformed.",
+        "server_error": "The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)",
+        "temporarily_unavailable": "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.  (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)",
+    }
+
+    def __init__(self, response: Union[Response, dict]):
         Exception.__init__(self, InvalidGrantRequest.to_message(response))
 
     @staticmethod
-    def to_message(response: Response) -> str:
+    def to_message(response: Union[Response, dict]) -> str:
         """
-        Handle response as described in https://tools.ietf.org/html/rfc6749#section-5.2
+        Handle response as described in:
+            * https://tools.ietf.org/html/rfc6749#section-5.2
+            * https://tools.ietf.org/html/rfc6749#section-4.1.2.1
+            * https://tools.ietf.org/html/rfc6749#section-4.2.2.1
         """
+        if isinstance(response, dict):
+            return InvalidGrantRequest.to_oauth2_message(
+                response, InvalidGrantRequest.browser_errors
+            )
+
         try:
-            content = response.json()
-            if "error" in content:
-                error = content.pop("error", None)
-                error_description = content.pop(
-                    "error_description", None
-                ) or InvalidGrantRequest.errors.get(error)
-                message = f"{error}: {error_description}"
-                if "error_uri" in content:
-                    message += (
-                        f"\nMore information can be found on {content.pop('error_uri')}"
-                    )
-                if content:
-                    message += f"\nAdditional information: {content}"
-            else:
-                message = response.text
+            return InvalidGrantRequest.to_oauth2_message(
+                response.json(), InvalidGrantRequest.request_errors
+            )
         except JSONDecodeError:
-            message = response.text
+            return response.text
+
+    @staticmethod
+    def to_oauth2_message(content: dict, errors: dict) -> str:
+        """
+        Handle content as described in:
+            * https://tools.ietf.org/html/rfc6749#section-5.2
+            * https://tools.ietf.org/html/rfc6749#section-4.1.2.1
+            * https://tools.ietf.org/html/rfc6749#section-4.2.2.1
+        """
+
+        def _pop(key: str) -> str:
+            value = content.pop(key, None)
+            if value and isinstance(value, list):
+                value = value[0]
+            return value
+
+        if "error" in content:
+            error = _pop("error")
+            error_description = _pop("error_description") or errors.get(error)
+            message = f"{error}: {error_description}"
+            if "error_uri" in content:
+                message += f"\nMore information can be found on {_pop('error_uri')}"
+            if content:
+                message += f"\nAdditional information: {content}"
+        else:
+            message = f"{content}"
         return message
 
 
diff --git a/requests_auth/oauth2_authentication_responses_server.py b/requests_auth/oauth2_authentication_responses_server.py
index 7959cc6..86db57a 100644
--- a/requests_auth/oauth2_authentication_responses_server.py
+++ b/requests_auth/oauth2_authentication_responses_server.py
@@ -51,6 +51,8 @@ class OAuth2ResponseHandler(BaseHTTPRequestHandler):
     def _parse_grant(self, arguments: dict):
         grants = arguments.get(self.server.grant_details.name)
         if not grants or len(grants) > 1:
+            if "error" in arguments:
+                raise InvalidGrantRequest(arguments)
             raise GrantNotProvided(self.server.grant_details.name, arguments)
         logger.debug(f"Received grants: {grants}")
         grant = grants[0]
@@ -125,7 +127,7 @@ class OAuth2ResponseHandler(BaseHTTPRequestHandler):
 
     def log_message(self, format: str, *args):
         """Make sure that messages are logged even with pythonw (seems like a bug in BaseHTTPRequestHandler)."""
-        logger.info(format, *args)
+        logger.debug(format, *args)
 
 
 class GrantDetails:
@@ -148,15 +150,6 @@ class GrantDetails:
 
 class FixedHttpServer(HTTPServer):
     def __init__(self, grant_details: GrantDetails):
-        """
-
-        :param grant_details: Must be a class providing the following attributes:
-            * name
-            * reception_success_display_time
-            * reception_failure_display_time
-            * redirect_uri_port
-            * reception_timeout
-        """
         HTTPServer.__init__(
             self, ("", grant_details.redirect_uri_port), OAuth2ResponseHandler
         )
@@ -172,9 +165,8 @@ class FixedHttpServer(HTTPServer):
         HTTPServer.finish_request(self, request, client_address)
 
     def ensure_no_error_occurred(self):
-        if (
-            self.request_error
-        ):  # Raise error encountered while processing a request if any
+        if self.request_error:
+            # Raise error encountered while processing a request if any
             raise self.request_error
         return not self.grant
 
@@ -182,17 +174,14 @@ class FixedHttpServer(HTTPServer):
         raise TimeoutOccurred(self.timeout)
 
 
-def request_new_grant(grant_details: GrantDetails):
+def request_new_grant(grant_details: GrantDetails) -> (str, str):
     """
     Ask for a new OAuth2 grant.
-    :param grant_details: Must be a class providing the following attributes:
-        * url
-        * name
-        * reception_timeout
-        * reception_success_display_time
-        * reception_failure_display_time
-        * redirect_uri_port
-    :return:A tuple (state, grant) or an Exception if not retrieved within timeout.
+    :return: A tuple (state, grant)
+    :raises InvalidGrantRequest: If the request was invalid.
+    :raises TimeoutOccurred: If not retrieved within timeout.
+    :raises GrantNotProvided: If grant is not provided in response (but no error occurred).
+    :raises StateNotProvided: If state if not provided in addition to the grant.
     """
     logger.debug(f"Requesting new {grant_details.name}...")
 
@@ -211,7 +200,7 @@ def _open_url(url: str):
             if hasattr(webbrowser, "iexplore")
             else webbrowser.get()
         )
-        logger.info(f"Opening browser on {url}")
+        logger.debug(f"Opening browser on {url}")
         if not browser.open(url, new=1):
             logger.warning("Unable to open URL, try with a GET request.")
             requests.get(url)
@@ -220,7 +209,14 @@ def _open_url(url: str):
         requests.get(url)
 
 
-def _wait_for_grant(server: FixedHttpServer):
+def _wait_for_grant(server: FixedHttpServer) -> (str, str):
+    """
+    :return: A tuple (state, grant)
+    :raises InvalidGrantRequest: If the request was invalid.
+    :raises TimeoutOccurred: If not retrieved within timeout.
+    :raises GrantNotProvided: If grant is not provided in response (but no error occurred).
+    :raises StateNotProvided: If state if not provided in addition to the grant.
+    """
     logger.debug("Waiting for user authentication...")
     while not server.grant:
         server.handle_request()
diff --git a/requests_auth/oauth2_tokens.py b/requests_auth/oauth2_tokens.py
index 2675204..fc14c16 100644
--- a/requests_auth/oauth2_tokens.py
+++ b/requests_auth/oauth2_tokens.py
@@ -9,7 +9,7 @@ from requests_auth.errors import *
 logger = logging.getLogger(__name__)
 
 
-def decode_base64(base64_encoded_string: str):
+def decode_base64(base64_encoded_string: str) -> str:
     """
     Decode base64, padding being optional.
 
@@ -22,7 +22,7 @@ def decode_base64(base64_encoded_string: str):
     return base64.b64decode(base64_encoded_string).decode("unicode_escape")
 
 
-def is_expired(expiry: float):
+def is_expired(expiry: float) -> bool:
     return datetime.datetime.utcfromtimestamp(expiry) < datetime.datetime.utcnow()
 
 
@@ -82,7 +82,7 @@ class TokenMemoryCache:
                 f'Inserting token expiring on {datetime.datetime.utcfromtimestamp(expiry)} (UTC) with "{key}" key: {token}'
             )
 
-    def get_token(self, key: str, on_missing_token=None, *on_missing_token_args):
+    def get_token(self, key: str, on_missing_token=None, *on_missing_token_args) -> str:
         """
         Return the bearer token.
         :param key: key identifier of the token


Test patch:
diff --git a/tests/oauth2_helper.py b/tests/oauth2_helper.py
index 3b321c7..b2a59cd 100644
--- a/tests/oauth2_helper.py
+++ b/tests/oauth2_helper.py
@@ -87,7 +87,7 @@ class BrowserMock:
 
     def open(self, url: str, new: int):
         assert new == 1
-        assert url in self.tabs
+        assert url in self.tabs, f"Browser call on {url} was not mocked."
         # Simulate a browser by sending the response in another thread
         self.tabs[url].start()
         return True
diff --git a/tests/test_oauth2_authorization_code.py b/tests/test_oauth2_authorization_code.py
index 1121775..82fc94f 100644
--- a/tests/test_oauth2_authorization_code.py
+++ b/tests/test_oauth2_authorization_code.py
@@ -204,7 +204,7 @@ def test_with_invalid_grant_request_invalid_request_error_and_error_description_
         requests.get("http://authorized_only", auth=auth)
     assert (
         str(exception_info.value)
-        == f"invalid_request: desc of the error\nMore information can be found on http://test_url\nAdditional information: {{'other': 'other info'}}"
+        == "invalid_request: desc of the error\nMore information can be found on http://test_url\nAdditional information: {'other': 'other info'}"
     )
     tab.assert_success(
         "You are now authenticated on 163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de. You may close this tab."
@@ -229,7 +229,7 @@ def test_with_invalid_grant_request_without_error(
     )
     with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
         requests.get("http://authorized_only", auth=auth)
-    assert str(exception_info.value) == f'{{"other": "other info"}}'
+    assert str(exception_info.value) == "{'other': 'other info'}"
     tab.assert_success(
         "You are now authenticated on 163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de. You may close this tab."
     )
@@ -383,6 +383,213 @@ def test_with_invalid_grant_request_invalid_scope_error(
     )
 
 
+def test_with_invalid_token_request_invalid_request_error(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert str(exception_info.value) == "invalid_request: desc"
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc"
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description_and_uri(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc&error_uri=http://test_url",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_request: desc\nMore information can be found on http://test_url"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc\nMore information can be found on http://test_url"
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description_and_uri_and_other_fields(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc&error_uri=http://test_url&other=test",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_request: desc\nMore information can be found on http://test_url\nAdditional information: {'other': ['test']}"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc\nMore information can be found on http://test_url\nAdditional information: {'other': ['test']}"
+    )
+
+
+def test_with_invalid_token_request_unauthorized_client_error(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=unauthorized_client",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "unauthorized_client: The client is not authorized to request an authorization code or an access token using this method."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: unauthorized_client: The client is not authorized to request an authorization code or an access token using this method."
+    )
+
+
+def test_with_invalid_token_request_access_denied_error(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=access_denied",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "access_denied: The resource owner or authorization server denied the request."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: access_denied: The resource owner or authorization server denied the request."
+    )
+
+
+def test_with_invalid_token_request_unsupported_response_type_error(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=unsupported_response_type",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "unsupported_response_type: The authorization server does not support obtaining an authorization code or an access token using this method."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: unsupported_response_type: The authorization server does not support obtaining an authorization code or an access token using this method."
+    )
+
+
+def test_with_invalid_token_request_invalid_scope_error(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_scope",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_scope: The requested scope is invalid, unknown, or malformed."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_scope: The requested scope is invalid, unknown, or malformed."
+    )
+
+
+def test_with_invalid_token_request_server_error_error(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=server_error",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "server_error: The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: server_error: The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+
+
+def test_with_invalid_token_request_temporarily_unavailable_error(
+    token_cache, browser_mock: BrowserMock
+):
+    auth = requests_auth.OAuth2AuthorizationCode(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=temporarily_unavailable",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.  (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.  (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+
+
 def test_nonce_is_sent_if_provided_in_authorization_url(
     token_cache, responses: RequestsMock, browser_mock: BrowserMock
 ):
diff --git a/tests/test_oauth2_authorization_code_pkce.py b/tests/test_oauth2_authorization_code_pkce.py
index 409d13f..0af7224 100644
--- a/tests/test_oauth2_authorization_code_pkce.py
+++ b/tests/test_oauth2_authorization_code_pkce.py
@@ -239,7 +239,7 @@ def test_with_invalid_grant_request_without_error(
     )
     with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
         requests.get("http://authorized_only", auth=auth)
-    assert str(exception_info.value) == f'{{"other": "other info"}}'
+    assert str(exception_info.value) == "{'other': 'other info'}"
     tab.assert_success(
         "You are now authenticated on 163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de. You may close this tab."
     )
@@ -398,6 +398,223 @@ def test_with_invalid_grant_request_invalid_scope_error(
     )
 
 
+def test_with_invalid_token_request_invalid_request_error(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=invalid_request",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert str(exception_info.value) == "invalid_request: desc"
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc"
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description_and_uri(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc&error_uri=http://test_url",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_request: desc\nMore information can be found on http://test_url"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc\nMore information can be found on http://test_url"
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description_and_uri_and_other_fields(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc&error_uri=http://test_url&other=test",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_request: desc\nMore information can be found on http://test_url\nAdditional information: {'other': ['test']}"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc\nMore information can be found on http://test_url\nAdditional information: {'other': ['test']}"
+    )
+
+
+def test_with_invalid_token_request_unauthorized_client_error(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=unauthorized_client",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "unauthorized_client: The client is not authorized to request an authorization code or an access token using this method."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: unauthorized_client: The client is not authorized to request an authorization code or an access token using this method."
+    )
+
+
+def test_with_invalid_token_request_access_denied_error(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=access_denied",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "access_denied: The resource owner or authorization server denied the request."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: access_denied: The resource owner or authorization server denied the request."
+    )
+
+
+def test_with_invalid_token_request_unsupported_response_type_error(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=unsupported_response_type",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "unsupported_response_type: The authorization server does not support obtaining an authorization code or an access token using this method."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: unsupported_response_type: The authorization server does not support obtaining an authorization code or an access token using this method."
+    )
+
+
+def test_with_invalid_token_request_invalid_scope_error(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=invalid_scope",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "invalid_scope: The requested scope is invalid, unknown, or malformed."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_scope: The requested scope is invalid, unknown, or malformed."
+    )
+
+
+def test_with_invalid_token_request_server_error_error(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=server_error",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "server_error: The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: server_error: The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+
+
+def test_with_invalid_token_request_temporarily_unavailable_error(
+    token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
+):
+    monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
+    auth = requests_auth.OAuth2AuthorizationCodePKCE(
+        "http://provide_code", "http://provide_access_token"
+    )
+    tab = browser_mock.add_response(
+        opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
+        reply_url="http://localhost:5000#error=temporarily_unavailable",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get("http://authorized_only", auth=auth)
+    assert (
+        str(exception_info.value)
+        == "temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.  (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.  (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+
+
 def test_response_type_can_be_provided_in_url(
     token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
 ):
diff --git a/tests/test_oauth2_client_credential.py b/tests/test_oauth2_client_credential.py
index 44433ae..4d71fac 100644
--- a/tests/test_oauth2_client_credential.py
+++ b/tests/test_oauth2_client_credential.py
@@ -143,7 +143,7 @@ def test_with_invalid_grant_request_without_error(token_cache, responses: Reques
     )
     with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
         requests.get("http://authorized_only", auth=auth)
-    assert str(exception_info.value) == f'{{"other": "other info"}}'
+    assert str(exception_info.value) == "{'other': 'other info'}"
 
 
 def test_with_invalid_grant_request_invalid_client_error(
diff --git a/tests/test_oauth2_implicit.py b/tests/test_oauth2_implicit.py
index 0159424..b82e23b 100644
--- a/tests/test_oauth2_implicit.py
+++ b/tests/test_oauth2_implicit.py
@@ -96,10 +96,10 @@ def test_oauth2_implicit_flow_token_is_reused_if_only_nonce_differs(
 def test_oauth2_implicit_flow_token_can_be_requested_on_a_custom_server_port(
     token_cache, responses: RequestsMock, browser_mock: BrowserMock
 ):
+    # TODO Should use a method to retrieve a free port instead
+    available_port = 5002
     auth = requests_auth.OAuth2Implicit(
-        "http://provide_token",
-        # TODO Should use a method to retrieve a free port instead
-        redirect_uri_port=5002,
+        "http://provide_token", redirect_uri_port=available_port
     )
     expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
     token = create_token(expiry_in_1_hour)
@@ -449,6 +449,213 @@ def test_oauth2_implicit_flow_get_failure_if_state_is_not_provided(
     )
 
 
+def test_with_invalid_token_request_invalid_request_error(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert str(exception_info.value) == "invalid_request: desc"
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc"
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description_and_uri(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc&error_uri=http://test_url",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "invalid_request: desc\nMore information can be found on http://test_url"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc\nMore information can be found on http://test_url"
+    )
+
+
+def test_with_invalid_token_request_invalid_request_error_and_error_description_and_uri_and_other_fields(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_request&error_description=desc&error_uri=http://test_url&other=test",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "invalid_request: desc\nMore information can be found on http://test_url\nAdditional information: {'other': ['test']}"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_request: desc\nMore information can be found on http://test_url\nAdditional information: {'other': ['test']}"
+    )
+
+
+def test_with_invalid_token_request_unauthorized_client_error(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=unauthorized_client",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "unauthorized_client: The client is not authorized to request an authorization code or an access token using this method."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: unauthorized_client: The client is not authorized to request an authorization code or an access token using this method."
+    )
+
+
+def test_with_invalid_token_request_access_denied_error(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=access_denied",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "access_denied: The resource owner or authorization server denied the request."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: access_denied: The resource owner or authorization server denied the request."
+    )
+
+
+def test_with_invalid_token_request_unsupported_response_type_error(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=unsupported_response_type",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "unsupported_response_type: The authorization server does not support obtaining an authorization code or an access token using this method."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: unsupported_response_type: The authorization server does not support obtaining an authorization code or an access token using this method."
+    )
+
+
+def test_with_invalid_token_request_invalid_scope_error(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=invalid_scope",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "invalid_scope: The requested scope is invalid, unknown, or malformed."
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: invalid_scope: The requested scope is invalid, unknown, or malformed."
+    )
+
+
+def test_with_invalid_token_request_server_error_error(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=server_error",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "server_error: The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: server_error: The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+
+
+def test_with_invalid_token_request_temporarily_unavailable_error(
+    token_cache, browser_mock: BrowserMock
+):
+    tab = browser_mock.add_response(
+        opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F",
+        reply_url="http://localhost:5000#error=temporarily_unavailable",
+    )
+    with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
+        requests.get(
+            "http://authorized_only",
+            auth=requests_auth.OAuth2Implicit("http://provide_token"),
+        )
+    assert (
+        str(exception_info.value)
+        == "temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.  (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+    tab.assert_failure(
+        "Unable to properly perform authentication: temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.  (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)"
+    )
+
+
 def test_oauth2_implicit_flow_failure_if_token_is_not_received_within_the_timeout_interval(
     token_cache, browser_mock: BrowserMock
 ):
diff --git a/tests/test_oauth2_resource_owner_password.py b/tests/test_oauth2_resource_owner_password.py
index ea0c713..86d6b0f 100644
--- a/tests/test_oauth2_resource_owner_password.py
+++ b/tests/test_oauth2_resource_owner_password.py
@@ -205,7 +205,7 @@ def test_with_invalid_grant_request_without_error(token_cache, responses: Reques
     )
     with pytest.raises(requests_auth.InvalidGrantRequest) as exception_info:
         requests.get("http://authorized_only", auth=auth)
-    assert str(exception_info.value) == f'{{"other": "other info"}}'
+    assert str(exception_info.value) == "{'other': 'other info'}"
 
 
 def test_with_invalid_grant_request_invalid_client_error(