From 41b3faafe1f5f53c62eda498c8e1f3ecc26a7f9b Mon Sep 17 00:00:00 2001 From: Melissa Autumn Date: Fri, 17 Nov 2023 11:30:55 -0800 Subject: [PATCH] Add session support for oauth flows. --- backend/.env.example | 4 +++ backend/requirements.txt | 1 + backend/src/appointment/controller/auth.py | 2 +- backend/src/appointment/dependencies/auth.py | 11 ++++++++- backend/src/appointment/main.py | 7 ++++++ backend/src/appointment/routes/zoom.py | 26 ++++++++++++++++---- backend/src/appointment/secrets.py | 1 + frontend/src/App.vue | 1 + 8 files changed, 46 insertions(+), 7 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index f4123e2c5..b8a92a38e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -16,6 +16,10 @@ DATABASE_SECRETS= # Secret phrase for database encryption (e.g. create it by running `openssl rand -hex 32`) DB_SECRET= +# -- SESSION -- +# Secret phrase for session encryption +SESSION_SECRET= + # -- AUTH0 -- # Management API AUTH0_API_CLIENT_ID= diff --git a/backend/requirements.txt b/backend/requirements.txt index ec79985e2..156dbad6e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,6 +9,7 @@ google-auth-httplib2==0.1.0 google-auth-oauthlib==1.0.0 jinja2==3.1.2 icalendar==5.0.4 +itsdangerous==2.1.2 mysqlclient==2.1.1 mysql-connector-python==8.0.32 python-dotenv==1.0.0 diff --git a/backend/src/appointment/controller/auth.py b/backend/src/appointment/controller/auth.py index 1de6f2c26..1f0bbdc20 100644 --- a/backend/src/appointment/controller/auth.py +++ b/backend/src/appointment/controller/auth.py @@ -26,7 +26,7 @@ class Auth: def __init__(self): """verify Appointment subscription via Auth0, return user or None""" scopes = {"read:calendars": "Read Calendar Ressources"} # TODO - self.auth0 = Auth0(domain=domain, api_audience=api_audience, scopes=scopes) + self.auth0 = Auth0(domain=domain, api_audience=api_audience, scopes=scopes, auto_error=False) def persist_user(self, db: Session, user: Auth0User): """Sync authed user to Appointment db""" diff --git a/backend/src/appointment/dependencies/auth.py b/backend/src/appointment/dependencies/auth.py index 8a0febb65..5d0462c49 100644 --- a/backend/src/appointment/dependencies/auth.py +++ b/backend/src/appointment/dependencies/auth.py @@ -1,4 +1,4 @@ -from fastapi import Depends, Security, Request +from fastapi import Depends, Security, Request, HTTPException from fastapi_auth0 import Auth0User from sqlalchemy.orm import Session @@ -17,6 +17,15 @@ def get_subscriber( user: Auth0User = Security(auth.auth0.get_user), ): """Automatically retrieve and return the subscriber based on the authenticated Auth0 user""" + + # Error out if auth0 didn't find a user + if user is None: + raise HTTPException(403, detail='Missing bearer token') + user = repo.get_subscriber_by_email(db, user.email) + # Error out if we didn't find a user + if user is None: + raise HTTPException(400, detail='Unknown user') + return user diff --git a/backend/src/appointment/main.py b/backend/src/appointment/main.py index 6fb4d0e64..2752d2e78 100644 --- a/backend/src/appointment/main.py +++ b/backend/src/appointment/main.py @@ -2,6 +2,8 @@ Boot application, init database, authenticate user and provide all API endpoints. """ +from starlette.middleware.sessions import SessionMiddleware + # Ignore "Module level import not at top of file" # ruff: noqa: E402 from .secrets import normalize_secrets @@ -72,6 +74,11 @@ def server(): # init app app = FastAPI() + app.add_middleware( + SessionMiddleware, + secret_key=os.getenv("SESSION_SECRET") + ) + # allow requests from own frontend running on a different port app.add_middleware( CORSMiddleware, diff --git a/backend/src/appointment/routes/zoom.py b/backend/src/appointment/routes/zoom.py index 81dfe32fe..3e180121a 100644 --- a/backend/src/appointment/routes/zoom.py +++ b/backend/src/appointment/routes/zoom.py @@ -1,7 +1,7 @@ import json import os -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Request, HTTPException from fastapi.responses import RedirectResponse from sqlalchemy.orm import Session @@ -18,6 +18,7 @@ @router.get("/auth") def zoom_auth( + request: Request, subscriber: Subscriber = Depends(get_subscriber), zoom_client: ZoomClient = Depends(get_zoom_client), ): @@ -25,19 +26,34 @@ def zoom_auth( url, state = zoom_client.get_redirect_url(state=sign_url(str(subscriber.id))) + # We'll need to store this in session + request.session['zoom_state'] = state + request.session['zoom_user_id'] = subscriber.id + return {'url': url} @router.get("/callback") def zoom_callback( + request: Request, code: str, state: str, - zoom_client: ZoomClient = Depends(get_zoom_client), - subscriber: Subscriber = Depends(get_subscriber), db=Depends(get_db), ): - if sign_url(str(subscriber.id)) != state: - raise RuntimeError("States do not match!") + if 'zoom_state' not in request.session or request.session['zoom_state'] != state: + raise HTTPException(400, "Invalid state.") + if 'zoom_user_id' not in request.session or 'zoom_user_id' == '': + raise HTTPException(400, "User ID could not be retrieved.") + + # Retrieve the user id set at the start of the zoom oauth process + subscriber = repo.get_subscriber(db, request.session['zoom_user_id']) + + # Clear zoom session keys + request.session.pop('zoom_state') + request.session.pop('zoom_user_id') + + # Generate the zoom client instance based on our subscriber (this can't be set as a dep injection since subscriber is based on session. + zoom_client: ZoomClient = get_zoom_client(subscriber) creds = zoom_client.get_credentials(code) diff --git a/backend/src/appointment/secrets.py b/backend/src/appointment/secrets.py index 288382866..7e70f03be 100644 --- a/backend/src/appointment/secrets.py +++ b/backend/src/appointment/secrets.py @@ -31,6 +31,7 @@ def normalize_secrets(): os.environ["DB_SECRET"] = secrets.get("secret") # Technically not db related...might rename this item later. os.environ["SIGNED_SECRET"] = secrets.get("signed_secret") + os.environ["SESSION_SECRET"] = secrets.get("session_secret") smtp_secrets = os.getenv("SMTP_SECRETS") diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 21dc5394b..46ce784ee 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -100,6 +100,7 @@ const call = createFetch({ }, fetchOptions: { mode: "cors", + credentials: "include", }, }); provide("auth", auth);