diff options
Diffstat (limited to 'backend/routes')
| -rw-r--r-- | backend/routes/auth/authorize.py | 81 | 
1 files changed, 68 insertions, 13 deletions
diff --git a/backend/routes/auth/authorize.py b/backend/routes/auth/authorize.py index 975936a..2244152 100644 --- a/backend/routes/auth/authorize.py +++ b/backend/routes/auth/authorize.py @@ -2,17 +2,23 @@  Use a token received from the Discord OAuth2 system to fetch user information.  """ +import datetime +from typing import Union +  import httpx  import jwt  from pydantic.fields import Field  from pydantic.main import BaseModel  from spectree.response import Response +from starlette.authentication import requires  from starlette.requests import Request  from starlette.responses import JSONResponse +from backend import constants +from backend.authentication.user import User  from backend.constants import SECRET_KEY -from backend.route import Route  from backend.discord import fetch_bearer_token, fetch_user_details +from backend.route import Route  from backend.validation import ErrorMessage, api @@ -21,7 +27,42 @@ class AuthorizeRequest(BaseModel):  class AuthorizeResponse(BaseModel): -    token: str = Field(description="A JWT token containing the user information") +    username: str = Field("Discord display name.") + + +AUTH_FAILURE = JSONResponse({"error": "auth_failure"}, status_code=400) + + +async def process_token(bearer_token: dict) -> Union[AuthorizeResponse, AUTH_FAILURE]: +    """Post a bearer token to Discord, and return a JWT and username.""" +    interaction_start = datetime.datetime.now() + +    try: +        user_details = await fetch_user_details(bearer_token["access_token"]) +    except httpx.HTTPStatusError: +        AUTH_FAILURE.delete_cookie("BackendToken") +        return AUTH_FAILURE + +    max_age = datetime.timedelta(seconds=int(bearer_token["expires_in"])) +    token_expiry = interaction_start + max_age + +    data = { +        "token": bearer_token["access_token"], +        "refresh": bearer_token["refresh_token"], +        "user_details": user_details, +        "expiry": token_expiry.isoformat() +    } + +    token = jwt.encode(data, SECRET_KEY, algorithm="HS256") +    user = User(token, user_details) + +    response = JSONResponse({"username": user.display_name}) +    response.set_cookie( +        "BackendToken", f"JWT {token}", +        secure=constants.PRODUCTION, httponly=True, samesite="strict", +        max_age=bearer_token["expires_in"] +    ) +    return response  class AuthorizeRoute(Route): @@ -40,19 +81,33 @@ class AuthorizeRoute(Route):      async def post(self, request: Request) -> JSONResponse:          """Generate an authorization token."""          data = await request.json() -          try: -            bearer_token = await fetch_bearer_token(data["token"]) -            user_details = await fetch_user_details(bearer_token["access_token"]) +            bearer_token = await fetch_bearer_token(data["token"], refresh=False)          except httpx.HTTPStatusError: -            return JSONResponse({ -                "error": "auth_failure" -            }, status_code=400) +            return AUTH_FAILURE + +        return await process_token(bearer_token) -        user_details["admin"] = await request.state.db.admins.find_one( -            {"_id": user_details["id"]} -        ) is not None -        token = jwt.encode(user_details, SECRET_KEY, algorithm="HS256") +class TokenRefreshRoute(Route): +    """ +    Use the refresh code from a JWT to get a new token and generate a new JWT token. +    """ + +    name = "refresh" +    path = "/refresh" + +    @requires(["authenticated"]) +    @api.validate( +        resp=Response(HTTP_200=AuthorizeResponse, HTTP_400=ErrorMessage), +        tags=["auth"] +    ) +    async def post(self, request: Request) -> JSONResponse: +        """Refresh an authorization token.""" +        try: +            token = request.user.decoded_token.get("refresh") +            bearer_token = await fetch_bearer_token(token, refresh=True) +        except httpx.HTTPStatusError: +            return AUTH_FAILURE -        return JSONResponse({"token": token}) +        return await process_token(bearer_token)  |