Skip to content

Commit

Permalink
feat: single authentication per device
Browse files Browse the repository at this point in the history
  • Loading branch information
flavien-hugs committed Jan 8, 2025
1 parent 3f6bd86 commit 3432257
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[settings]
known_third_party = beanie,email_validator,fastapi,fastapi_cache,fastapi_jwt,fastapi_pagination,httpx,jinja2,jose,mongomock_motor,pwdlib,pydantic,pydantic_settings,pymongo,pyotp,pytest,pytest_asyncio,slugify,starlette,typer,uvicorn,yaml
known_third_party = beanie,email_validator,fastapi,fastapi_cache,fastapi_jwt,fastapi_pagination,getmac,httpx,jinja2,jose,mongomock_motor,pwdlib,pydantic,pydantic_settings,pymongo,pyotp,pytest,pytest_asyncio,slugify,starlette,typer,uvicorn,yaml
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ user-agents = "^2.2.0"
python-multipart = "^0.0.12"
cachetools = "^5.5.0"
python-jose = "^3.3.0"
getmac = "0.9.5"


[tool.poetry.group.test.dependencies]
Expand Down
47 changes: 43 additions & 4 deletions src/services/auth/auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from datetime import datetime, timezone
from datetime import datetime, timezone, UTC
from typing import Optional

from beanie import PydanticObjectId
from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from getmac import get_mac_address
from starlette.responses import JSONResponse

from src.common.helpers.caching import delete_custom_key
Expand Down Expand Up @@ -62,15 +63,47 @@ async def login(request: Request, payload: LoginUser) -> JSONResponse:
)

role = await get_one_role(role_id=PydanticObjectId(user.role))

# Récupérer l'adresse IP et le device_id
address_ip = request.client.host
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
address_ip = forwarded_for.split(",")[0]

device_id = (
get_mac_address(ip=address_ip)
or get_mac_address(interface="eth0")
or get_mac_address(ip=address_ip, network_request=True)
)

# Vérifier l'authentification unique par appareil
if user.attributes["device_id"] and user.attributes["device_id"] != device_id:
raise CustomHTTPException(
code_error=AuthErrorCode.AUTH_ALREADY_LOGGED_IN,
message_error="You are already logged in on another device.",
status_code=status.HTTP_403_FORBIDDEN,
)

# Mettre à jour les informations de l'utilisateur
current_time = datetime.now(tz=UTC)
update_data = {"last_login": current_time, "address_ip": address_ip, "device_id": device_id}
await user.set({"attributes": {**user.attributes, **update_data}, "updated_at": current_time})

user_data = user.model_dump(
by_alias=True,
mode="json",
exclude={"password", "attributes.otp_secret", "attributes.otp_created_at", "is_primary"},
exclude={
"password",
"attributes.otp_secret",
"attributes.otp_created_at",
"is_primary",
},
)
user_data.update(
{"role": role.model_dump(by_alias=True, mode="json", exclude={"permissions", "created_at", "updated_at"})}
)

# Générer les tokens
response_data = {
"access_token": CustomAccessBearer.access_token(data=jsonable_encoder(user_data), user_id=str(user.id)),
"referesh_token": CustomAccessBearer.refresh_token(data=jsonable_encoder(user_data), user_id=str(user.id)),
Expand All @@ -82,8 +115,14 @@ async def login(request: Request, payload: LoginUser) -> JSONResponse:

async def logout(request: Request) -> JSONResponse:
authorization = request.headers.get("Authorization")
token = authorization.split()[1]
await blacklist_token.add_blacklist_token(token)
token = authorization.split()[1] if authorization else None

decode_token = CustomAccessBearer.decode_access_token(token=token)
user_id = decode_token.get("subject", {}).get("_id")
user = await get_one_user(user_id=PydanticObjectId(user_id))
await user.set({"attributes": {**user.attributes, "device_id": None}})

await blacklist_token.add_blacklist_token(token=token)

await delete_custom_key(settings.APP_NAME + "access")
await delete_custom_key(settings.APP_NAME + "validate")
Expand Down
1 change: 1 addition & 0 deletions src/shared/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AuthErrorCode(StrEnum):
AUTH_INSUFFICIENT_PERMISSION = "auth/insufficient-permission"
AUTH_OTP_NOT_VALID = "auth/otp-not-valid"
AUTH_OTP_EXPIRED = "auth/otp-code-expired"
AUTH_ALREADY_LOGGED_IN = "auth/already-logged-in-another-device"


class UserErrorCode(StrEnum):
Expand Down

0 comments on commit 3432257

Please sign in to comment.