From ddd795b2f4667b1cc06b3b8499752252018322d7 Mon Sep 17 00:00:00 2001 From: gustavoschaedler Date: Mon, 19 Jun 2023 23:50:19 +0100 Subject: [PATCH] Add authentication and authorization functionality The commit adds the auth module and updates dependencies. It includes authentication and authorization functionality and models to create access tokens. The endpoints for login and users/me have been removed and moved to the appropriate modules. These changes have improved security and code organization. --- src/backend/langflow/auth/__init__.py | 0 src/backend/langflow/auth/auth.py | 72 +++++++++++ src/backend/langflow/login.py | 143 --------------------- src/backend/langflow/main.py | 154 ++--------------------- src/backend/langflow/models/__init__.py | 0 src/backend/langflow/models/token.py | 10 ++ src/backend/langflow/models/user.py | 29 +++++ src/backend/langflow/routers/__init__.py | 0 src/backend/langflow/routers/health.py | 8 ++ src/backend/langflow/routers/items.py | 12 ++ src/backend/langflow/routers/login.py | 35 ++++++ src/backend/langflow/routers/users.py | 10 ++ 12 files changed, 183 insertions(+), 290 deletions(-) create mode 100644 src/backend/langflow/auth/__init__.py create mode 100644 src/backend/langflow/auth/auth.py delete mode 100644 src/backend/langflow/login.py create mode 100644 src/backend/langflow/models/__init__.py create mode 100644 src/backend/langflow/models/token.py create mode 100644 src/backend/langflow/models/user.py create mode 100644 src/backend/langflow/routers/__init__.py create mode 100644 src/backend/langflow/routers/health.py create mode 100644 src/backend/langflow/routers/items.py create mode 100644 src/backend/langflow/routers/login.py create mode 100644 src/backend/langflow/routers/users.py diff --git a/src/backend/langflow/auth/__init__.py b/src/backend/langflow/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/auth/auth.py b/src/backend/langflow/auth/auth.py new file mode 100644 index 000000000..503b2bd5b --- /dev/null +++ b/src/backend/langflow/auth/auth.py @@ -0,0 +1,72 @@ +from typing import Annotated + +from fastapi import Depends, HTTPException, status +from passlib.context import CryptContext +from jose import JWTError, jwt +from datetime import datetime, timedelta, timezone +from fastapi.security import OAuth2PasswordBearer +from ..models.token import TokenData +from ..models.user import get_user, fake_users_db, User + + +SECRET_KEY = "your_secret_key" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def create_access_token(data: dict, expires_delta: timedelta = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode["exp"] = expire + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user diff --git a/src/backend/langflow/login.py b/src/backend/langflow/login.py deleted file mode 100644 index c2782be56..000000000 --- a/src/backend/langflow/login.py +++ /dev/null @@ -1,143 +0,0 @@ -from datetime import datetime, timedelta -from typing import Annotated - -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from jose import JWTError, jwt -from passlib.context import CryptContext -from pydantic import BaseModel - -# to get a string like this run: -# openssl rand -hex 32 -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - - -fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", - "disabled": False, - } -} - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: str | None = None - - -class User(BaseModel): - username: str - email: str | None = None - full_name: str | None = None - disabled: bool | None = None - - -class UserInDB(User): - hashed_password: str - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return pwd_context.hash(password) - - -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) - - -def authenticate_user(fake_db, username: str, password: str): - user = get_user(fake_db, username) - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: timedelta | None = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - - -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - if username is None: - raise credentials_exception - token_data = TokenData(username=username) - except JWTError: - raise credentials_exception - user = get_user(fake_users_db, username=token_data.username) - if user is None: - raise credentials_exception - return user - - -async def get_current_active_user( - current_user: Annotated[User, Depends(get_current_user)] -): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -@app.post("/token", response_model=Token) -async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): - user = authenticate_user(fake_users_db, form_data.username, form_data.password) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.username}, expires_delta=access_token_expires - ) - return {"access_token": access_token, "token_type": "bearer"} - - -@app.get("/users/me/", response_model=User) -async def read_users_me( - current_user: Annotated[User, Depends(get_current_active_user)] -): - return current_user - - -@app.get("/users/me/items/") -async def read_own_items( - current_user: Annotated[User, Depends(get_current_active_user)] -): - return [{"item_id": "Foo", "owner": current_user.username}] \ No newline at end of file diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 78ac1e75f..d546fd58e 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -2,160 +2,15 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from langflow.api import router +from langflow.routers import login, users, items, health from langflow.database.base import create_db_and_tables -from datetime import datetime, timedelta -from typing import Annotated - -from fastapi import Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from jose import JWTError, jwt -from passlib.context import CryptContext -from pydantic import BaseModel - -# to get a string like this run: -# openssl rand -hex 32 -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - - -fake_users_db = { - "johndoe": { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", - "disabled": False, - } -} - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: str | None = None - - -class User(BaseModel): - username: str - email: str | None = None - full_name: str | None = None - disabled: bool | None = None - - -class UserInDB(User): - hashed_password: str - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return pwd_context.hash(password) - - -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) - - -def authenticate_user(fake_db, username: str, password: str): - user = get_user(fake_db, username) - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -def create_access_token(data: dict, expires_delta: timedelta | None = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - - -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - if username is None: - raise credentials_exception - token_data = TokenData(username=username) - except JWTError: - raise credentials_exception - user = get_user(fake_users_db, username=token_data.username) - if user is None: - raise credentials_exception - return user - - -async def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)]): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - def create_app(): """Create the FastAPI app and include the router.""" app = FastAPI() - origins = [ - "*", - ] - - @app.post("/token", response_model=Token) - async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): - user = authenticate_user(fake_users_db, form_data.username, form_data.password) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.username}, - expires_delta=access_token_expires - ) - return {"access_token": access_token, "token_type": "bearer"} - - - @app.get("/users/me/", response_model=User) - async def read_users_me( - current_user: Annotated[User, Depends(get_current_active_user)] - ): - return current_user - - - @app.get("/users/me/items/") - async def read_own_items( - current_user: Annotated[User, Depends(get_current_active_user)] - ): - return [{"item_id": "Foo", "owner": current_user.username}] - - @app.get("/health") - def get_health(): - return {"status": "OK"} + origins = ["*"] app.add_middleware( CORSMiddleware, @@ -165,7 +20,12 @@ def create_app(): allow_headers=["*"], ) + app.include_router(login.router) + app.include_router(users.router) + app.include_router(items.router) + app.include_router(health.router) app.include_router(router) + app.on_event("startup")(create_db_and_tables) return app diff --git a/src/backend/langflow/models/__init__.py b/src/backend/langflow/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/models/token.py b/src/backend/langflow/models/token.py new file mode 100644 index 000000000..080286787 --- /dev/null +++ b/src/backend/langflow/models/token.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None diff --git a/src/backend/langflow/models/user.py b/src/backend/langflow/models/user.py new file mode 100644 index 000000000..1023a6a65 --- /dev/null +++ b/src/backend/langflow/models/user.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) diff --git a/src/backend/langflow/routers/__init__.py b/src/backend/langflow/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/routers/health.py b/src/backend/langflow/routers/health.py new file mode 100644 index 000000000..244ef001d --- /dev/null +++ b/src/backend/langflow/routers/health.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/health") +def get_health(): + return {"status": "OK"} diff --git a/src/backend/langflow/routers/items.py b/src/backend/langflow/routers/items.py new file mode 100644 index 000000000..e6d21340e --- /dev/null +++ b/src/backend/langflow/routers/items.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, Depends +from ..models.user import User +from ..auth.auth import get_current_active_user + +router = APIRouter() + + +@router.get("/users/me/items/") +async def read_own_items( + current_user: User = Depends(get_current_active_user) +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/src/backend/langflow/routers/login.py b/src/backend/langflow/routers/login.py new file mode 100644 index 000000000..eac2d57bb --- /dev/null +++ b/src/backend/langflow/routers/login.py @@ -0,0 +1,35 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from langflow.models.token import Token +from langflow.models.user import fake_users_db +from datetime import timedelta +from langflow.auth.auth import ( + ACCESS_TOKEN_EXPIRE_MINUTES, + authenticate_user, + create_access_token +) + +router = APIRouter() + + +@router.post("/token", response_model=Token) +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +): + user = authenticate_user( + fake_users_db, + form_data.username, + form_data.password + ) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, + expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} diff --git a/src/backend/langflow/routers/users.py b/src/backend/langflow/routers/users.py new file mode 100644 index 000000000..1a9184ec8 --- /dev/null +++ b/src/backend/langflow/routers/users.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter, Depends +from langflow.models.user import User +from langflow.auth.auth import get_current_active_user + +router = APIRouter() + + +@router.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user