Skip to content

Commit 428939b

Browse files
committed
refactor: OAuth and JWT based authentication
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
1 parent 15de231 commit 428939b

File tree

4 files changed

+155
-57
lines changed

4 files changed

+155
-57
lines changed

src/labs/routers/auth/__init__.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77
"""
88
from datetime import datetime
99
from uuid import UUID
10-
from fastapi import APIRouter, Request, Depends, HTTPException
10+
11+
from fastapi import APIRouter, Request, Depends,\
12+
HTTPException, status
13+
from fastapi.security import OAuth2PasswordRequestForm
1114
from sqlalchemy.ext.asyncio import AsyncSession
1215

1316

1417
from ...db import get_async_session
1518
from ...models import User
16-
from ...schema import UserRequest,\
17-
PasswordLoginRequest, AuthResponse
19+
from ...schema import UserRequest, UserResponse, Token
20+
from ...utils.auth import create_access_token
21+
from ..utils import get_current_user
1822

1923
from .create import router as router_account_create
2024
from .otp import router as router_otp
@@ -25,33 +29,41 @@
2529
router.include_router(router_account_create)
2630
router.include_router(router_otp, prefix="/otp")
2731

28-
@router.post("/login",
29-
summary=""" Provides an endpoint for login via email and password
30-
""",
31-
response_model=AuthResponse,
32+
@router.post(
33+
"/token",
34+
summary="Provides an endpoint for login via email and password",
35+
response_model=Token,
3236
)
33-
async def login_user(request: PasswordLoginRequest,
34-
session: AsyncSession = Depends(get_async_session)):
37+
async def login_for_auth_token(
38+
form_data: OAuth2PasswordRequestForm = Depends(),
39+
session: AsyncSession = Depends(get_async_session)
40+
):
3541
""" Attempt to authenticate a user and issue JWT token
3642
3743
"""
38-
user = await User.get_by_email(session, request.username)
39-
40-
if user is None:
41-
raise HTTPException(status_code=401, detail="Failed to authenticate user")
42-
43-
access_token = Authorize.create_access_token(subject=user.email,fresh=True)
44-
refresh_token = Authorize.create_refresh_token(subject=user.email)
45-
46-
return AuthResponse(access_token=access_token,
47-
refresh_token=refresh_token,
48-
token_type="Bearer",
49-
expires_in=100)
50-
44+
user = await User.get_by_email(session, form_data.username)
45+
46+
if user is None or not user.check_password(form_data.password):
47+
raise HTTPException(
48+
status_code=status.HTTP_401_UNAUTHORIZED,
49+
detail="Incorrect username or password",
50+
headers={"WWW-Authenticate": "Bearer"},
51+
)
52+
53+
access_token = create_access_token(
54+
subject=user.email,
55+
fresh=True
56+
)
57+
58+
return Token(
59+
access_token=access_token,
60+
token_type="bearer"
61+
)
5162

52-
@router.post("/refresh",
63+
@router.post(
64+
"/refresh",
5365
summary=""" Provides an endpoint for refreshing the JWT token""",
54-
response_model=AuthResponse,
66+
response_model=Token,
5567
)
5668
async def refresh_jwt_token(request: Request,
5769
session: AsyncSession = Depends(get_async_session)):
@@ -60,7 +72,8 @@ async def refresh_jwt_token(request: Request,
6072
return {}
6173

6274

63-
@router.post("/logout",
75+
@router.post(
76+
"/logout",
6477
summary=""" Provides an endpoint for logging out the user""",
6578
)
6679
async def logout_user(session: AsyncSession = Depends(get_async_session)):
@@ -69,25 +82,17 @@ async def logout_user(session: AsyncSession = Depends(get_async_session)):
6982
"""
7083
return {}
7184

