Repository: MushroomMaula/fastapi_login

Issue:
Exception handling
When I use the example code from https://fastapi-login.readthedocs.io/advanced_usage/ and there is no, or an invalid token present in the request I get an error:

```
INFO:     127.0.0.1:5898 - "GET /proteced HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 41, in call_next
    message = await recv_stream.receive()
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\anyio\streams\memory.py", line 81, in receive
    return self.receive_nowait()
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\anyio\streams\memory.py", line 74, in receive_nowait
    raise EndOfStream
anyio.EndOfStream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 373, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\applications.py", line 261, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 63, in __call__
    response = await self.dispatch_func(request, call_next)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi_login\fastapi_login.py", line 439, in user_middleware
    return await call_next(request)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 44, in call_next
    raise app_exc
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 34, in coro
    await self.app(scope, request.receive, send_stream.send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 21, in __call__
    raise e
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\routing.py", line 656, in __call__
    await route.handle(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\routing.py", line 259, in handle
    await self.app(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\routing.py", line 61, in app
    response = await func(request)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\routing.py", line 217, in app
    solved_result = await solve_dependencies(
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\dependencies\utils.py", line 527, in solve_dependencies
    solved = await call(**sub_values)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi_login\fastapi_login.py", line 405, in __call__
    token = await self._get_token(request)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi_login\fastapi_login.py", line 349, in _get_token
    except type(self.not_authenticated_exception):
TypeError: catching classes that do not inherit from BaseException is not allowed
```

I use:
```
class NotAuthenticatedException(Exception):
    pass

secret = SECRET
manager = LoginManager(SECRET, '/login', use_header=False, use_cookie=True, custom_exception=NotAuthenticatedException)
@app.exception_handler(NotAuthenticatedException)
def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
    """
    Redirect the user to the login page if not logged in
    """
    return RedirectResponse(url='/login')
```

What do I do wrong? 

Discussion:

Patch:
diff --git a/fastapi_login/fastapi_login.py b/fastapi_login/fastapi_login.py
index b4c4864..7e217f1 100644
--- a/fastapi_login/fastapi_login.py
+++ b/fastapi_login/fastapi_login.py
@@ -58,6 +58,7 @@ class LoginManager(OAuth2PasswordBearer):
 
         """
         if use_cookie is False and use_header is False:
+            # TODO: change this to AttributeError
             raise Exception(
                 "use_cookie and use_header are both False one of them needs to be True"
             )
@@ -72,19 +73,19 @@ class LoginManager(OAuth2PasswordBearer):
             custom_exception or InvalidCredentialsException
         )
 
-        # When a custom_exception is set we have to make sure it is actually raised
-        # when calling super(LoginManager, self).__call__(request) inside `_get_token`
-        # a HTTPException from fastapi is raised automatically as long as auto_error
-        # is set to True
-        if custom_exception is not None:
-            self.auto_error = False
-
         self.use_cookie = use_cookie
         self.use_header = use_header
         self.cookie_name = cookie_name
         self.default_expiry = default_expiry
 
-        super().__init__(tokenUrl=token_url, auto_error=True, scopes=scopes)
+        # When a custom_exception is set we have to make sure it is actually raised
+        # when calling super(LoginManager, self).__call__(request) inside `_get_token`
+        # a HTTPException from fastapi is raised automatically as long as auto_error
+        # is set to True
+        if custom_exception is not None:
+            super().__init__(tokenUrl=token_url, auto_error=False, scopes=scopes)
+        else:
+            super().__init__(tokenUrl=token_url, auto_error=True, scopes=scopes)
 
     @property
     def not_authenticated_exception(self):
@@ -317,7 +318,7 @@ class LoginManager(OAuth2PasswordBearer):
         """
         token = request.cookies.get(self.cookie_name)
 
-        # we dont use `token is None` in case a cookie with self.cookie_name
+        # we don't use `token is None` in case a cookie with self.cookie_name
         # exists but is set to "", in which case `token is None` evaluates to False
         if not token and self.auto_error:
             # either default InvalidCredentialsException or set by user
@@ -346,7 +347,7 @@ class LoginManager(OAuth2PasswordBearer):
                 token = self._token_from_cookie(request)
         # The Exception is either a InvalidCredentialsException
         # or a custom exception set by the user
-        except type(self.not_authenticated_exception):
+        except Exception as _e:
             # In case use_cookie and use_header is enabled
             # headers should be checked if cookie lookup fails
             if self.use_header:
