Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"Authentication microservice, the goal of this project is to learn Python and FastApi"
__version__= "1.0.1"
__version__= "1.0.2"
70 changes: 70 additions & 0 deletions src/config/settings_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os, sys, signal
from dotenv import load_dotenv

load_dotenv()

class Settings():
DB_URL: str
DB_NAME: str = "auth-service"
DB_USERS_COLLECTION: str = "users"
DB_USERS_PICTURES_COLLECTION: str = "users.pictures"
DB_USERS_CONTACTS_COLLECTION: str = "users.contacts"
DB_USERS_MESSAGES_COLLECTION: str = "users.messages"
LOG_DB_URL: str
LOG_DATABASE_NAME: str = "auth-service-logs"
LOG_LEVEL: str = "DEBUG"
JWT_SECRET_KEY: str
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_MINUTES: int = 240
CORS_ALLOWED_HOSTS: str = ""
TOTP_SECRET: str
TOTP_DIGEST: str = "sha1"
TOTP_RETURN_DIGITS: int = 8
TOTP_TIME_STEP: int = 30
GOOGLE_OAUTH_ID: str
GOOGLE_OAUTH_CLIENT: str
GOOGLE_OAUTH_SECRET: str
GOOGLE_OAUTH_REDIRECT_RESPONSE: str = "https://127.0.0.1:8000/auth/google-response"
GOOGLE_OAUTH_JS_ORIGINS: str = "http://127.0.0.1:8000,http://localhost:8081"
GOOGLE_OAUTH_SCOPES: str = "https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/userinfo.profile,openid"
CORS_ALLOWED_HOSTS: str
TZ: str = "UTC"

def __init__(self) -> None:
try:
self.DB_URL = self.get_env("DB_URL")
self.DB_NAME = self.get_env("DB_NAME")
self.DB_USERS_COLLECTION = self.get_env("DB_USERS_COLLECTION")
self.DB_USERS_PICTURES_COLLECTION = self.get_env("DB_USERS_PICTURES_COLLECTION")
self.DB_USERS_CONTACTS_COLLECTION = self.get_env("DB_USERS_CONTACTS_COLLECTION")
self.DB_USERS_MESSAGES_COLLECTION = self.get_env("DB_USERS_MESSAGES_COLLECTION")
self.LOG_DB_URL = self.get_env("LOG_DB_URL")
self.LOG_DATABASE_NAME = self.get_env("LOG_DATABASE_NAME")
self.LOG_LEVEL = self.get_env("LOG_LEVEL")
self.JWT_SECRET_KEY = self.get_env("JWT_SECRET_KEY")
self.JWT_ALGORITHM = self.get_env("JWT_ALGORITHM")
self.JWT_EXPIRE_MINUTES = int(self.get_env("JWT_EXPIRE_MINUTES"))
self.CORS_ALLOWED_HOSTS = self.get_env("CORS_ALLOWED_HOSTS")
self.TOTP_SECRET = self.get_env("TOTP_SECRET")
self.TOTP_DIGEST = self.get_env("TOTP_DIGEST")
self.TOTP_RETURN_DIGITS = int(self.get_env("TOTP_RETURN_DIGITS"))
self.TOTP_TIME_STEP = int(self.get_env("TOTP_TIME_STEP"))
self.GOOGLE_OAUTH_ID = self.get_env("GOOGLE_OAUTH_ID")
self.GOOGLE_OAUTH_CLIENT = self.get_env("GOOGLE_OAUTH_CLIENT")
self.GOOGLE_OAUTH_SECRET = self.get_env("GOOGLE_OAUTH_SECRET")
self.GOOGLE_OAUTH_REDIRECT_RESPONSE = self.get_env("GOOGLE_OAUTH_REDIRECT_RESPONSE")
self.GOOGLE_OAUTH_JS_ORIGINS = self.get_env("GOOGLE_OAUTH_JS_ORIGINS")
self.GOOGLE_OAUTH_SCOPES = self.get_env("GOOGLE_OAUTH_SCOPES")
self.CORS_ALLOWED_HOSTS = self.get_env("CORS_ALLOWED_HOSTS")
self.TZ = self.get_env("TZ")
except Exception as e:
print("Missing or incorrect configuration value: " + e.__str__())
os.kill(os.getppid(), signal.SIGTERM)
sys.exit(78)

