Skip to content

Commit

Permalink
fix: wrong behaviour in email auth
Browse files Browse the repository at this point in the history
  • Loading branch information
TanookiVerde committed Dec 5, 2024
1 parent 44b606e commit 73344dc
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 58 deletions.
7 changes: 7 additions & 0 deletions app/auth/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# -*- coding: utf-8 -*-

from fastapi import APIRouter
from app.auth.routers.basic import router as auth_basic
from app.auth.routers.email import router as auth_email
from app.auth.routers.totp import router as auth_totp


router = APIRouter(prefix="/auth", tags=["Autenticação"])

router.include_router(auth_basic)
router.include_router(auth_email)
router.include_router(auth_totp)
14 changes: 7 additions & 7 deletions app/auth/routers/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
from typing import Annotated

from fastapi import Depends
from fastapi import APIRouter
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import JSONResponse
from loguru import logger

from app import config
from app.auth.routers import router
from app.types.pydantic_models import Token
from app.utils import authenticate_user, generate_user_token
from app.enums import LoginStatusEnum
from app.types.errors import (
AuthenticationErrorModel
)
from app.types import Token
from app.auth.utils import authenticate_user, generate_user_token
from app.auth.enums import LoginStatusEnum
from app.auth.types import AuthenticationErrorModel


router = APIRouter(prefix="/basic")

