From f1cbd6d26516ce30dfe40e68016dfc886fa794fc Mon Sep 17 00:00:00 2001 From: HeshamTB Date: Wed, 13 Apr 2022 05:24:06 +0300 Subject: [PATCH] jwt auth Signed-off-by: HeshamTB --- reqs.txt | 20 +++++++++++++++ run-tls | 0 sql_app/.gitignore | 2 ++ sql_app/auth_helper.py | 35 ++++++++++++++++++++++++++ sql_app/crud.py | 2 +- sql_app/crypto.py | 2 +- sql_app/gen_secret.sh | 13 ++++++++++ sql_app/main.py | 57 +++++++++++++++++++++++++++++++++++++++--- sql_app/models.py | 2 +- sql_app/schemas.py | 9 +++++++ 10 files changed, 135 insertions(+), 7 deletions(-) mode change 100644 => 100755 run-tls create mode 100644 sql_app/auth_helper.py create mode 100755 sql_app/gen_secret.sh diff --git a/reqs.txt b/reqs.txt index 519278a..7b10bc9 100644 --- a/reqs.txt +++ b/reqs.txt @@ -1,17 +1,37 @@ +aiocoap==0.4.3 anyio==3.5.0 asgiref==3.5.0 +bitlist==0.6.2 +cbor2==5.4.2.post1 +cffi==1.15.0 click==8.0.4 +cryptography==36.0.1 +Cython==0.29.28 +DTLSSocket==0.1.12 fastapi==0.74.1 +fe25519==1.1.0 +filelock==3.6.0 +fountains==1.2.0 +ge25519==1.1.0 greenlet==1.1.2 h11==0.13.0 httptools==0.3.0 idna==3.3 +LinkHeader==0.4.3 +parts==1.2.2 +pycparser==2.21 pydantic==1.9.0 +Pygments==2.11.2 +PyJWT==2.3.0 +python-decouple==3.6 python-dotenv==0.19.2 +python-multipart==0.0.5 PyYAML==6.0 +six==1.16.0 sniffio==1.2.0 SQLAlchemy==1.4.31 starlette==0.17.1 +termcolor==1.1.0 typing_extensions==4.1.1 uvicorn==0.17.5 uvloop==0.16.0 diff --git a/run-tls b/run-tls old mode 100644 new mode 100755 diff --git a/sql_app/.gitignore b/sql_app/.gitignore index c18dd8d..c7f5b33 100644 --- a/sql_app/.gitignore +++ b/sql_app/.gitignore @@ -1 +1,3 @@ __pycache__/ +.env + diff --git a/sql_app/auth_helper.py b/sql_app/auth_helper.py new file mode 100644 index 0000000..98203f6 --- /dev/null +++ b/sql_app/auth_helper.py @@ -0,0 +1,35 @@ + +from typing import Optional +from decouple import config +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from fastapi import Depends +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from . import crud, crypto, schemas +import jwt + +import time + + +JWT_SECRET = config('jwt_secret') +JWT_ALGO = config('jwt_algorithm') + + + +def create_access_token(data : dict, expires_delta : Optional[timedelta] = None): + # TODO: Consider making non-expiring token + to_encode = data.copy() # Since we may change the dict + 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, JWT_SECRET, algorithm=JWT_ALGO) + return encoded_jwt + +def authenticate_user(db: Session, email : str, password : str): + user = crud.get_user_by_email(db=db, email=email) + if not user: + return False + return crypto.verify_key(password, user.passwd_salt, user.hashed_password) + diff --git a/sql_app/crud.py b/sql_app/crud.py index 88422ce..bac5a18 100644 --- a/sql_app/crud.py +++ b/sql_app/crud.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import Session -from . import models, schemas, crypto +from . import models, schemas, crypto, auth_helper def get_user(db: Session, user_id: int): diff --git a/sql_app/crypto.py b/sql_app/crypto.py index 7fa65b5..a4fd8f5 100644 --- a/sql_app/crypto.py +++ b/sql_app/crypto.py @@ -27,4 +27,4 @@ def verify_key(plain_passwd : str, salt : bytes, stored_key : bytes) -> bool: return (stored_key == key_tmp) def calc_key(passwd: str, salt : bytes) -> bytes: - return pbkdf2_hmac(HASH_FUNC, passwd.encode(PASS_ENCODING), salt, NUM_ITIRATIONS) \ No newline at end of file + return pbkdf2_hmac(HASH_FUNC, passwd.encode(PASS_ENCODING), salt, NUM_ITIRATIONS) diff --git a/sql_app/gen_secret.sh b/sql_app/gen_secret.sh new file mode 100755 index 0000000..28de6d1 --- /dev/null +++ b/sql_app/gen_secret.sh @@ -0,0 +1,13 @@ +#!/bin/env bash + +# TODO: set a user only umask for proper premissions +# Mar 23, 2022 - H.B. + + +if [ -z $(command -v openssl) ] +then + echo "openssl not installed" + exit 1 +fi + +openssl rand -hex 32 diff --git a/sql_app/main.py b/sql_app/main.py index 1be4c5f..5327616 100644 --- a/sql_app/main.py +++ b/sql_app/main.py @@ -1,13 +1,17 @@ -from fastapi import Depends, FastAPI, HTTPException +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session -from . import crud, models, schemas +from . import crud, models, schemas, auth_helper from .database import SessionLocal, engine from typing import List - +from datetime import timedelta +import jwt models.Base.metadata.create_all(bind=engine) +#oauth2_scheme = OAuth2PasswordBearer(tokenUrl="tkn") +oauth = OAuth2PasswordBearer(tokenUrl="tkn") app = FastAPI() @@ -20,6 +24,29 @@ def get_db(): finally: db.close() +async def get_current_user(token: str = Depends(oauth), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, auth_helper.JWT_SECRET, algorithms=[auth_helper.JWT_ALGO]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = schemas.TokenData(username=username) + except jwt.PyJWTError: + raise credentials_exception + user = crud.get_user_by_username(db, username=token_data.username) + if user is None: + raise credentials_exception + return user + +async def get_current_active_user(current_user: schemas.User = Depends(get_current_user)): + if not current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user @app.post("/users/reg", response_model=schemas.User) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): @@ -55,7 +82,29 @@ def create_item_for_user( return crud.create_user_item(db=db, item=item, user_id=user_id) -@app.get("/items/", response_model=List[schemas.IotEntity]) +@app.get("/items", response_model=List[schemas.IotEntity]) def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): items = crud.get_items(db, skip=skip, limit=limit) return items + +@app.post("/tkn", response_model=schemas.Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = auth_helper.authenticate_user(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 = auth_helper.create_access_token( + data={"sub": form_data.username}, expires_delta=timedelta(minutes=15) + ) + return {"access_token": access_token, "token_type": "bearer"} + +@app.get("/users/me/", response_model=schemas.User) +async def read_users_me(current_user: schemas.User = Depends(get_current_active_user)): + return current_user + + + diff --git a/sql_app/models.py b/sql_app/models.py index 2566fbe..31985e9 100644 --- a/sql_app/models.py +++ b/sql_app/models.py @@ -10,7 +10,7 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True) username = Column(String, unique=True, index=True) - hashed_password = Column(String) + hashed_password = Column(String) # TODO: make not null passwd_salt = Column(String) is_active = Column(Boolean, default=True) diff --git a/sql_app/schemas.py b/sql_app/schemas.py index 3882895..7bbe21e 100644 --- a/sql_app/schemas.py +++ b/sql_app/schemas.py @@ -36,3 +36,12 @@ class User(UserBase): class Config: orm_mode = True + +class Token(BaseModel): + access_token : str + token_type : str + +class TokenData(BaseModel): + username : str + # Token can conatin information. But we are already recording this in a database + # for scalability.