def get_env(self, name: str) -> str:
v = os.environ[name]
if not v or not str(v).strip():
raise ValueError(f"Variable {name} is None, empty, or just whitespace.")
else:
return v
2 changes: 1 addition & 1 deletion src/dependency_injection/containers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dependency_injector import containers, providers
from log2mongo import log2mongo

from src.settings_validator import Settings
from src.config.settings_validator import Settings
from src.services import mongodb_service
from src.services.crypto_service import CryptoService
from src.services.totp_service import TOTP
Expand Down
2 changes: 0 additions & 2 deletions src/dependencies.py → src/dependency_injection/mongo_db.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from pymongo import AsyncMongoClient
from pymongo.server_api import ServerApi
from dotenv import load_dotenv
import os

load_dotenv()
client = AsyncMongoClient(os.environ["DB_URL"], server_api= ServerApi(version='1', strict=True, deprecation_errors=True))
database = client.get_database(os.environ["DB_NAME"])

Expand Down
2 changes: 1 addition & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from src.middlewares.jwt_middleware import JWTMiddleware
from src.middlewares.http_middleware import HttpMiddleware
from src.dependency_injection.containers import Container
from src.dependencies import close_db
from src.dependency_injection.mongo_db import close_db

container = Container()
origins = container.config.d.CORS_ALLOWED_HOSTS().split(',') if container.config.d.CORS_ALLOWED_HOSTS() else []
Expand Down
4 changes: 2 additions & 2 deletions src/models/address_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from typing import Annotated, Optional
from bson import ObjectId
from pydantic import BaseModel, PlainValidator, Field
from pydantic import AliasChoices, BaseModel, PlainValidator, Field

from src.models.pydantic_objects import PyObjectId

Expand All @@ -14,7 +14,7 @@ def set_id(value):


class Address(BaseModel):
id: Annotated[Optional[PyObjectId], PlainValidator(set_id), Field(validate_default=True, serialization_alias="_id")] = Field(default=None, validation_alias="_id")
id: Annotated[Optional[PyObjectId], PlainValidator(set_id), Field(validate_default=True, serialization_alias="_id")] = Field(default=None, validation_alias=AliasChoices("id", "_id"))
country: str
state: str
colony: str
Expand Down
2 changes: 1 addition & 1 deletion src/models/user_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from typing import Annotated, List, Optional
from bson import ObjectId
from pydantic import AfterValidator, BaseModel, BeforeValidator, ConfigDict, Field, field_validator
from pydantic import BaseModel, BeforeValidator, Field, field_validator

from src.models.address_model import Address
from src.models.pydantic_objects import PyObjectId
Expand Down
2 changes: 1 addition & 1 deletion src/routers/admin/users_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from src.models.user_model import User
from src.models.address_model import Address
from src.services.user_service import change_password, get_address, insert_address
from src.dependencies import get_db
from src.dependency_injection.mongo_db import get_db
from src.middlewares.auth_roles_jwt import JWTCustom
from src.dependency_injection.containers import Container
import src.services.user_service as uSvc
Expand Down
2 changes: 1 addition & 1 deletion src/routers/products_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from src.custom_json import MJSONEncoder
from src.models.product_model import Product
from src.middlewares.auth_jwt import JWTCustom
from src.dependencies import get_db
from src.dependency_injection.mongo_db import get_db

oauth2_scheme = JWTCustom(tokenUrl="/auth/sign-in")
router = APIRouter(tags=["products"])
Expand Down
14 changes: 7 additions & 7 deletions src/routers/users_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from src.services.jwt_service import verify_token_from_requests
from src.services.totp_service import TOTP
import src.services.user_service as uSvc
from src.dependencies import get_db
from src.dependency_injection.mongo_db import get_db

