Skip to content
Open
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 django/university/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class CustomOIDCAuthentationBackend(OIDCAuthenticationBackend):

def create_user(self, claims):
User = get_user_model()

if User.objects.filter(username=claims.get('nmec', '')).exists():
user = User.objects.get(username=claims.get('nmec', ''))
return self.update_user(user, claims)
Expand Down
24 changes: 24 additions & 0 deletions django/university/routes/auth/SocketToken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.http import JsonResponse
from django.views import View
import jwt
import datetime

from tts_be.settings import JWT_KEY, DEBUG


class SocketToken(View):
def get(self, request):
if not request.user.is_authenticated:
return JsonResponse({"error": "Not authenticated"}, status=401)

expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
hours=12
)

token = jwt.encode(
{"username": request.user.username, "exp": expiration.timestamp()},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datetime.timestamp() returns a float and from what I looked up the JWT exp claim should be an integer and some libraries will reject a float.

JWT_KEY,
algorithm="HS256",
)

return JsonResponse({"token": token})
11 changes: 6 additions & 5 deletions django/university/socket/participant.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import uuid

class Participant:
def __init__(self, sid, name, selected_slots):
def __init__(self, sid, name, selected_slots, nmec=None):
self.sid = sid
self.client_id = str(uuid.uuid4())
self.name = name
self.selected_slots = selected_slots or []

self.nmec = nmec

def to_json(self):
return {
'client_id': self.client_id,
'name': self.name,
'selected_slots': self.selected_slots,
}

@staticmethod
def from_json(sid, data):
if 'name' not in data:
raise ValueError('name is required')
return Participant(sid, data['name'], data.get('selected_slots', []))
return Participant(sid, data['name'], data.get('selected_slots', []), data.get('nmec'))

def update_from_json(self, data):
if 'name' in data:
self.name = data['name']
Expand Down
54 changes: 30 additions & 24 deletions django/university/socket/sessionsserver.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,87 @@
from typing import Coroutine
from typing import Coroutine, Optional
from random import randbytes
import socketio
from socketio.exceptions import ConnectionRefusedError
import jwt
from django.conf import settings

from university.socket.session import Session
from university.socket.participant import Participant


class SessionsServer:
def __init__(self, sio: socketio.AsyncServer):
self.sio = sio
self.sessions = {}
self.clients = {}

def valid_token(self, token: str) -> bool:
return True

def validate_token(self, token: str) -> Optional[str]:
try:
payload = jwt.decode(token, settings.JWT_KEY, algorithms=["HS256"])
return payload.get("username")
except jwt.PyJWTError:
return None

def event(self, event):
return self.sio.event(event)

def emit(self, event, data, to):
return self.sio.emit(event, data, to=to)

def emit_to_session(self, event, data, session_id, sid):
return self.sio.emit(event, data, room=session_id, skip_sid=sid)

def generate_session_id(self):
while True:
session_id = randbytes(4).hex().upper()
if session_id not in self.sessions:
return session_id
def create_session(self, participant: Participant) -> Coroutine:

def create_session(self, participant: Participant) -> Coroutine:
if self.get_client_session(participant.sid):
raise ConnectionRefusedError('Client is already in a session')
raise ConnectionRefusedError("Client is already in a session")

session_id = self.generate_session_id()
result = self.sio.enter_room(participant.sid, session_id)

self.sessions.setdefault(session_id, Session(session_id))
self.sessions[session_id].add_client(participant)
self.clients[participant.sid] = session_id

return result



def enter_session(self, participant: Participant, session_id: str) -> Coroutine:
if self.get_client_session(participant.sid):
raise ConnectionRefusedError('Client is already in a session')

if session_id not in self.sessions:
print(self.sessions)
raise ConnectionRefusedError('Session is empty')

self.sessions[session_id].add_client(participant)
result = self.sio.enter_room(participant.sid, session_id)

self.clients[participant.sid] = session_id

return result

def leave_session(self, sid: str) -> Coroutine:
session_id = self.clients.get(sid)
if session_id is None:
raise ConnectionRefusedError('Client is not in a session')

result = self.sio.leave_room(sid, session_id)

self.sessions[session_id].remove_client(sid)
if self.sessions[session_id].no_participants() or self.sessions[session_id].expired():
del self.sessions[session_id]

