diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..e2fb864 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,74 @@ +import os +import json + +from fastapi import APIRouter, Depends, HTTPException, status, Header +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from passlib.context import CryptContext as PasswordContext +from cryptography.fernet import Fernet, InvalidToken, InvalidSignature + +from models import User + +AUTH_ERROR = HTTPException(status.HTTP_400_BAD_REQUEST) + +password_context = PasswordContext(schemes=["bcrypt"], deprecated="auto") + +SECRET_KEY = os.environ["SECRET_KEY"] +crypto_context = Fernet(SECRET_KEY) + +users_db = { + "kai": { + "username": "kai", + "hashed_password": "$2b$12$mh5hSniE.1SqxK3IDDTIO.1jDKgU0KX2eZev3yFu4Z1ZUPXWMw2Xa", + } +} + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") + + +class UserInDb(User): + hashed_password: str + + +def get_current_user(token: str = Depends(oauth2_scheme)): + try: + token_data = json.loads(crypto_context.decrypt(token.encode())) + except (InvalidToken, InvalidSignature): + raise AUTH_ERROR + return User(username=token_data["username"]) + + +auth_router = APIRouter() + + +class TokenLoginResponse(BaseModel): + access_token: str + token_type: str + + +@auth_router.post( + "/login", + tags=["auth"], + responses={ + status.HTTP_400_BAD_REQUEST: {}, + status.HTTP_200_OK: { + "model": TokenLoginResponse, + }, + }, +) +async def login(form_data: OAuth2PasswordRequestForm = Depends()): + + if form_data.username not in users_db: + raise AUTH_ERROR + + user = UserInDb(**users_db[form_data.username]) + + if not password_context.verify(form_data.password, user.hashed_password): + raise AUTH_ERROR + + token = crypto_context.encrypt(json.dumps({"username": user.username}).encode()) + + return { + "access_token": token, + "token_type": "bearer", + } diff --git a/backend/main.py b/backend/main.py index 5a2ee50..3d7d658 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,9 @@ -from fastapi import FastAPI, Response -from pydantic import BaseSettings - +from fastapi import FastAPI, Depends, Response from fastapi.staticfiles import StaticFiles +from pydantic import BaseSettings, BaseModel + +from models import User +from auth import get_current_user, auth_router class Settings(BaseSettings): @@ -11,6 +13,19 @@ class Settings(BaseSettings): settings = Settings() app = FastAPI() + +app.include_router( + auth_router, + prefix="/auth", + tags=["auth"] +) + + +@app.get("/test") +async def read_test(user = Depends(get_current_user)): + return {"name": user.username, "foo": "bar"} + + if settings.dev_mode: import httpx diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..4ae707c --- /dev/null +++ b/backend/models.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class User(BaseModel): + username: str \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 38a4a7d..57bfc8e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,16 +1,22 @@ anyio==3.4.0 asgiref==3.4.1 certifi==2021.10.8 +cffi==1.15.0 charset-normalizer==2.0.9 click==8.0.3 +cryptography==36.0.1 fastapi==0.70.1 h11==0.12.0 httpcore==0.14.3 httpx==0.21.1 idna==3.3 +passlib==1.7.4 +pycparser==2.21 pydantic==1.8.2 +python-multipart==0.0.5 rfc3986==1.5.0 +six==1.16.0 sniffio==1.2.0 starlette==0.16.0 typing-extensions==4.0.1 -uvicorn==0.16.0 +uvicorn==0.15.0 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 891155e..7401d35 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,17 @@ -import React from 'react'; +import { useState } from 'react'; +import Login from './Login'; +import MainView from './MainView'; + export const App: React.FC = () => { - return ( -
+ Token: {data.name} +
++ Foo: {data.foo} +
+ > + } +} + +export default MainView; \ No newline at end of file