Files
amap/backend/src/auth/auth.py

268 lines
7.7 KiB
Python

import secrets
from typing import Annotated
from urllib.parse import urlencode
import jwt
import requests
import src.messages as messages
import src.users.service as service
from fastapi import (APIRouter, Cookie, Depends, HTTPException, Request,
Security)
from fastapi.responses import RedirectResponse, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import PyJWKClient
from sqlmodel import Session, select
from src.database import get_session
from src.models import User, UserCreate, UserPublic
from src.settings import (AUTH_URL, ISSUER, JWKS_URL, LOGOUT_URL, TOKEN_URL,
settings)
router = APIRouter(prefix='/auth')
jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer()
@router.get('/logout')
def logout():
params = {
'client_id': settings.keycloak_client_id,
'post_logout_redirect_uri': settings.origins,
}
response = RedirectResponse(f'{LOGOUT_URL}?{urlencode(params)}')
response.delete_cookie(
key='access_token',
path='/',
secure=not settings.debug,
samesite='strict',
)
response.delete_cookie(
key='refresh_token',
path='/',
secure=not settings.debug,
samesite='strict',
)
response.delete_cookie(
key='id_token',
path='/',
secure=not settings.debug,
samesite='strict',
)
return response
@router.get('/login')
def login():
state = secrets.token_urlsafe(16)
params = {
'client_id': settings.keycloak_client_id,
'response_type': 'code',
'scope': 'openid',
'redirect_uri': settings.keycloak_redirect_uri,
'state': state,
}
request_url = requests.Request(
'GET', AUTH_URL, params=params).prepare().url
return RedirectResponse(request_url)
@router.get('/callback')
def callback(code: str, session: Session = Depends(get_session)):
data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': settings.keycloak_redirect_uri,
'client_id': settings.keycloak_client_id,
'client_secret': settings.keycloak_client_secret,
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
response = requests.post(TOKEN_URL, data=data, headers=headers)
if response.status_code != 200:
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('token')
)
token_data = response.json()
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})
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'],
}
requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
return resp
roles = 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'],
}
requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
return resp
user_create = UserCreate(
email=decoded_token.get('email'),
name=decoded_token.get('name'),
role_names=roles['roles']
)
service.get_or_create_user(session, user_create)
response = RedirectResponse(settings.origins)
response.set_cookie(
key='access_token',
value=token_data['access_token'],
httponly=True,
secure=not settings.debug,
samesite='strict',
max_age=settings.max_age
)
response.set_cookie(
key='refresh_token',
value=token_data['refresh_token'] or '',
httponly=True,
secure=not settings.debug,
samesite='strict',
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='strict',
max_age=settings.max_age
)
return response
def verify_token(token: str):
try:
signing_key = jwk_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience=settings.keycloak_client_id,
issuer=ISSUER,
leeway=60,
)
return decoded
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=401,
detail=messages.Messages.tokenexipired
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=401,
detail=messages.Messages.invalidtoken
)
def get_current_user(
request: Request,
session: Session = Depends(get_session)):
access_token = request.cookies.get('access_token')
if not access_token:
raise HTTPException(
status_code=401,
detail=messages.Messages.notauthenticated
)
payload = verify_token(access_token)
if not payload:
raise HTTPException(
status_code=401,
detail='aze'
)
email = payload.get('email')
if not email:
raise HTTPException(
status_code=401,
detail=messages.Messages.notauthenticated
)
user = session.exec(select(User).where(User.email == email)).first()
if not user:
raise HTTPException(
status_code=401,
detail=messages.Messages.not_found('user')
)
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=404,
detail=messages.Messages.not_found('token')
)
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='strict',
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',
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='strict',
max_age=settings.max_age
)
return response
@router.get('/user/me')
def me(user: UserPublic = Depends(get_current_user)):
if not user:
return {'logged': False}
return {
'logged': True,
'user': {
'name': user.name,
'email': user.email,
'id': user.id,
'roles': user.roles
}
}