Implement additional security and functionality in authentication with the introduction of docstrings, type hints, and extended verification logic in the TOTP model to prevent repeated token use, improving robustness against replay attacks. Simultaneously, established the groundwork for RADIUS (Remote Authentication Dial-In User Service) support by creating models and management commands essential for handling authentication, accounting packets, and web-based authentication challenges, broadening the system's capability to integrate with network access servers and services. Resolves issues with token replay attacks and sets the stage for scalable network authentication mechanisms.
158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
from django.contrib.auth import authenticate, get_user_model
|
|
|
|
from pyrad import dictionary, packet, server
|
|
|
|
from radius.models import (
|
|
RadiusAccountingSession,
|
|
RadiusUser,
|
|
RadiusAccountingEvent,
|
|
RadiusWebAuthentication,
|
|
)
|
|
from authentication.helpers.otp import has_otp
|
|
from authentication.models import TOTPSecret
|
|
|
|
import uuid
|
|
|
|
|
|
class RadiusServer(server.Server):
|
|
def HandleAuthPacket(self, pkt):
|
|
"""Handle an authentication packet
|
|
|
|
Args:
|
|
pkt (packet.Packet): The packet to handle
|
|
"""
|
|
if state:
|
|
# Handle the challenge response
|
|
if self.validate_challenge_response(packet):
|
|
# Consider the challenge successfully passed
|
|
reply = self.CreateReplyPacket(pkt)
|
|
reply.code = packet.AccessAccept
|
|
else:
|
|
# Challenge failed
|
|
reply = self.CreateReplyPacket(pkt)
|
|
reply.code = packet.AccessReject
|
|
self.SendReplyPacket(pkt.fd, reply)
|
|
return
|
|
|
|
# No state implies this may be the first packet for authentication
|
|
# So we need to generate a challenge if necessary
|
|
|
|
# First check if the user exists
|
|
user = authenticate(username=username, password=password)
|
|
|
|
if user is None:
|
|
# User does not exist or password is incorrect
|
|
reply = self.CreateReplyPacket(pkt)
|
|
reply.code = packet.AccessReject
|
|
self.SendReplyPacket(pkt.fd, reply)
|
|
return
|
|
|
|
if user.radiususer.challenge == "none":
|
|
# No challenge is required
|
|
reply = self.CreateReplyPacket(pkt)
|
|
reply.code = packet.AccessAccept
|
|
self.SendReplyPacket(pkt.fd, reply)
|
|
return
|
|
|
|
# Generate a challenge
|
|
|
|
state = str(uuid.uuid4())
|
|
|
|
if user.radiususer.challenge == "totp":
|
|
if not has_otp(user):
|
|
# This user is not configured for TOTP...? # TODO: Make sure this is handled in the admin
|
|
reply = self.CreateReplyPacket(pkt)
|
|
reply.code = packet.AccessReject
|
|
self.SendReplyPacket(pkt.fd, reply)
|
|
return
|
|
|
|
challenge_reply = self.CreateReplyPacket(
|
|
pkt,
|
|
**{"State": state, "Reply-Message": "Please enter your TOTP token."}
|
|
)
|
|
|
|
challenge_reply.code = packet.AccessChallenge
|
|
self.SendReplyPacket(pkt.fd, challenge_reply)
|
|
return
|
|
|
|
if user.radiususer.challenge == "web":
|
|
challenge_reply = self.CreateReplyPacket(
|
|
pkt,
|
|
**{
|
|
"State": state,
|
|
"Reply-Message": "Please visit http://kumidc.local/auth to authenticate.", # TODO: Replace with a URL to our authentication page
|
|
}
|
|
)
|
|
challenge_reply.code = packet.AccessChallenge
|
|
self.SendReplyPacket(pkt.fd, challenge_reply)
|
|
return
|
|
|
|
# We should never get here. If we do, something has gone wrong.
|
|
reply = self.CreateReplyPacket(pkt)
|
|
reply.code = packet.AccessReject
|
|
self.SendReplyPacket(pkt.fd, reply)
|
|
|
|
def validate_challenge_response(self, packet, password):
|
|
state = packet["State"][0]
|
|
username = packet["User-Name"][0]
|
|
challenge_response = packet.get("User-Password", [None])[0]
|
|
|
|
user = get_user_model().objects.get(username=username)
|
|
|
|
if not user:
|
|
# This user... doesn't exist?
|
|
return False
|
|
|
|
if not user.radiususer.challenge:
|
|
# This user is not configured for challenge-response. Where did this response come from?
|
|
return False
|
|
|
|
if user.radiususer.challenge == "totp":
|
|
if not has_otp(user):
|
|
# This user is not configured for TOTP...
|
|
return False
|
|
|
|
totp = TOTPSecret.objects.get(user=user)
|
|
|
|
return totp.verify(
|
|
challenge_response
|
|
) # Returns True if the token is valid, False otherwise
|
|
|
|
if user.radiususer.challenge == "web":
|
|
ip = packet["NAS-IP-Address"][0]
|
|
auth = RadiusWebAuthentication.objects.filter(
|
|
user=user, ip=ip, expiry__gte=datetime.now()
|
|
)
|
|
auth.delete() # Delete any existing authentication sessions for this user
|
|
return True
|
|
|
|
def HandleAcctPacket(self, pkt: packet.Packet):
|
|
"""Handle an accounting packet
|
|
|
|
Args:
|
|
pkt (packet.Packet): The packet to handle
|
|
"""
|
|
status_type = pkt["Acct-Status-Type"][0]
|
|
username = pkt["User-Name"][0]
|
|
session_id = pkt["Acct-Session-Id"][0]
|
|
|
|
user = RadiusUser.objects.get(user__username=username)
|
|
|
|
session, created = RadiusAccountingSession.objects.get_or_create(
|
|
id=session_id,
|
|
defaults={
|
|
user: user,
|
|
},
|
|
)
|
|
|
|
if status_type == "Start":
|
|
session.start()
|
|
elif status_type == "Stop":
|
|
session.stop()
|
|
|
|
RadiusAccountingEvent.objects.create(
|
|
session=session, event_type=status_type, raw_data=pkt.packet
|
|
)
|
|
|
|
acct.save()
|