From 6a6d28038494626ed74fc842c814477b16af1e75 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 09:54:22 +0200 Subject: Create BaseUser implementation for JWT authentication --- backend/authentication/__init__.py | 3 +++ backend/authentication/user.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 backend/authentication/__init__.py create mode 100644 backend/authentication/user.py diff --git a/backend/authentication/__init__.py b/backend/authentication/__init__.py new file mode 100644 index 0000000..35b01f3 --- /dev/null +++ b/backend/authentication/__init__.py @@ -0,0 +1,3 @@ +from .user import User + +__all__ = ["User"] diff --git a/backend/authentication/user.py b/backend/authentication/user.py new file mode 100644 index 0000000..afa243f --- /dev/null +++ b/backend/authentication/user.py @@ -0,0 +1,22 @@ +import typing as t +from abc import ABC + +from starlette.authentication import BaseUser + + +class User(BaseUser, ABC): + """Starlette BaseUser implementation for JWT authentication.""" + + def __init__(self, token: str, payload: t.Dict) -> None: + self.token = token + self.payload = payload + + @property + def is_authenticated(self) -> bool: + """Returns True because user is always authenticated at this stage.""" + return True + + @property + def display_name(self) -> str: + """Return username and discriminator as display name.""" + return f"{self.payload['username']}#{self.payload['discriminator']}" -- cgit v1.2.3 From 79cdf5e89f6825b5b20c54772fc62b2a944d505a Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 10:18:04 +0200 Subject: Create authentication backend for JWT --- backend/authentication/__init__.py | 3 ++- backend/authentication/backend.py | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 backend/authentication/backend.py diff --git a/backend/authentication/__init__.py b/backend/authentication/__init__.py index 35b01f3..43601a7 100644 --- a/backend/authentication/__init__.py +++ b/backend/authentication/__init__.py @@ -1,3 +1,4 @@ +from .backend import JWTAuthenticationBackend from .user import User -__all__ = ["User"] +__all__ = ["JWTAuthenticationBackend", "User"] diff --git a/backend/authentication/backend.py b/backend/authentication/backend.py new file mode 100644 index 0000000..c38bfaf --- /dev/null +++ b/backend/authentication/backend.py @@ -0,0 +1,52 @@ +import jwt +import typing as t +from abc import ABC + +from starlette import authentication +from starlette.requests import Request + +from backend import constants +from backend.authentication import User + + +class JWTAuthenticationBackend(authentication.AuthenticationBackend, ABC): + """Custom Starlette authentication backend for JWT.""" + + @staticmethod + def get_token_from_header(header: str) -> t.Optional[str]: + """Parse JWT token from header value.""" + try: + prefix, token = header.split() + except ValueError: + raise authentication.AuthenticationError( + "Unable to split prefix and token from Authorization header." + ) + + if prefix.upper() != "JWT": + raise authentication.AuthenticationError( + f"Invalid Authorization header prefix '{prefix}'." + ) + + return token + + async def authenticate( + self, request: Request + ) -> t.Optional[t.Tuple[authentication.AuthCredentials, authentication.BaseUser]]: + """Handles JWT authentication process.""" + if "Authorization" not in request.headers: + return + + auth = request.headers["Authorization"] + token = self.get_token_from_header(auth) + + try: + payload = jwt.decode(token, constants.SECRET_KEY, algorithms=["HS256"]) + except jwt.InvalidTokenError as e: + raise authentication.AuthenticationError(str(e)) + + scopes = ["authenticated"] + + if payload.get("admin", False) is True: + scopes.append("admin") + + return authentication.AuthCredentials(scopes), User(token, payload) -- cgit v1.2.3 From 04c019feba935ade892ca06e39fafe7d2ec8de14 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 10:24:24 +0200 Subject: Fix import of user --- backend/authentication/backend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/authentication/backend.py b/backend/authentication/backend.py index c38bfaf..38668eb 100644 --- a/backend/authentication/backend.py +++ b/backend/authentication/backend.py @@ -6,7 +6,8 @@ from starlette import authentication from starlette.requests import Request from backend import constants -from backend.authentication import User +# We must import user such way here to avoid circular imports +from .user import User class JWTAuthenticationBackend(authentication.AuthenticationBackend, ABC): -- cgit v1.2.3 From b5e1b67c5f34685308035a9c8c602f94ebef35b1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 10:24:43 +0200 Subject: Add JWT authentication middleware to middlewares list --- backend/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/__init__.py b/backend/__init__.py index 6215961..7b4cb4d 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -2,8 +2,10 @@ import os from starlette.applications import Starlette from starlette.middleware import Middleware +from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.cors import CORSMiddleware +from backend.authentication import JWTAuthenticationBackend from backend.route_manager import create_route_map from backend.middleware import DatabaseMiddleware @@ -19,7 +21,8 @@ middleware = [ ], allow_methods=["*"] ), - Middleware(DatabaseMiddleware) + Middleware(DatabaseMiddleware), + Middleware(AuthenticationMiddleware, backend=JWTAuthenticationBackend()) ] app = Starlette(routes=create_route_map(), middleware=middleware) -- cgit v1.2.3 From 4714e9a277f024141fa30f0ec6739b8acf390331 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 1 Dec 2020 19:03:46 +0000 Subject: Add user property to index response --- backend/routes/index.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/routes/index.py b/backend/routes/index.py index 8144723..b37f381 100644 --- a/backend/routes/index.py +++ b/backend/routes/index.py @@ -18,7 +18,19 @@ class IndexRoute(Route): path = "/" def get(self, request: Request) -> JSONResponse: - return JSONResponse({ + response_data = { "message": "Hello, world!", - "client": request.client.host - }) + "client": request.client.host, + "user": { + "authenticated": False + } + } + + if request.user.is_authenticated: + response_data["user"] = { + "authenticated": True, + "user": request.user.payload, + "scopes": request.auth.scopes + } + + return JSONResponse(response_data) -- cgit v1.2.3