72-
@router.get("/me", response_model=UserRequest)
73-
async def get_me(request: Request,
74-
session: AsyncSession = Depends(get_async_session)
85+
@router.get(
86+
"/me",
87+
response_model=UserResponse,
88+
)
89+
async def get_me(
90+
current_user: User = Depends(get_current_user)
7591
):
7692
"""Get the currently logged in user or myself
7793
7894
This endpoint will return the currently logged in user or raise
7995
and exception if the user is not logged in.
8096
"""
81-
model = UserRequest(
82-
id = UUID('{12345678-1234-5678-1234-567812345678}'),
83-
first_name="Dev",
84-
last_name="Mukherjee",
85-
86-
mobile_phone="042-1234567",
87-
verified=True,
88-
created_at=datetime.now(),
89-
updated_at=datetime.now()
90-
)
91-
92-
return model
97+
return current_user
9398

src/labs/routers/utils.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,62 @@
88

99
from sqlalchemy.ext.asyncio import AsyncSession
1010
from fastapi import Depends, HTTPException
11+
from fastapi.security import OAuth2PasswordBearer
12+
import jwt
1113

14+
from ..config import config
1215
from ..db import get_async_session
16+
from ..models import User
17+
from ..schema import TokenData
1318

14-
async def get_current_user(session:
15-
AsyncSession = Depends(get_async_session),
19+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
20+
21+
async def get_current_user(
22+
token: str = Depends(oauth2_scheme),
23+
session: AsyncSession = Depends(get_async_session),
1624
):
1725
"""
1826
"""
19-
return {}
27+
# Reused a few times around the lifecycle
28+
credentials_exception = HTTPException(
29+
status_code=status.HTTP_401_UNAUTHORIZED,
30+
detail="Could not validate credentials",
31+
headers={"WWW-Authenticate": "Bearer"},
32+
)
33+
34+
try:
35+
payload = jwt.decode(
36+
token,
37+
config.JWT_SECRET_KEY,
38+
algorithm=config.JWT_ALGORITHM
39+
)
40+
41+
username: str = payload.get("sub")
42+
43+
if username is None:
44+
raise credentials_exception
45+
46+
token_data = TokenData(username=username)
47+
48+
except:
49+
raise credentials_exception
2050

21-
if not user:
22-
raise HTTPException(status_code=404, detail="User not found")
51+
user = await User.get_by_email(session, token_data.username)
2352

24-
return user
53+
if user is None:
54+
raise credentials_exception
55+
56+
return user
57+
58+
59+
async def get_current_active_user(
60+
current_user: User = Depends(get_current_user)
61+
):
62+
"""
63+
"""
64+
if current_user.verified:
65+
raise HTTPException(
66+
status_code=status.HTTP_400_BAD_REQUEST,
67+
detail="Inactive user"
68+
)
69+
return current_user

src/labs/schema/auth.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1+
from pydantic import BaseModel
12
from .utils import AppBaseModel
2-
class PasswordLoginRequest(AppBaseModel):
3-
""" Requires parameters to login via password
3+
4+
class Token(BaseModel):
5+
""" A model that represents a JWT token
6+
7+
48
"""
5-
username: str
6-
password: str
9+
access_token: str
10+
token_type: str
11+
12+
class TokenData(BaseModel):
13+
""" A model that represents the data in a JWT token
14+
15+
Literally used to validate if what we have unpacked
16+
is a valid token.
17+
18+
"""
19+
username: str = None
20+
721

822
class SignupRequest(AppBaseModel):
23+
""" A simple request to sign up a user with an email and password
24+
"""
925
password: str
1026
email: str
1127

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

30-
class AuthResponse(AppBaseModel):
31-
"""Response from the authentication endpoint
32-
"""
33-
access_token: str
34-
refresh_token: str
35-
token_type: str
36-
expires_in: int

src/labs/utils/auth.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
1313
"""
1414

15+
from datetime import datetime, timedelta
16+
1517
from passlib.context import CryptContext
18+
import jwt
19+
20+
from ..config import config
1621

1722
# Password hashing and validation helpers
1823

@@ -41,3 +46,37 @@ def hash_password(password) -> str:
4146
"""
4247
return _pwd_context.hash(password)
4348

49+
50+
def create_access_token(
51+
subject: str,
52+
fresh: bool = False
53+
) -> str:
54+
""" Creates a JWT token for the user
55+
56+
This is used by the authentication handler to create
57+
a JWT token for the user to use for subsequent requests.
58+
59+
Args:
60+
subject (str): The subject of the token, usually the email
61+
expires_delta (int, optional): The number of seconds the token
62+
should be valid for. Defaults to None.
63+
fresh (bool, optional): Whether the token is fresh or not.
64+
Defaults to False.
65+
66+
Returns:
67+
str: The encoded JWT token
68+
"""
69+
delta = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
70+
to_encode = {
71+
"sub": subject,
72+
"fresh": fresh,
73+
"exp": datetime.utcnow() + delta
74+
}
75+
76+
encoded_jwt = jwt.encode(
77+
to_encode,
78+
config.JWT_SECRET_KEY.get_secret_value(),
79+
algorithm=config.JWT_ALGORITHM
80+
)
81+
82+
return encoded_jwt

0 commit comments

Comments
 (0)