Skip to content

Commit

Permalink
Add session support for oauth flows.
Browse files Browse the repository at this point in the history
  • Loading branch information
MelissaAutumn committed Nov 17, 2023
1 parent bbfd249 commit 41b3faa
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 7 deletions.
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion backend/src/appointment/controller/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
11 changes: 10 additions & 1 deletion backend/src/appointment/dependencies/auth.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
7 changes: 7 additions & 0 deletions backend/src/appointment/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 21 additions & 5 deletions backend/src/appointment/routes/zoom.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -18,26 +18,42 @@

@router.get("/auth")
def zoom_auth(
request: Request,
subscriber: Subscriber = Depends(get_subscriber),
zoom_client: ZoomClient = Depends(get_zoom_client),
):
"""Starts the zoom oauth process"""

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)

Expand Down
1 change: 1 addition & 0 deletions backend/src/appointment/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
1 change: 1 addition & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ const call = createFetch({
},
fetchOptions: {
mode: "cors",
credentials: "include",
},
});
provide("auth", auth);
Expand Down

0 comments on commit 41b3faa

Please sign in to comment.