diff --git a/classquiz/__init__.py b/classquiz/__init__.py index 0afb815..5b873f1 100644 --- a/classquiz/__init__.py +++ b/classquiz/__init__.py @@ -12,6 +12,8 @@ from starlette.middleware.sessions import SessionMiddleware from classquiz.config import settings from classquiz.db import database from datetime import timedelta + +from classquiz.oauth import rememberme_middleware from classquiz.routers import users, quiz, utils, stats, storage, search, testing_routes, editor, live, eximport from classquiz.socket_server import sio from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks @@ -60,6 +62,11 @@ async def shutdown() -> None: await database_.disconnect() +@app.middleware("http") +async def auth_middleware_wrapper(request: Request, call_next): + return await rememberme_middleware(request, call_next) + + app.add_middleware(SessionMiddleware, secret_key=settings.secret_key) app.include_router(users.router, tags=["users"], prefix="/api/v1/users", include_in_schema=True) app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz", include_in_schema=True) diff --git a/classquiz/auth.py b/classquiz/auth.py index ac432d3..ed81a58 100644 --- a/classquiz/auth.py +++ b/classquiz/auth.py @@ -44,7 +44,11 @@ class OAuth2PasswordBearerWithCookie(OAuth2): async def __call__(self, request: Request) -> Optional[str]: authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie - + if authorization is None: + try: + authorization = request.state.access_token + except AttributeError: + pass scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: diff --git a/classquiz/oauth/__init__.py b/classquiz/oauth/__init__.py index db7a4f9..5b35f89 100644 --- a/classquiz/oauth/__init__.py +++ b/classquiz/oauth/__init__.py @@ -1,11 +1,44 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from datetime import timedelta, datetime -from fastapi import APIRouter +from fastapi import APIRouter, Request, Response + +from classquiz.auth import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token +from classquiz.db.models import UserSession from classquiz.oauth import google, github router = APIRouter() router.include_router(google.router, prefix="/google") router.include_router(github.router, prefix="/github") + + +async def rememberme_middleware(request: Request, call_next): + rememberme_cookie = request.cookies.get("rememberme_token") + bearer_token = request.cookies.get("access_token") + if rememberme_cookie is not None and bearer_token is None: + user_session: UserSession | None = ( + await UserSession.objects.filter(session_key=rememberme_cookie) + .select_related(UserSession.user) + .get_or_none() + ) + if (user_session is None) or (user_session.user is None): + response: Response = await call_next(request) + return response + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES * 60) + access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires) + await user_session.update(last_seen=datetime.now()) + request.state.access_token = f"Bearer {access_token}" + response: Response = await call_next(request) + response.set_cookie( + key="access_token", + value=f"Bearer {access_token}", + httponly=True, + samesite="lax", + max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60, + ) + else: + response: Response = await call_next(request) + return response diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 6fccfe9..d1dd4d3 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -9,37 +9,15 @@ import type { Handle } from '@sveltejs/kit'; /** @type {import('@sveltejs/kit').Handle} */ export const handle: Handle = async ({ event, resolve }) => { - const cookies = cookie.parse(event.request.headers.get('cookie') || ''); - const jwt = /^Bearer (.*)$/gm.exec(cookies.access_token); - const rememberme_token = cookies.rememberme_token; - if (rememberme_token) { - const res = await fetch(`${process.env.API_URL}/api/v1/users/auth/internal`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - rememberme: rememberme_token, - jwt: jwt === null ? undefined : jwt[0] - }) - }); - let new_jwt; - if (jwt) { - new_jwt = jwt[0]; - } else { - new_jwt = cookie.parse(res.headers.get('set-cookie') ?? '').access_token; + const res = await fetch(`${process.env.API_URL}/api/v1/users/check`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Cookie: event.request.headers.get('cookie') || '' } - event.locals.email = await ( - await fetch(`${process.env.API_URL}/api/v1/users/auth/internal/email`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - jwt: new_jwt - }) - }) - ).text(); + }); + if (res.ok) { + event.locals.email = await res.text(); const resp = await resolve(event); try { resp.headers.set('Set-Cookie', res.headers.get('set-cookie'));