@router.post(
"/token",
Expand Down
37 changes: 21 additions & 16 deletions app/auth/routers/email.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
from fastapi import HTTPException
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from loguru import logger

from app import config
from app.types import Token
from app.auth.enums import LoginStatusEnum
from app.types.pydantic_models import Token
from app.auth.types import AuthenticationErrorModel
from app.auth.routers import router
from app.auth.types import LoginForm, LoginFormWith2FA
from app.auth.utils import (
authenticate_user,
Expand All @@ -21,25 +20,31 @@
)


router = APIRouter(prefix="/email")


@router.post(
"/2fa/email/generate-code/",
"/generate-code/",
response_model=dict,
responses={
401: {"model": AuthenticationErrorModel}
}
)
async def generate_2fa_code(
async def gen_2fa_code(
login: LoginForm
):
result = await authenticate_user(login.username, login.password)

if result['status'] != LoginStatusEnum.SUCCESS:
if result['status'] not in [
LoginStatusEnum.SUCCESS,
LoginStatusEnum.REQUIRE_2FA
]:
raise JSONResponse(
status_code=401,
content=AuthenticationErrorModel(
message="Something went wrong",
type=result['status']
),
content={
"message": "Something went wrong",
"type": result['status']
},
)

code = generate_2fa_code()
Expand All @@ -52,12 +57,12 @@ async def generate_2fa_code(
success = False

if not success:
raise JSONResponse(
return JSONResponse(
status_code=401,
content=AuthenticationErrorModel(
message="Error during the email queueing process. Try Again.",
type=LoginStatusEnum.EMAIL_QUEUE_ERROR
),
content={
"message": "Error during the email queueing process. Try Again.",
"type": LoginStatusEnum.EMAIL_QUEUE_ERROR
},
)

return {
Expand All @@ -66,7 +71,7 @@ async def generate_2fa_code(


@router.post(
"/2fa/email/login/",
"/login/",
response_model=Token,
responses={
401: {"model": AuthenticationErrorModel}
Expand Down
21 changes: 10 additions & 11 deletions app/auth/routers/totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@
import io

from fastapi import HTTPException, status
from fastapi import APIRouter
from fastapi.responses import StreamingResponse,JSONResponse
from loguru import logger

from app import config
from app.types.frontend import LoginFormWith2FA, LoginForm
from app.types.pydantic_models import Token
from app.security import TwoFactorAuth
from app.enums import LoginStatusEnum
from app.types.errors import (
AuthenticationErrorModel
)
from app.auth.routers import router
from app.types import Token
from app.auth.types import LoginForm, LoginFormWith2FA
from app.auth.enums import LoginStatusEnum
from app.auth.types import AuthenticationErrorModel
from app.auth.utils import authenticate_user, generate_user_token
from app.auth.utils.totp import validate_code


router = APIRouter(prefix="/totp")

@router.post(
"/2fa/totp/is-2fa-active/",
"/is-2fa-active/",
response_model=bool,
responses={
401: {"model": AuthenticationErrorModel}
Expand Down Expand Up @@ -48,7 +47,7 @@ async def is_2fa_active(


@router.post(
"/2fa/totp/login/",
"/login/",
response_model=Token,
responses={
401: {"model": AuthenticationErrorModel}
Expand Down Expand Up @@ -86,7 +85,7 @@ async def login_with_2fa(


@router.post(
"/2fa/totp/generate-qrcode/",
"/generate-qrcode/",
response_model=bytes,
responses={
400: {"model": str},
Expand Down
30 changes: 14 additions & 16 deletions app/auth/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional, Tuple
from typing import Optional, Tuple, Callable
import jwt

from app import config
Expand Down Expand Up @@ -33,7 +33,7 @@ def generate_user_token(user: User) -> str:
async def authenticate_user(
username: str,
password: str,
verificator, # Async Callable[User, str]
verificator: Callable[[User, str], bool] = None,
code: Optional[str] = None,
) -> Tuple[User, bool, str]:
user = await User.get_or_none(username=username).prefetch_related("role")
Expand Down Expand Up @@ -62,25 +62,23 @@ async def authenticate_user(
}

# 2FA TOTP SENT
if user.is_2fa_required and not code:
if (not code) or (not verificator):
return {
"user": user,
"status": LoginStatusEnum.REQUIRE_2FA,
}

# 2FA TOTP VALIDATION
if user.is_2fa_required and code:
is_2fa_valid = await verificator(user, code)
if not is_2fa_valid:
return {
"user": user,
"status": LoginStatusEnum.BAD_OTP,
}

return {
"user": user,
"status": LoginStatusEnum.SUCCESS,
}
is_2fa_valid = await verificator(user, code)
if not is_2fa_valid:
return {
"user": user,
"status": LoginStatusEnum.BAD_OTP,
}
else:
return {
"user": user,
"status": LoginStatusEnum.SUCCESS,
}


def create_access_token(data: dict, expires_delta: timedelta | None = None):
Expand Down
9 changes: 5 additions & 4 deletions app/auth/utils/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def store_code(user: User, code: str, ttl: int = 300):
async def validate_code(user: User, code: str):
stored_code = redis_client.get(f"2fa:{user.id}")

if store_code is None:
if stored_code is None:
return False

if stored_code.decode() == code:
Expand All @@ -48,11 +48,12 @@ def generate_2fa_code():
async def send_2fa_email_to_user(user: User, code: str):
logger.info(f"Sending 2FA code {code} to {user.email}")
response = requests.post(
url=DATARELAY_URL,
url=f"{DATARELAY_URL}data/mailman",
headers={
'x-api-key': DATARELAY_MAILMAN_TOKEN
'x-api-key': DATARELAY_MAILMAN_TOKEN,
'Content-Type': 'application/json'
},
body={
json={
"to_addresses": [user.email],
"cc_addresses": [],
"bcc_addresses": [],
Expand Down
1 change: 1 addition & 0 deletions app/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID", action="raise"
)
BIGQUERY_ERGON_TABLE_ID = getenv_or_action("BIGQUERY_ERGON_TABLE_ID", action="raise")
BIGQUERY_PATIENT_SEARCH_TABLE_ID = getenv_or_action("BIGQUERY_PATIENT_SEARCH_TABLE_ID", action="raise")

# JWT configuration
JWT_SECRET_KEY = getenv_or_action("JWT_SECRET_KEY", default=token_bytes(32).hex())
Expand Down
6 changes: 3 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from loguru import logger

from app import config
from app.utils import prepare_gcp_credential
from app.lifespan import api_lifespan
from app.routers import entities_raw, frontend, misc
from app.auth import routers as auth
from app.utils import prepare_gcp_credential
from app.auth.routers import router as auth_routers

logger.remove()
logger.add(sys.stdout, level=config.LOG_LEVEL)
Expand Down Expand Up @@ -47,7 +47,7 @@
allow_credentials=config.ALLOW_CREDENTIALS,
)

app.include_router(auth_routers)
app.include_router(entities_raw.router)
app.include_router(auth.router)
app.include_router(frontend.router)
app.include_router(misc.router)
51 changes: 51 additions & 0 deletions app/routers/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from app.config import (
BIGQUERY_PROJECT,
BIGQUERY_PATIENT_HEADER_TABLE_ID,
BIGQUERY_PATIENT_SEARCH_TABLE_ID,
BIGQUERY_PATIENT_SUMMARY_TABLE_ID,
BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID,
REQUEST_LIMIT_MAX,
Expand Down Expand Up @@ -88,6 +89,56 @@ async def accept_use_terms(
)


@router_request(
method="GET",
router=router,
path="/patient/search",
response_model=List[dict],
responses={
404: {"model": AccessErrorModel},
403: {"model": AccessErrorModel}
},
dependencies=[Depends(RateLimiter(times=REQUEST_LIMIT_MAX, seconds=REQUEST_LIMIT_WINDOW_SIZE))]
)
async def search_patient(
request: Request,
user: Annotated[User, Depends(assert_user_is_active)],
cpf: str = None,
cns: str = None,
name: str = None,
) -> List[dict]:
if sum([bool(cpf), bool(cns), bool(name)]) != 1:
return JSONResponse(
status_code=400,
content={"message": "Only one of the parameters is allowed"},
)

validation_job = validate_user_access_to_patient_data(user, cpf)

clause = ""
if cns:
clause = f"cns = {cns}"
elif cpf:
clause = f"cpf_particao = {cpf}"
elif name:
clause = f"nome_completo = '{name}'"

results_job = read_bq(
f"""
SELECT nome_completo, cns, cpf
FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_SEARCH_TABLE_ID}
WHERE {clause}
""",
from_file="/tmp/credentials.json",
)
validation, results = await asyncio.gather(validation_job, results_job)

has_access, response = validation
if not has_access:
return response

return results.to_dict(orient="records")

@router_request(
method="GET",
router=router,
Expand Down
2 changes: 1 addition & 1 deletion app/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pyotp import TOTP

from app.models import User
from app.types.pydantic_models import User2FA as UserPydantic
from app.auth.types.totp import User2FA as UserPydantic


class TwoFactorAuth:
Expand Down

0 comments on commit 73344dc

Please sign in to comment.