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.
This commit is contained in:
parent
866398111f
commit
ddd795b2f4
12 changed files with 183 additions and 290 deletions
0
src/backend/langflow/auth/__init__.py
Normal file
0
src/backend/langflow/auth/__init__.py
Normal file
72
src/backend/langflow/auth/auth.py
Normal file
72
src/backend/langflow/auth/auth.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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}]
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
0
src/backend/langflow/models/__init__.py
Normal file
0
src/backend/langflow/models/__init__.py
Normal file
10
src/backend/langflow/models/token.py
Normal file
10
src/backend/langflow/models/token.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: str | None = None
|
||||
29
src/backend/langflow/models/user.py
Normal file
29
src/backend/langflow/models/user.py
Normal file
|
|
@ -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)
|
||||
0
src/backend/langflow/routers/__init__.py
Normal file
0
src/backend/langflow/routers/__init__.py
Normal file
8
src/backend/langflow/routers/health.py
Normal file
8
src/backend/langflow/routers/health.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def get_health():
|
||||
return {"status": "OK"}
|
||||
12
src/backend/langflow/routers/items.py
Normal file
12
src/backend/langflow/routers/items.py
Normal file
|
|
@ -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}]
|
||||
35
src/backend/langflow/routers/login.py
Normal file
35
src/backend/langflow/routers/login.py
Normal file
|
|
@ -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"}
|
||||
10
src/backend/langflow/routers/users.py
Normal file
10
src/backend/langflow/routers/users.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue