add login / logout logic for user

This commit is contained in:
Julien Aldon
2026-02-17 17:31:29 +01:00
parent a8c8c489da
commit aca24ca560
14 changed files with 258 additions and 108 deletions

View File

@@ -1,11 +1,12 @@
from fastapi import APIRouter, Security, HTTPException, Depends, Request
from typing import Annotated
from fastapi import APIRouter, Security, HTTPException, Depends, Request, Cookie
from fastapi.responses import RedirectResponse, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import Session, select
import jwt
from jwt import PyJWKClient
from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, settings
from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, LOGOUT_URL, settings
import src.users.service as service
from src.database import get_session
from src.models import UserCreate, User, UserPublic
@@ -20,11 +21,45 @@ router = APIRouter(prefix='/auth')
jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer()
@router.post('/logout')
def logout(response: Response):
response.delete_cookie('access_token')
response.delete_cookie('refresh_token')
return {'detail': messages.userloggedout}
@router.get('/logout')
def logout(
id_token: Annotated[str | None, Cookie()] = None,
refresh_token: Annotated[str | None, Cookie()] = None,
):
if refresh_token:
print("invalidate tokens")
requests.post(LOGOUT_URL, data={
"client_id": settings.keycloak_client_id,
"client_secret": settings.keycloak_client_secret,
"refresh_token": refresh_token
})
if id_token:
print("redirect keycloak")
response = RedirectResponse(f'{LOGOUT_URL}?post_logout_redirect_uri={settings.origins}&id_token_hint={id_token}')
else:
response = RedirectResponse(settings.origins)
print("clear cookies")
response.delete_cookie(
key='access_token',
path='/',
secure=not settings.debug,
samesite='lax',
)
response.delete_cookie(
key='refresh_token',
path='/',
secure=not settings.debug,
samesite='lax',
)
response.delete_cookie(
key='id_token',
path='/',
secure=not settings.debug,
samesite='lax',
)
return response
@router.get('/login')
@@ -64,7 +99,27 @@ def callback(code: str, session: Session = Depends(get_session)):
id_token = token_data['id_token']
decoded_token = jwt.decode(id_token, options={'verify_signature': False})
decoded_access_token = jwt.decode(token_data['access_token'], options={'verify_signature': False})
roles = decoded_access_token['resource_access'][settings.keycloak_client_id]
resource_access = decoded_access_token.get('resource_access')
if not resource_access:
data = {
'client_id': settings.keycloak_client_id,
'client_secret': settings.keycloak_client_secret,
'refresh_token': token_data['refresh_token'],
}
res = requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(settings.origins)
return resp
resource_access.get(settings.keycloak_client_id)
if not roles:
data = {
'client_id': settings.keycloak_client_id,
'client_secret': settings.keycloak_client_secret,
'refresh_token': token_data['refresh_token'],
}
res = requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(settings.origins)
return resp
user_create = UserCreate(
email=decoded_token.get('email'),
name=decoded_token.get('preferred_username'),
@@ -76,18 +131,27 @@ def callback(code: str, session: Session = Depends(get_session)):
key='access_token',
value=token_data['access_token'],
httponly=True,
secure=True if settings.debug == False else True,
samesite='strict',
secure=not settings.debug,
samesite='lax',
max_age=settings.max_age
)
response.set_cookie(
key='refresh_token',
value=token_data['refresh_token'] or '',
httponly=True,
secure=True if settings.debug == False else True,
samesite='strict',
secure=not settings.debug,
samesite='lax',
max_age=30 * 24 * settings.max_age
)
response.set_cookie(
key='id_token',
value=token_data['id_token'],
httponly=True,
secure=not settings.debug,
samesite='lax',
max_age=settings.max_age
)
return response
def verify_token(token: str):
@@ -109,12 +173,12 @@ def verify_token(token: str):
def get_current_user(request: Request, session: Session = Depends(get_session)):
access_token = request.cookies.get("access_token")
access_token = request.cookies.get('access_token')
if not access_token:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
payload = verify_token(access_token)
if not payload:
raise HTTPException(status_code=401, detail="aze")
raise HTTPException(status_code=401, detail='aze')
email = payload.get('email')
if not email:
@@ -125,16 +189,55 @@ def get_current_user(request: Request, session: Session = Depends(get_session)):
raise HTTPException(status_code=401, detail=messages.usernotfound)
return user
@router.post('/refresh')
def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
refresh = refresh_token
data = {
'grant_type': 'refresh_token',
'client_id': settings.keycloak_client_id,
'client_secret': settings.keycloak_client_secret,
'refresh_token': refresh,
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
result = requests.post(TOKEN_URL, data=data, headers=headers)
if result.status_code != 200:
raise HTTPException(
status_code=400,
detail=messages.failtogettoken
)
token_data = result.json()
response = Response()
response.set_cookie(
key='access_token',
value=token_data['access_token'],
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
max_age=settings.max_age
)
response.set_cookie(
key='refresh_token',
value=token_data['refresh_token'] or '',
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
max_age=4
)
return response
@router.get('/user/me')
def me(user: UserPublic = Depends(get_current_user)):
if not user:
return {"logged": False}
return {'logged': False}
return {
"logged": True,
"user": {
"name": user.name,
"email": user.email,
"id": user.id,
"roles": [role.name for role in user.roles]
'logged': True,
'user': {
'name': user.name,
'email': user.email,
'id': user.id,
'roles': [role.name for role in user.roles]
}
}

View File

@@ -73,7 +73,6 @@ def create_occasional_dict(contract_products: list[models.ContractProduct]):
async def create_contract(
contract: models.ContractCreate,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
new_contract = service.create_one(session, contract)
occasional_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.OCCASIONAL, new_contract.products))

View File

@@ -18,7 +18,9 @@ app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[settings.origins],
allow_origins=[
settings.origins
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

View File

@@ -5,4 +5,5 @@ invalidtoken = "Invalid token"
notauthenticated = "Not authenticated"
usernotfound = "User not found"
userloggedout = "User logged out"
failtogettoken = "Failed to get token"
failtogettoken = "Failed to get token"
unauthorized = "Unauthorized"

View File

@@ -25,3 +25,4 @@ AUTH_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protoco
TOKEN_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/token"
ISSUER = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
LOGOUT_URL = f'{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/logout'