Skip to content

Commit

Permalink
refactor: OAuth and JWT based authentication
Browse files Browse the repository at this point in the history
a reference implementation of password based OAuth login and JWT sessions working
this commit has the get_current_user not working properly.

note that this does not use pyjose but pyjwt instead REFS #52
  • Loading branch information
devraj committed Nov 20, 2022
1 parent 15de231 commit 428939b
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 57 deletions.
85 changes: 45 additions & 40 deletions src/labs/routers/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
"""
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Request, Depends, HTTPException

from fastapi import APIRouter, Request, Depends,\
HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession


from ...db import get_async_session
from ...models import User
from ...schema import UserRequest,\
PasswordLoginRequest, AuthResponse
from ...schema import UserRequest, UserResponse, Token
from ...utils.auth import create_access_token
from ..utils import get_current_user

from .create import router as router_account_create
from .otp import router as router_otp
Expand All @@ -25,33 +29,41 @@
router.include_router(router_account_create)
router.include_router(router_otp, prefix="/otp")

@router.post("/login",
summary=""" Provides an endpoint for login via email and password
""",
response_model=AuthResponse,
@router.post(
"/token",
summary="Provides an endpoint for login via email and password",
response_model=Token,
)
async def login_user(request: PasswordLoginRequest,
session: AsyncSession = Depends(get_async_session)):
async def login_for_auth_token(
form_data: OAuth2PasswordRequestForm = Depends(),
session: AsyncSession = Depends(get_async_session)
):
""" Attempt to authenticate a user and issue JWT token
"""
user = await User.get_by_email(session, request.username)

if user is None:
raise HTTPException(status_code=401, detail="Failed to authenticate user")

access_token = Authorize.create_access_token(subject=user.email,fresh=True)
refresh_token = Authorize.create_refresh_token(subject=user.email)

return AuthResponse(access_token=access_token,
refresh_token=refresh_token,
token_type="Bearer",
expires_in=100)

user = await User.get_by_email(session, form_data.username)

if user is None or not user.check_password(form_data.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)

access_token = create_access_token(
subject=user.email,
fresh=True
)

return Token(
access_token=access_token,
token_type="bearer"
)

@router.post("/refresh",
@router.post(
"/refresh",
summary=""" Provides an endpoint for refreshing the JWT token""",
response_model=AuthResponse,
response_model=Token,
)
async def refresh_jwt_token(request: Request,
session: AsyncSession = Depends(get_async_session)):
Expand All @@ -60,7 +72,8 @@ async def refresh_jwt_token(request: Request,
return {}


@router.post("/logout",
@router.post(
"/logout",
summary=""" Provides an endpoint for logging out the user""",
)
async def logout_user(session: AsyncSession = Depends(get_async_session)):
Expand All @@ -69,25 +82,17 @@ async def logout_user(session: AsyncSession = Depends(get_async_session)):
"""
return {}

@router.get("/me", response_model=UserRequest)
async def get_me(request: Request,
session: AsyncSession = Depends(get_async_session)
@router.get(
"/me",
response_model=UserResponse,
)
async def get_me(
current_user: User = Depends(get_current_user)
):
"""Get the currently logged in user or myself
This endpoint will return the currently logged in user or raise
and exception if the user is not logged in.
"""
model = UserRequest(
id = UUID('{12345678-1234-5678-1234-567812345678}'),
first_name="Dev",
last_name="Mukherjee",
email="[email protected]",
mobile_phone="042-1234567",
verified=True,
created_at=datetime.now(),
updated_at=datetime.now()
)

return model
return current_user

57 changes: 51 additions & 6 deletions src/labs/routers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,62 @@

from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
import jwt

from ..config import config
from ..db import get_async_session
from ..models import User
from ..schema import TokenData

async def get_current_user(session:
AsyncSession = Depends(get_async_session),
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_async_session),
):
"""
"""
return {}
# Reused a few times around the lifecycle
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)

try:
payload = jwt.decode(
token,
config.JWT_SECRET_KEY,
algorithm=config.JWT_ALGORITHM
)

username: str = payload.get("sub")

if username is None:
raise credentials_exception

token_data = TokenData(username=username)

except:
raise credentials_exception

if not user:
raise HTTPException(status_code=404, detail="User not found")
user = await User.get_by_email(session, token_data.username)

return user
if user is None:
raise credentials_exception

return user


async def get_current_active_user(
current_user: User = Depends(get_current_user)
):
"""
"""
if current_user.verified:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return current_user
31 changes: 20 additions & 11 deletions src/labs/schema/auth.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
from pydantic import BaseModel
from .utils import AppBaseModel
class PasswordLoginRequest(AppBaseModel):
""" Requires parameters to login via password

class Token(BaseModel):
""" A model that represents a JWT token
"""
username: str
password: str
access_token: str
token_type: str

class TokenData(BaseModel):
""" A model that represents the data in a JWT token
Literally used to validate if what we have unpacked
is a valid token.
"""
username: str = None


class SignupRequest(AppBaseModel):
""" A simple request to sign up a user with an email and password
"""
password: str
email: str

Expand All @@ -27,10 +43,3 @@ class OTPTriggerResponse(AppBaseModel):
""" OTP Verification result """
success: bool

class AuthResponse(AppBaseModel):
"""Response from the authentication endpoint
"""
access_token: str
refresh_token: str
token_type: str
expires_in: int
39 changes: 39 additions & 0 deletions src/labs/utils/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
"""

from datetime import datetime, timedelta

from passlib.context import CryptContext
import jwt

from ..config import config

# Password hashing and validation helpers

Expand Down Expand Up @@ -41,3 +46,37 @@ def hash_password(password) -> str:
"""
return _pwd_context.hash(password)


def create_access_token(
subject: str,
fresh: bool = False
) -> str:
""" Creates a JWT token for the user
This is used by the authentication handler to create
a JWT token for the user to use for subsequent requests.
Args:
subject (str): The subject of the token, usually the email
expires_delta (int, optional): The number of seconds the token
should be valid for. Defaults to None.
fresh (bool, optional): Whether the token is fresh or not.
Defaults to False.
Returns:
str: The encoded JWT token
"""
delta = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {
"sub": subject,
"fresh": fresh,
"exp": datetime.utcnow() + delta
}

encoded_jwt = jwt.encode(
to_encode,
config.JWT_SECRET_KEY.get_secret_value(),
algorithm=config.JWT_ALGORITHM
)

return encoded_jwt

0 comments on commit 428939b

Please sign in to comment.