del self.clients[sid]

return result

def get_client_session(self, sid) -> Session | None:
return self.sessions.get(self.clients.get(sid))

def get_session(self, session_id) -> Session | None:
return self.sessions.get(session_id)
21 changes: 12 additions & 9 deletions django/university/socket/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)
)


async def emit_session_update(sid, session: Session):
payload = {
'session_id': session.session_id,
Expand All @@ -27,16 +28,18 @@ async def emit_session_update(sid, session: Session):
async def connect(sid, environ, auth):
if auth is None or 'token' not in auth:
raise ConnectionRefusedError('Authentication failed: No token provided')
if not sessions_server.valid_token(auth['token']):

username = sessions_server.validate_token(auth['token'])
if not username:
raise ConnectionRefusedError('Authentication failed: Invalid token')

print(f'Participant {sid} connected')
print(f'Participant {sid} connected (username: {username})')

query_params = parse_qs(environ.get("QUERY_STRING", ""))
query_params = parse_qs(environ.get('QUERY_STRING', ''))
session_id = query_params.get('session_id', [None])[0]

participant_name = query_params.get('participant_name', ["Anonymous"])[0]
participant = Participant(sid, participant_name)
participant_name = query_params.get('participant_name', ['Anonymous'])[0]
participant = Participant(sid, participant_name, username)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

username is passed as the 3rd arg, which maps to selected_slots in the constructor signature (sid, name, selected_slots, nmec=None). So nmec ends up None for every participant.

Suggested change
participant = Participant(sid, participant_name, username)
participant = Participant(sid, participant_name, [], username)


if session_id is None:
await sessions_server.create_session(participant)
Expand All @@ -61,16 +64,16 @@ async def connect(sid, environ, auth):
await sessions_server.emit('connected', payload, to=sid)
await emit_session_update(sid, session)


@sessions_server.event
async def disconnect(sid):
session = cast(Session, sessions_server.get_client_session(sid))

await sessions_server.leave_session(sid)
print(f'Client {sid} disconnected')

if sessions_server.get_session(session.session_id) is not None:

await emit_session_update(sid, session)
await emit_session_update(sid, session)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check why did we remove if sessions_server.get_session(session.session_id) is not None ? Was it redundant ?



@sessions_server.event
async def update_participant(sid, updated_participant):
Expand All @@ -87,4 +90,4 @@ async def update_schedule(sid, selected_slots):

participant.selected_slots = selected_slots

await emit_session_update(sid, user_session)
await emit_session_update(sid, user_session)
4 changes: 3 additions & 1 deletion django/university/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from university.routes.exchange.AdminMarketplaceView import AdminMarketplaceView
from university.routes.MarketplaceExchangeView import MarketplaceExchangeView
from university.routes.auth.Csrf import Csrf
from university.routes.auth.SocketToken import SocketToken
from university.routes.exchange.DirectExchangeView import DirectExchangeView
from university.routes.exchange.export.ExchangeExportView import ExchangeExportView
from university.routes.exchange.options.ExchangeOptionsView import ExchangeOptionsView
Expand Down Expand Up @@ -55,6 +56,7 @@
path('professors/<int:schedule>/', views.professor),
path('info/', views.info),
path('auth/info/', InfoView.as_view()),
path("auth/socket-token/", SocketToken.as_view()),
path('csrf/', Csrf.as_view()),
path('admin/courses/', AdminExchangeCoursesView.as_view()),
path('student/schedule', StudentScheduleView.as_view(), name="auth-student-schedule"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add in the is_authenticated decorator since after the security issues its better to guard everything

Expand All @@ -81,7 +83,7 @@
path('class/<int:course_unit_id>/', views.classes),
path('professors/<int:slot>/', views.professor),
path('course_unit/hash', views.get_course_unit_hashes),
path('course_unit/enrollment/', CourseUnitEnrollmentView.as_view()),
path('course_unit/enrollment/', CourseUnitEnrollmentView.as_view()),
path('oidc-auth/', include('mozilla_django_oidc.urls')),
path('exchange/admin/courses/', exchange_admin_required(AdminExchangeCoursesView.as_view())),
path('exchange/admin/course_units/', AdminExchangeCourseUnitsView.as_view()),
Expand Down