From ec5bdef719b5b0b7c25e7441866500f22248fc13 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sun, 22 Jan 2023 21:36:52 +0900 Subject: [PATCH 1/9] Not sure this will work --- backend/oasst_backend/api/v1/api.py | 2 ++ backend/requirements.txt | 2 ++ docker-compose.yaml | 2 +- scripts/backend-development/run-local.sh | 1 + website/src/pages/api/auth/[...nextauth].ts | 11 +++++++++++ website/src/pages/dashboard.tsx | 3 +++ 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/oasst_backend/api/v1/api.py b/backend/oasst_backend/api/v1/api.py index 2931ac05..003f039f 100644 --- a/backend/oasst_backend/api/v1/api.py +++ b/backend/oasst_backend/api/v1/api.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from oasst_backend.api.v1 import ( admin, + auth, frontend_messages, frontend_users, hugging_face, @@ -23,3 +24,4 @@ api_router.include_router(stats.router, prefix="/stats", tags=["stats"]) api_router.include_router(leaderboards.router, prefix="/leaderboards", tags=["leaderboards"]) api_router.include_router(hugging_face.router, prefix="/hf", tags=["hugging_face"]) api_router.include_router(admin.router, prefix="/admin", tags=["admin"]) +api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) diff --git a/backend/requirements.txt b/backend/requirements.txt index 0f91315e..dff8d14c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,7 +6,9 @@ loguru==0.6.0 numpy==1.22.4 psycopg2-binary==2.9.5 pydantic==1.9.1 +pyjwt python-dotenv==0.21.0 +redis scipy==1.8.1 SQLAlchemy==1.4.41 sqlmodel==0.0.8 diff --git a/docker-compose.yaml b/docker-compose.yaml index 908457cd..60048763 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,7 @@ services: # Use `docker compose up backend-dev --build --attach-dependencies` to start a database and work and the backend. backend-dev: image: sverrirab/sleep - depends_on: [db, adminer, redis, redis-insights] + depends_on: [db, adminer, redis, redis-insights, webdb, maildev] # Use `docker compose up frontend-dev --build --attach-dependencies` to start all services needed to work on the frontend. frontend-dev: diff --git a/scripts/backend-development/run-local.sh b/scripts/backend-development/run-local.sh index 7366cde6..f81362dd 100755 --- a/scripts/backend-development/run-local.sh +++ b/scripts/backend-development/run-local.sh @@ -8,6 +8,7 @@ export DEBUG_USE_SEED_DATA=True export DEBUG_SKIP_TOXICITY_CALCULATION=True export DEBUG_ALLOW_SELF_LABELING=True export DEBUG_SKIP_EMBEDDING_COMPUTATION=True +export BACKEND_CORS_ORIGINS='["http://localhost:3000"]' uvicorn main:app --reload --port 8080 --host 0.0.0.0 diff --git a/website/src/pages/api/auth/[...nextauth].ts b/website/src/pages/api/auth/[...nextauth].ts index 3d3dbaa4..af2cbd59 100644 --- a/website/src/pages/api/auth/[...nextauth].ts +++ b/website/src/pages/api/auth/[...nextauth].ts @@ -148,6 +148,17 @@ export const authOptions: AuthOptions = { } }, }, + cookies: { + sessionToken: { + name: `next-auth.session-token`, + options: { + httpOnly: true, + sameSite: "none", + path: "/", + secure: true, + }, + }, + }, session: { strategy: "jwt", }, diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx index e0b8bba4..2739821b 100644 --- a/website/src/pages/dashboard.tsx +++ b/website/src/pages/dashboard.tsx @@ -7,9 +7,12 @@ import { TaskCategory } from "src/components/Tasks/TaskTypes"; import { get } from "src/lib/api"; import type { AvailableTasks, TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; +import useSWR from "swr"; import useSWRImmutable from "swr/immutable"; const Dashboard = () => { + useSWR("http://localhost:8080/api/v1/auth/check", get); + const { data } = useSWRImmutable("/api/available_tasks", get); // TODO: show only these tasks: From 3c2d1086b80f354e52c10bd22c84aef0cb574e1b Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Mon, 23 Jan 2023 18:13:33 +0900 Subject: [PATCH 2/9] Adding a new auth route --- backend/oasst_backend/api/v1/auth.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 backend/oasst_backend/api/v1/auth.py diff --git a/backend/oasst_backend/api/v1/auth.py b/backend/oasst_backend/api/v1/auth.py new file mode 100644 index 00000000..a2386362 --- /dev/null +++ b/backend/oasst_backend/api/v1/auth.py @@ -0,0 +1,38 @@ +from typing import Union + +import jwt +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import APIKeyCookie +from pydantic import BaseModel + +router = APIRouter() + +oauth2_scheme = APIKeyCookie(name="next-auth.session-token") + +SECRET_KEY = "O/M2uIbGj+lDD2oyNa8ax4jEOJqCPJzO53UbWShmq98=" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +class TokenData(BaseModel): + sub: Union[str, None] = None + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + print("get_current_user") + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + print(payload) + sub: str = payload.get("sub") + if sub is None: + raise credentials_exception + return TokenData(sub=sub) + + +@router.get("/check", response_model=str) +async def auth_check(token_data: TokenData = Depends(get_current_user)): + return token_data.sub From 5b7c32ebec6fd7a80f9d5366c2ef156003ae9244 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Tue, 24 Jan 2023 16:48:34 +0900 Subject: [PATCH 3/9] Updating axios to always send credentials --- website/src/lib/api.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/src/lib/api.ts b/website/src/lib/api.ts index df4bd399..f7a29f49 100644 --- a/website/src/lib/api.ts +++ b/website/src/lib/api.ts @@ -6,8 +6,11 @@ const headers = { "Content-Type": "application/json", }; +// Create Axios such that we always send credential cookies along with the +// request. This allows the Backend services to authenticate the user. const api = axios.create({ headers, + withCredentials: true, }); export const get = (url: string) => api.get(url).then((res) => res.data); From 76ef08dfe481016d886c8eb723902f713ab96633 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Wed, 25 Jan 2023 18:12:44 +0900 Subject: [PATCH 4/9] decoding --- backend/oasst_backend/api/v1/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/oasst_backend/api/v1/auth.py b/backend/oasst_backend/api/v1/auth.py index a2386362..9aa46150 100644 --- a/backend/oasst_backend/api/v1/auth.py +++ b/backend/oasst_backend/api/v1/auth.py @@ -25,7 +25,8 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + print(token) + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_signature": False}) print(payload) sub: str = payload.get("sub") if sub is None: From 62d00d5bd4af27a9ce0e39048e0783536a100438 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 28 Jan 2023 16:56:34 +0900 Subject: [PATCH 5/9] Adding a minimal backend API route that decrypts the web's JWE and returns the email --- backend/oasst_backend/api/v1/auth.py | 54 ++++++++++++++++------------ backend/oasst_backend/config.py | 9 +++++ backend/requirements.txt | 3 +- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/backend/oasst_backend/api/v1/auth.py b/backend/oasst_backend/api/v1/auth.py index 9aa46150..888be410 100644 --- a/backend/oasst_backend/api/v1/auth.py +++ b/backend/oasst_backend/api/v1/auth.py @@ -1,39 +1,49 @@ +import json from typing import Union -import jwt -from fastapi import APIRouter, Depends, HTTPException, status +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from fastapi import APIRouter, Depends, Security from fastapi.security import APIKeyCookie +from jose import jwe +from oasst_backend.config import settings from pydantic import BaseModel router = APIRouter() -oauth2_scheme = APIKeyCookie(name="next-auth.session-token") - -SECRET_KEY = "O/M2uIbGj+lDD2oyNa8ax4jEOJqCPJzO53UbWShmq98=" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 +oauth2_scheme = APIKeyCookie(name=settings.AUTH_COOKIE_NAME) class TokenData(BaseModel): - sub: Union[str, None] = None + """ + A minimal re-creation of the web's token type. To be expanded later. + """ + + email: Union[str, None] = None -async def get_current_user(token: str = Depends(oauth2_scheme)): - print("get_current_user") - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, +async def get_current_user(token: str = Security(oauth2_scheme)): + """ + Decrypts the user's JSON Web Token using HKDF encryption and returns the + TokenData. + """ + # We first generate a key from the auth secret. + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=settings.AUTH_LENGTH, + salt=settings.AUTH_SALT, + info=settings.AUTH_INFO, ) - print(token) - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_signature": False}) - print(payload) - sub: str = payload.get("sub") - if sub is None: - raise credentials_exception - return TokenData(sub=sub) + key = hkdf.derive(settings.AUTH_SECRET) + # Next we decrypt the JWE token. + payload = jwe.decrypt(token, key) + # Finally we have the real token JSON payload and can do whatever we want. + content = json.loads(payload) + email = content["email"] + return TokenData(email=email) @router.get("/check", response_model=str) async def auth_check(token_data: TokenData = Depends(get_current_user)): - return token_data.sub + """Returns the user's email if it can be decrypted.""" + return token_data.email diff --git a/backend/oasst_backend/config.py b/backend/oasst_backend/config.py index 8ca2a413..a6ed1d60 100644 --- a/backend/oasst_backend/config.py +++ b/backend/oasst_backend/config.py @@ -63,6 +63,15 @@ class Settings(BaseSettings): API_V1_STR: str = "/api/v1" OFFICIAL_WEB_API_KEY: str = "1234" + # Encryption fields for handling the web generated JSON Web Tokens. + # These fields need to be shared with the web's auth settings in order to + # correctly decrypt the web tokens. + AUTH_INFO: bytes = b"NextAuth.js Generated Encryption Key" + AUTH_SALT: bytes = b"" + AUTH_LENGTH: int = 32 + AUTH_SECRET: bytes = b"O/M2uIbGj+lDD2oyNa8ax4jEOJqCPJzO53UbWShmq98=" + AUTH_COOKIE_NAME: str = "next-auth.session-token" + POSTGRES_HOST: str = "localhost" POSTGRES_PORT: str = "5432" POSTGRES_USER: str = "postgres" diff --git a/backend/requirements.txt b/backend/requirements.txt index dff8d14c..1f66fe09 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,5 @@ alembic==1.8.1 +cryptography==39.0.0 fastapi==0.88.0 fastapi-limiter==0.1.5 fastapi-utils==0.2.1 @@ -6,8 +7,8 @@ loguru==0.6.0 numpy==1.22.4 psycopg2-binary==2.9.5 pydantic==1.9.1 -pyjwt python-dotenv==0.21.0 +python-jose[cryptography]==3.3.0 redis scipy==1.8.1 SQLAlchemy==1.4.41 From 75c4f90db3187ed6a8cb14a41c091982b0fa0d13 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 28 Jan 2023 17:58:52 +0900 Subject: [PATCH 6/9] Reverting some uneccesary changes --- docker-compose.yaml | 2 +- scripts/backend-development/run-local.sh | 1 - website/src/pages/api/auth/[...nextauth].ts | 4 ++++ website/src/pages/dashboard.tsx | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 60048763..908457cd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,7 @@ services: # Use `docker compose up backend-dev --build --attach-dependencies` to start a database and work and the backend. backend-dev: image: sverrirab/sleep - depends_on: [db, adminer, redis, redis-insights, webdb, maildev] + depends_on: [db, adminer, redis, redis-insights] # Use `docker compose up frontend-dev --build --attach-dependencies` to start all services needed to work on the frontend. frontend-dev: diff --git a/scripts/backend-development/run-local.sh b/scripts/backend-development/run-local.sh index f81362dd..7366cde6 100755 --- a/scripts/backend-development/run-local.sh +++ b/scripts/backend-development/run-local.sh @@ -8,7 +8,6 @@ export DEBUG_USE_SEED_DATA=True export DEBUG_SKIP_TOXICITY_CALCULATION=True export DEBUG_ALLOW_SELF_LABELING=True export DEBUG_SKIP_EMBEDDING_COMPUTATION=True -export BACKEND_CORS_ORIGINS='["http://localhost:3000"]' uvicorn main:app --reload --port 8080 --host 0.0.0.0 diff --git a/website/src/pages/api/auth/[...nextauth].ts b/website/src/pages/api/auth/[...nextauth].ts index af2cbd59..7a5ddbe4 100644 --- a/website/src/pages/api/auth/[...nextauth].ts +++ b/website/src/pages/api/auth/[...nextauth].ts @@ -148,6 +148,9 @@ export const authOptions: AuthOptions = { } }, }, + /* + * We maybe need this, we maybe don't. Checking in this uncommented until + * it's confirmed we can drop this. cookies: { sessionToken: { name: `next-auth.session-token`, @@ -159,6 +162,7 @@ export const authOptions: AuthOptions = { }, }, }, + */ session: { strategy: "jwt", }, diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx index 2739821b..8b9e3cae 100644 --- a/website/src/pages/dashboard.tsx +++ b/website/src/pages/dashboard.tsx @@ -11,7 +11,8 @@ import useSWR from "swr"; import useSWRImmutable from "swr/immutable"; const Dashboard = () => { - useSWR("http://localhost:8080/api/v1/auth/check", get); + // Adding a demonstrative call to the backend that includes the web's JWT. + useSWR(`${process.env.BACKEND_URL}/api/v1/auth/check`, get); const { data } = useSWRImmutable("/api/available_tasks", get); From 9541607473fba9a2c7129713654191d3a9e59b46 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 28 Jan 2023 18:02:01 +0900 Subject: [PATCH 7/9] Small change to simplify the demo call to the backend --- website/src/pages/dashboard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx index 8b9e3cae..3f97301f 100644 --- a/website/src/pages/dashboard.tsx +++ b/website/src/pages/dashboard.tsx @@ -7,12 +7,11 @@ import { TaskCategory } from "src/components/Tasks/TaskTypes"; import { get } from "src/lib/api"; import type { AvailableTasks, TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -import useSWR from "swr"; import useSWRImmutable from "swr/immutable"; const Dashboard = () => { // Adding a demonstrative call to the backend that includes the web's JWT. - useSWR(`${process.env.BACKEND_URL}/api/v1/auth/check`, get); + useSWRImmutable(`${process.env.FASTAPI_URL}/api/v1/auth/check`, get); const { data } = useSWRImmutable("/api/available_tasks", get); From 3dc8ff6ddd78120595bdfcf50aa629962c24b550 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 28 Jan 2023 19:22:24 +0900 Subject: [PATCH 8/9] Fixing a build error from merging --- website/src/pages/dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx index 6236b467..caa41c14 100644 --- a/website/src/pages/dashboard.tsx +++ b/website/src/pages/dashboard.tsx @@ -12,7 +12,7 @@ import useSWR from "swr"; const Dashboard = () => { // Adding a demonstrative call to the backend that includes the web's JWT. - useSWRImmutable(`${process.env.FASTAPI_URL}/api/v1/auth/check`, get); + useSWR(`${process.env.FASTAPI_URL}/api/v1/auth/check`, get); const { t, From 3197b6088be439ee77c8a41e836c1be54700e1fe Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 28 Jan 2023 19:35:40 +0900 Subject: [PATCH 9/9] Using more pydantic features in the backend and fixing env issues on the website --- backend/oasst_backend/api/v1/auth.py | 9 +++------ backend/requirements.txt | 1 + website/.env | 3 +++ website/src/pages/dashboard.tsx | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/oasst_backend/api/v1/auth.py b/backend/oasst_backend/api/v1/auth.py index 888be410..838b0f52 100644 --- a/backend/oasst_backend/api/v1/auth.py +++ b/backend/oasst_backend/api/v1/auth.py @@ -1,4 +1,3 @@ -import json from typing import Union from cryptography.hazmat.primitives import hashes @@ -7,7 +6,7 @@ from fastapi import APIRouter, Depends, Security from fastapi.security import APIKeyCookie from jose import jwe from oasst_backend.config import settings -from pydantic import BaseModel +from pydantic import BaseModel, EmailStr router = APIRouter() @@ -19,7 +18,7 @@ class TokenData(BaseModel): A minimal re-creation of the web's token type. To be expanded later. """ - email: Union[str, None] = None + email: Union[EmailStr, None] = None async def get_current_user(token: str = Security(oauth2_scheme)): @@ -38,9 +37,7 @@ async def get_current_user(token: str = Security(oauth2_scheme)): # Next we decrypt the JWE token. payload = jwe.decrypt(token, key) # Finally we have the real token JSON payload and can do whatever we want. - content = json.loads(payload) - email = content["email"] - return TokenData(email=email) + return TokenData.parse_raw(payload) @router.get("/check", response_model=str) diff --git a/backend/requirements.txt b/backend/requirements.txt index 1f66fe09..4a112bc8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,6 +7,7 @@ loguru==0.6.0 numpy==1.22.4 psycopg2-binary==2.9.5 pydantic==1.9.1 +pydantic[email]==1.9.1 python-dotenv==0.21.0 python-jose[cryptography]==3.3.0 redis diff --git a/website/.env b/website/.env index 65d8b88e..18cbfcde 100644 --- a/website/.env +++ b/website/.env @@ -7,6 +7,9 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5433/oasst_web FASTAPI_URL=http://localhost:8080 FASTAPI_KEY=1234 +# Used to expose the backend url to the clientside javascript +NEXT_PUBLIC_BACKEND_URL=$FASTAPI_URL + # A dev Auth Secret. Can be exposed if we never use this publicly. NEXTAUTH_SECRET=O/M2uIbGj+lDD2oyNa8ax4jEOJqCPJzO53UbWShmq98= diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx index caa41c14..dbba3c8a 100644 --- a/website/src/pages/dashboard.tsx +++ b/website/src/pages/dashboard.tsx @@ -12,7 +12,7 @@ import useSWR from "swr"; const Dashboard = () => { // Adding a demonstrative call to the backend that includes the web's JWT. - useSWR(`${process.env.FASTAPI_URL}/api/v1/auth/check`, get); + useSWR(`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/v1/auth/check`, get); const { t,