@@ -372,7 +373,7 @@ class LoginManager(OAuth2PasswordBearer):
         """
         try:
             payload = self._get_payload(token)
-        except type(self.not_authenticated_exception):
+        except Exception as _e:
             # We got an error while decoding the token
             return False
 
@@ -429,7 +430,7 @@ class LoginManager(OAuth2PasswordBearer):
         async def user_middleware(request: Request, call_next):
             try:
                 request.state.user = await self.__call__(request)
-            except Exception as e:
+            except Exception as _e:
                 # An error occurred while getting the user
                 # as middlewares are called for every incoming request
                 # it's not a good idea to return the Exception


Test patch:
diff --git a/tests/test_advanced/test_exception_handling.py b/tests/test_advanced/test_exception_handling.py
index e229ddd..d9192a4 100644
--- a/tests/test_advanced/test_exception_handling.py
+++ b/tests/test_advanced/test_exception_handling.py
@@ -15,12 +15,13 @@ def exception_manager(app, secret, token_url, load_user_fn, custom_exception) ->
 
     # exception handling setup
 
+    @app.exception_handler(custom_exception)
     def redirect_on_auth_exc(request, exc):
         return RedirectResponse(url="/redirect")
 
-    app.add_exception_handler(custom_exception, redirect_on_auth_exc)
-
     # routes
+    # Should raise the custom exception when encountering
+    # a non-authenticated user and redirect
     @app.get("/private/exception")
     def raise_exception(_=Depends(instance)):
         return {"detail": "error"}
@@ -43,10 +44,10 @@ async def test_exception_call_cookie_error_user_header_false(clean_manager):
     clean_manager.use_cookie = True
     clean_manager.use_header = False
     response = Mock(cookies={})
-    with pytest.raises(Exception) as exc_info:
+    with pytest.raises(Exception) as exc_info:  # HTTPExceptions cannot be used with pytest.raises
         await clean_manager(response)
 
-    assert exc_info.value == InvalidCredentialsException
+    assert exc_info.value is InvalidCredentialsException
 
 
 @pytest.mark.asyncio
@@ -54,10 +55,10 @@ async def test_exception_call_raises_no_token_auto_error_off(clean_manager):
     clean_manager.auto_error = False
     # set headers so fastapi internals dont raise an error
     response = Mock(headers={"abc": "abc"})
-    with pytest.raises(Exception) as exc_info:
+    with pytest.raises(Exception) as exc_info:   # HTTPExceptions cannot be used with pytest.raises
         await clean_manager(response)
 
-    assert exc_info.value == InvalidCredentialsException
+    assert exc_info.value is InvalidCredentialsException
 
 
 @pytest.mark.asyncio
@@ -72,25 +73,35 @@ async def test_exception_handling(exception_manager, client, invalid_data):
 
 
 @pytest.mark.asyncio
-async def test_exception_change_no_sub(exception_manager, custom_exception):
+async def test_exception_handling_with_no_token(exception_manager, client, invalid_data):
+    # Set use cookie true for this test to check all possible ways to raise an error
+    exception_manager.use_cookie = True
+    resp = await client.get(
+        "/private/exception"
+    )
+    assert resp.json()["detail"] == "Redirected"
+
+
+@pytest.mark.asyncio
+async def test_exception_changes_no_sub(exception_manager, custom_exception):
     no_sub_token = exception_manager.create_access_token(data={"id": "something"})
-    with pytest.raises(custom_exception):
+    with pytest.raises(custom_exception) as exc_info:
         await exception_manager.get_current_user(no_sub_token)
 
 
 @pytest.mark.asyncio
-async def test_exceptions_change_invalid_token(exception_manager, custom_exception):
+async def test_exception_changes_invalid_token(exception_manager, custom_exception):
     invalid_jwt_token = (
         "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aGVsb"
         "G8gd29ybGQ.SIr03zM64awWRdPrAM_61QWsZchAtgDV"
         "3pphfHPPWkI"
     )  # this token is taken from pyjwt (https://github.com/jpadilla/pyjwt/blob/master/tests/test_api_jwt.py#L82)
-    with pytest.raises(custom_exception):
+    with pytest.raises(custom_exception) as exc_info:
         await exception_manager.get_current_user(invalid_jwt_token)
 
 
 @pytest.mark.asyncio
-async def test_exceptions_change_user_is_none(exception_manager, custom_exception, invalid_data):
+async def test_exception_changes_user_is_none(exception_manager, custom_exception, invalid_data):
     invalid_user_token = exception_manager.create_access_token(data={"sub": invalid_data["username"]})
-    with pytest.raises(custom_exception):
+    with pytest.raises(custom_exception) as exc_info:
         await exception_manager.get_current_user(invalid_user_token)