-
Notifications
You must be signed in to change notification settings - Fork 2
Add authentication to collaborative sessions #307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/collaborative-sessions
Are you sure you want to change the base?
Changes from all commits
9f0279f
7eedaa6
3e1e272
de34e45
fbc1001
1caa7e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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()}, | ||
| JWT_KEY, | ||
| algorithm="HS256", | ||
| ) | ||
|
|
||
| return JsonResponse({"token": token}) | ||
| 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) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |||||
| ) | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| async def emit_session_update(sid, session: Session): | ||||||
| payload = { | ||||||
| 'session_id': session.session_id, | ||||||
|
|
@@ -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) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| if session_id is None: | ||||||
| await sessions_server.create_session(participant) | ||||||
|
|
@@ -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) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||||||
|
|
@@ -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) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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"), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
@@ -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()), | ||
|
|
||
There was a problem hiding this comment.
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.