oauth2_scheme = JWTCustom(tokenUrl="/auth/sign-in")
router = APIRouter(
Expand All @@ -24,7 +24,7 @@
totp_dependency = Annotated[TOTP, Depends(Provide[Container.totp])]

# Route to add an users
@router.get("/user")
@router.get("/user", response_model_by_alias = False, response_model = User)
@inject
async def get_user(db: db_dependency, email: Annotated[str, Depends(verify_token_from_requests)]):
user = await uSvc.get_user(email, db.get_db())
Expand All @@ -33,7 +33,7 @@ async def get_user(db: db_dependency, email: Annotated[str, Depends(verify_token
else:
return Response(status_code = status.HTTP_404_NOT_FOUND)

@router.post("/user/img", response_model_by_alias = False)
@router.post("/user/img")
@inject
async def add_user_image(file: UploadFile, db: db_dependency, email: Annotated[str, Depends(verify_token_from_requests)]):
result = await uSvc.add_user_picture(email, db.get_db(), file = file, content_type = file.content_type)
Expand All @@ -42,7 +42,7 @@ async def add_user_image(file: UploadFile, db: db_dependency, email: Annotated[s
else:
return Response(status_code= status.HTTP_400_BAD_REQUEST)

@router.get("/user/img", response_model_by_alias = False)
@router.get("/user/img")
@inject
async def get_user_image(db: db_dependency, email: Annotated[str, Depends(verify_token_from_requests)]):
result = await uSvc.get_user_picture(email, db.get_db())
Expand All @@ -51,7 +51,7 @@ async def get_user_image(db: db_dependency, email: Annotated[str, Depends(verify
else:
return Response(status_code= status.HTTP_400_BAD_REQUEST)

@router.put("/user")
@router.put("/user", response_model = User, response_model_by_alias = False)
@inject
async def update_user(db: db_dependency, model: User, email: Annotated[str, Depends(oauth2_scheme)]):
user = await uSvc.update_user(db.get_db(), model)
Expand Down Expand Up @@ -85,7 +85,7 @@ async def verify_2f_code(code: str, email: Annotated[str, Depends(oauth2_scheme)
else:
return Response(status_code = status.HTTP_401_UNAUTHORIZED)

@router.post("/user/address")
@router.post("/user/address", response_model_by_alias = False, response_model = Address)
@inject
async def create_address(db: db_dependency, model: Address, email: Annotated[str, Depends(oauth2_scheme)]):
address = await insert_address(email, model, db.get_db())
Expand All @@ -94,7 +94,7 @@ async def create_address(db: db_dependency, model: Address, email: Annotated[str
else:
return Response(status_code=status.HTTP_400_BAD_REQUEST)

@router.get("/user/address")
@router.get("/user/address", response_model_by_alias = False, response_model = list[Address])
@inject
async def get_addresses(db: db_dependency, email: Annotated[str, Depends(oauth2_scheme)]):
addresses = await get_address(db.get_db(), email)
Expand Down
14 changes: 9 additions & 5 deletions src/services/jwt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@

crypto_service: CryptoService = Provide[Container.crypto_service]
log_service: log2mongo = Provide[Container.logging]
config = Container.config.d()

def get_jwt_expire_time() -> timedelta:
# Solving DI framework problem, executes later, when the route is called
minutes = int(Container.config.d()["JWT_EXPIRE_MINUTES"])
return timedelta(minutes=minutes)

@inject
async def create_token(data: dict, expire_time: timedelta = timedelta(minutes=int(config["JWT_EXPIRE_MINUTES"])), crypto = crypto_service, log = log_service):
async def create_token(data: dict, expire_time: timedelta = get_jwt_expire_time(), crypto = crypto_service, log = log_service):
try:
for item in data:
if isinstance(data[item], list):
Expand All @@ -25,7 +29,7 @@ async def create_token(data: dict, expire_time: timedelta = timedelta(minutes=in

expire = datetime.now(timezone.utc) + expire_time
data.update({ "exp": expire })
encode_jwt = jwt.encode(data, str(config["JWT_SECRET_KEY"]), algorithm= config["JWT_ALGORITHM"])
encode_jwt = jwt.encode(data, Container.config.d()["JWT_SECRET_KEY"], algorithm= Container.config.d()["JWT_ALGORITHM"])
return encode_jwt
except Exception as e:
log.logger.error(e)
Expand Down Expand Up @@ -73,7 +77,7 @@ async def verify_token_from_requests(request: Request):
@inject
async def verify(request_token: str, log = log_service):
try:
payload = jwt.decode(request_token, str(config["JWT_SECRET_KEY"]), config["JWT_ALGORITHM"])
payload = jwt.decode(request_token, Container.config.d()["JWT_SECRET_KEY"], Container.config.d()["JWT_ALGORITHM"])
return payload
except jwt.ExpiredSignatureError as e:
log.logger.error(e)
Expand All @@ -88,7 +92,7 @@ async def verify(request_token: str, log = log_service):
@inject
async def get_email(token: str, crypto = crypto_service, log = log_service):
try:
payload = jwt.decode(token, str(config["JWT_SECRET_KEY"]), config["JWT_ALGORITHM"])
payload = jwt.decode(token, Container.config.d()["JWT_SECRET_KEY"], Container.config.d()["JWT_ALGORITHM"])
return await crypto.decrypt_text(payload.get("sub"))
except Exception as e:
log.logger.error(e)
Expand Down
28 changes: 20 additions & 8 deletions src/services/oauth_google_service.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import google.oauth2.credentials
from log2mongo import log2mongo
import google_auth_oauthlib.flow
from dependency_injector.wiring import Provide, inject

from src.dependency_injection.containers import Container

log_service: log2mongo = Provide[Container.logging]
config = Container.config.d()

@inject
async def get_auth_url(log = log_service):
try:
oauth_client = Container.config.d()["GOOGLE_OAUTH_CLIENT"]
oauth_id = Container.config.d()["GOOGLE_OAUTH_ID"]
oauth_js_origins = Container.config.d()["GOOGLE_OAUTH_JS_ORIGINS"]
oauth_secret = Container.config.d()["GOOGLE_OAUTH_SECRET"]
oauth_scopes = Container.config.d()["GOOGLE_OAUTH_SCOPES"]
oauth_redirect_response = Container.config.d()["GOOGLE_OAUTH_REDIRECT_RESPONSE"]

flow = google_auth_oauthlib.flow.Flow.from_client_config(
{"web":{"client_id":config["GOOGLE_OAUTH_CLIENT"],"project_id":config["GOOGLE_OAUTH_ID"],"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":config["GOOGLE_OAUTH_SECRET"],"javascript_origins":config["GOOGLE_OAUTH_JS_ORIGINS"].split(',') if config["GOOGLE_OAUTH_JS_ORIGINS"] else []}},
scopes = config["GOOGLE_OAUTH_SCOPES"].split(',') if config["GOOGLE_OAUTH_SCOPES"] else []
{"web":{"client_id":oauth_client,"project_id":oauth_id,"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":oauth_secret,"javascript_origins":oauth_js_origins.split(',') if oauth_js_origins else []}},
scopes = oauth_scopes.split(',') if oauth_scopes else []
)
flow.redirect_uri = config["GOOGLE_OAUTH_REDIRECT_RESPONSE"]
flow.redirect_uri = oauth_redirect_response

auth_url , state = flow.authorization_url(
acces_type = 'offline',
Expand All @@ -32,11 +37,18 @@ async def get_auth_url(log = log_service):
async def get_auth_response(url: str, log = log_service):
try:
credentials = None
oauth_client = Container.config.d()["GOOGLE_OAUTH_CLIENT"]
oauth_id = Container.config.d()["GOOGLE_OAUTH_ID"]
oauth_js_origins = Container.config.d()["GOOGLE_OAUTH_JS_ORIGINS"]
oauth_secret = Container.config.d()["GOOGLE_OAUTH_SECRET"]
oauth_scopes = Container.config.d()["GOOGLE_OAUTH_SCOPES"]
oauth_redirect_response = Container.config.d()["GOOGLE_OAUTH_REDIRECT_RESPONSE"]

flow = google_auth_oauthlib.flow.Flow.from_client_config(
{"web":{"client_id":config["GOOGLE_OAUTH_CLIENT"],"project_id":config["GOOGLE_OAUTH_ID"],"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":config["GOOGLE_OAUTH_SECRET"],"javascript_origins":config["GOOGLE_OAUTH_JS_ORIGINS"].split(',') if config["GOOGLE_OAUTH_JS_ORIGINS"] else []}},
scopes = config["GOOGLE_OAUTH_SCOPES"].split(',') if config["GOOGLE_OAUTH_SCOPES"] else []
{"web":{"client_id":oauth_client,"project_id":oauth_id,"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":oauth_secret,"javascript_origins":oauth_js_origins.split(',') if oauth_js_origins else []}},
scopes = oauth_scopes.split(',') if oauth_scopes else []
)
flow.redirect_uri = config["GOOGLE_OAUTH_REDIRECT_RESPONSE"]
flow.redirect_uri = oauth_redirect_response

auth_reponse = flow.fetch_token(authorization_response = url)
credentials = flow.credentials
Expand Down
2 changes: 1 addition & 1 deletion src/services/totp_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
import time
from typing import Optional, Union
from src.services.otp_service import OTP
import time
from log2mongo import log2mongo

class TOTP(OTP):
Expand Down
Loading