Added new features

This commit is contained in:
Ivan Nikolskiy 2025-07-31 22:19:17 +02:00
parent f59a7cd3b0
commit a75d135589
4 changed files with 91 additions and 11 deletions

52
django_hopid/auth.py Normal file
View File

@ -0,0 +1,52 @@
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from django.http import HttpRequest
from typing import Optional, Tuple
from .utils import (
verify_jwt_token,
get_user_from_jwt_claims,
refresh_access_token
)
class HopIDAuthentication(BaseAuthentication):
def authenticate(self, request: HttpRequest) -> Optional[Tuple[object, str]]:
refresh_token = None
is_session_based = False
auth_header = request.headers.get('Authorization', '')
if auth_header.lower().startswith('bearer '):
access_token = auth_header.split(' ', 1)[1].strip()
else:
access_token = request.session.get('access_token')
refresh_token = request.session.get('refresh_token')
is_session_based = True
user = None
if access_token:
try:
claims = verify_jwt_token(access_token)
user = get_user_from_jwt_claims(claims, access_token)
except Exception:
user = None
# Try refreshing session-based tokens
if not user and is_session_based and refresh_token:
tokens = refresh_access_token(refresh_token)
if tokens:
access_token = tokens.get('access_token')
refresh_token = tokens.get('refresh_token')
request.session['access_token'] = access_token
request.session['refresh_token'] = refresh_token
try:
claims = verify_jwt_token(access_token)
user = get_user_from_jwt_claims(claims, access_token)
except Exception:
user = None
if not user:
raise AuthenticationFailed('Invalid or expired token.')
return user, access_token

View File

@ -53,7 +53,7 @@ def hopid_protected(view_func):
user = None user = None
if access_token: if access_token:
claims = verify_jwt_token(access_token) claims = verify_jwt_token(access_token)
user = get_user_from_jwt_claims(claims) user = get_user_from_jwt_claims(claims, access_token)
if not user and is_session_based and refresh_token: if not user and is_session_based and refresh_token:
tokens = refresh_access_token(refresh_token) tokens = refresh_access_token(refresh_token)
@ -65,7 +65,7 @@ def hopid_protected(view_func):
request.session['refresh_token'] = refresh_token request.session['refresh_token'] = refresh_token
claims = verify_jwt_token(access_token) claims = verify_jwt_token(access_token)
user = get_user_from_jwt_claims(claims) user = get_user_from_jwt_claims(claims, access_token)
if not user: if not user:
return JsonResponse({'detail': 'Invalid or expired token.'}, status=401) return JsonResponse({'detail': 'Invalid or expired token.'}, status=401)
@ -104,7 +104,7 @@ def hopid_callback(response=None):
return fail("Invalid or missing nonce") return fail("Invalid or missing nonce")
access_claims = verify_jwt_token(access_token) access_claims = verify_jwt_token(access_token)
profile = get_user_from_jwt_claims(access_claims) profile = get_user_from_jwt_claims(access_claims, access_token)
if not profile: if not profile:
return fail("Invalid access token") return fail("Invalid access token")

View File

@ -54,16 +54,18 @@ def verify_jwt_token(token):
token, token,
key=public_key, key=public_key,
algorithms=["RS256"], algorithms=["RS256"],
audience=settings.HOPID_CLIENT_ID, #audience=settings.HOPID_CLIENT_ID,
issuer=settings.HOPID_URL, issuer=settings.HOPID_URL,
options={"verify_aud": False} # BAD PRACTICE / DELETE
) )
return claims return claims
except (InvalidTokenError, ExpiredSignatureError, requests.RequestException, KeyError): except (InvalidTokenError, ExpiredSignatureError, requests.RequestException, KeyError) as e:
print(e)
return None return None
def get_user_from_jwt_claims(claims): def get_user_from_jwt_claims(claims, access_token):
if not claims: if not claims:
return None return None
@ -73,10 +75,35 @@ def get_user_from_jwt_claims(claims):
if not username or not email: if not username or not email:
return None return None
user, _ = get_user_model().objects.get_or_create( User = get_user_model()
email=email, user = User.objects.filter(email=email).first()
defaults={"username": username.lower()}
) if user:
return user
if not access_token:
return None
try:
response = requests.get(
f"{settings.HOPID_URL}/users/userinfo/",
headers={"Authorization": f"Bearer {access_token}"},
timeout=5
)
if response.status_code != 200:
return None
userinfo = response.json()
except requests.RequestException:
return None
model_fields = {f.name for f in User._meta.get_fields()}
safe_defaults = {
k: v for k, v in userinfo.items()
if k in model_fields and k not in {'id', 'pk', 'password', 'email', 'username'}
}
safe_defaults.setdefault('username', username.lower())
user, _ = User.objects.get_or_create(email=email, defaults=safe_defaults)
return user return user

View File

@ -37,7 +37,8 @@ setup(
'requests>=2.20', 'requests>=2.20',
'Django>=4.2', 'Django>=4.2',
'python-jose', 'python-jose',
'requests' 'requests',
'PyJWT'
], ],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',