From 2c6ca5f273791e292ddf36a298745b84845b7020 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Tue, 5 Jan 2016 22:53:52 +0000 Subject: [PATCH] Assign user to discordauthtoken for security - prevents exploit of another user entering incorrect password for email yet passing validation using existing token mentioned in #146 - does not protect against same user entering wrong password (I can live with this.) Periodic task to remove invalid token every 2 hours --- celerytask/tasks.py | 14 ++++++++- services/managers/discord_manager.py | 47 +++++++++++++++++----------- services/models.py | 8 +++-- services/views.py | 2 +- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/celerytask/tasks.py b/celerytask/tasks.py index 271ecc9d..f1c26465 100755 --- a/celerytask/tasks.py +++ b/celerytask/tasks.py @@ -9,7 +9,7 @@ from services.managers.mumble_manager import MumbleManager from services.managers.phpbb3_manager import Phpbb3Manager from services.managers.ipboard_manager import IPBoardManager from services.managers.teamspeak3_manager import Teamspeak3Manager -from services.managers.discord_manager import DiscordManager +from services.managers.discord_manager import DiscordManager, DiscordAPIManager from services.models import AuthTS from services.models import TSgroup from authentication.models import AuthServicesInfo @@ -25,6 +25,7 @@ from util.common_task import generate_corp_group_name from eveonline.models import EveCharacter from eveonline.models import EveCorporationInfo from authentication.managers import AuthServicesInfoManager +from services.models import DiscordAuthToken import logging @@ -277,6 +278,17 @@ def run_databaseUpdate(): add_to_databases(user, groups, syncgroups) remove_from_databases(user, groups, syncgroups) +# Run every 2 hours +@periodic_task(run_every=crontab(minute="0", hour="*/2")) +def run_discord_token_cleanup(): + logger.debug("Running validation of all DiscordAuthTokens") + for auth in DiscordAuthToken.objects.all(): + logger.debug("Testing DiscordAuthToken %s" % auth) + if DiscordAPIManager.validate_token(auth.token): + logger.debug("Token passes validation. Retaining %s" % auth) + else: + logger.debug("DiscordAuthToken failed validation. Deleting %s" % auth) + auth.delete() # Run every 3 hours @periodic_task(run_every=crontab(minute=0, hour="*/3")) diff --git a/services/managers/discord_manager.py b/services/managers/discord_manager.py index 64ceab86..f1310b64 100644 --- a/services/managers/discord_manager.py +++ b/services/managers/discord_manager.py @@ -13,8 +13,8 @@ DISCORD_URL = "https://discordapp.com/api" class DiscordAPIManager: - def __init__(self, server_id, email, password): - self.token = DiscordAPIManager.get_token_by_user(email, password) + def __init__(self, server_id, email, password, user=None): + self.token = DiscordAPIManager.get_token_by_user(email, password, user) self.email = email self.password = password self.server_id = server_id @@ -131,9 +131,8 @@ class DiscordAPIManager: r.raise_for_status() return r.json() - @staticmethod - def accept_invite(invite_id, token): - custom_headers = {'accept': 'application/json', 'authorization': token} + def accept_invite(self, invite_id): + custom_headers = {'accept': 'application/json', 'authorization': self.token} path = DISCORD_URL + "/invite/" + str(invite_id) r = requests.post(path, headers=custom_headers) logger.debug("Received status code %s after accepting invite." % r.status_code) @@ -223,17 +222,20 @@ class DiscordAPIManager: raise KeyError('Group not found on server: ' + group_name) @staticmethod - def get_token_by_user(email, password): + def get_token_by_user(email, password, user): if DiscordAuthToken.objects.filter(email=email).exists(): - logger.debug("Discord auth token cached for supplied email starting with %s" % email[0:3]) auth = DiscordAuthToken.objects.get(email=email) + if not auth.user == user: + raise ValueError("User mismatch while validating DiscordAuthToken for email %s - user %s, requesting user %s" % (email, auth.user, user)) + logger.debug("Discord auth token cached for supplied email starting with %s" % email[0:3]) + auth = DiscordAuthToken.objects.get(email=email, user=user) if DiscordAPIManager.validate_token(auth.token): logger.debug("Token still valid. Returning token starting with %s" % auth.token[0:5]) return auth.token else: logger.debug("Token has expired. Deleting.") auth.delete() - logger.debug("Generating auth token for email starting with %s and password of length %s" % (email[0:3], len(password))) + logger.debug("Generating auth token for email starting with %s user %s and password of length %s" % (email[0:3], user, len(password))) data = { "email" : email, "password": password, @@ -244,11 +246,19 @@ class DiscordAPIManager: logger.debug("Received status code %s after generating auth token for custom user." % r.status_code) r.raise_for_status() token = r.json()['token'] - auth = DiscordAuthToken(email=email, token=token) + auth = DiscordAuthToken(email=email, token=token, user=user) auth.save() logger.debug("Created cached token for email starting with %s" % email[0:3]) return token + def get_profile(self): + custom_headers = {'accept': 'application/json', 'authorization': self.token} + path = DISCORD_URL + "/users/@me" + r = requests.get(path, headers=custom_headers) + logger.debug("Received status code %s after retrieving user profile with email %s" % (r.status_code, self.email[0:3])) + r.raise_for_status() + return r.json() + @staticmethod def get_user_profile(email, password): token = DiscordAPIManager.get_token_by_user(email, password) @@ -371,22 +381,21 @@ class DiscordManager: return current_password @staticmethod - def add_user(email, password): + def add_user(email, password, user): try: - logger.debug("Adding new user to discord with email %s and password of length %s" % (email[0:3], len(password))) - api = DiscordAPIManager(settings.DISCORD_SERVER_ID, settings.DISCORD_USER_EMAIL, settings.DISCORD_USER_PASSWORD) - profile = DiscordAPIManager.get_user_profile(email, password) + logger.debug("Adding new user %s to discord with email %s and password of length %s" % (user, email[0:3], len(password))) + server_api = DiscordAPIManager(settings.DISCORD_SERVER_ID, settings.DISCORD_USER_EMAIL, settings.DISCORD_USER_PASSWORD) + user_api = DiscordAPIManager(settings.DISCORD_SERVER_ID, email, password, user=user) + profile = user_api.get_profile() logger.debug("Got profile for user: %s" % profile) user_id = profile['id'] logger.debug("Determined user id: %s" % user_id) - if api.check_if_user_banned(user_id): + if server_api.check_if_user_banned(user_id): logger.debug("User is currently banned. Unbanning %s" % user_id) - api.unban_user(user_id) - invite_code = api.create_invite()['code'] + server_api.unban_user(user_id) + invite_code = server_api.create_invite()['code'] logger.debug("Generated invite code beginning with %s" % invite_code[0:5]) - token = DiscordAPIManager.get_token_by_user(email, password) - logger.debug("Got auth token for supplied credentials beginning with %s" % token[0:5]) - DiscordAPIManager.accept_invite(invite_code, token) + user_api.accept_invite(invite_code) logger.info("Added user to discord server %s with id %s" % (settings.DISCORD_SERVER_ID, user_id)) return user_id except: diff --git a/services/models.py b/services/models.py index 396036f0..2cc7f017 100644 --- a/services/models.py +++ b/services/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User class TSgroup(models.Model): ts_group_id = models.IntegerField(primary_key=True) @@ -32,7 +32,9 @@ class UserTSgroup(models.Model): return self.user.name class DiscordAuthToken(models.Model): - email = models.CharField(max_length=254, primary_key=True) + email = models.CharField(max_length=254, unique=True) token = models.CharField(max_length=254) + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) def __str__(self): - return self.email + output = "Discord Token for email %s user %s" % (self.email, self.user) + return output.encode('utf-8') diff --git a/services/views.py b/services/views.py index c5d502e1..1c0e4b06 100755 --- a/services/views.py +++ b/services/views.py @@ -436,7 +436,7 @@ def activate_discord(request): password = form.cleaned_data['password'] logger.debug("Form contains password of length %s" % len(password)) try: - user_id = DiscordManager.add_user(email, password) + user_id = DiscordManager.add_user(email, password, request.user) logger.debug("Received discord uid %s" % user_id) if user_id != "": AuthServicesInfoManager.update_user_discord_info(user_id, request.user)