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
This commit is contained in:
Adarnof 2016-01-05 22:53:52 +00:00
parent b5b13e828a
commit 2c6ca5f273
4 changed files with 47 additions and 24 deletions

View File

@ -9,7 +9,7 @@ from services.managers.mumble_manager import MumbleManager
from services.managers.phpbb3_manager import Phpbb3Manager from services.managers.phpbb3_manager import Phpbb3Manager
from services.managers.ipboard_manager import IPBoardManager from services.managers.ipboard_manager import IPBoardManager
from services.managers.teamspeak3_manager import Teamspeak3Manager 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 AuthTS
from services.models import TSgroup from services.models import TSgroup
from authentication.models import AuthServicesInfo 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 EveCharacter
from eveonline.models import EveCorporationInfo from eveonline.models import EveCorporationInfo
from authentication.managers import AuthServicesInfoManager from authentication.managers import AuthServicesInfoManager
from services.models import DiscordAuthToken
import logging import logging
@ -277,6 +278,17 @@ def run_databaseUpdate():
add_to_databases(user, groups, syncgroups) add_to_databases(user, groups, syncgroups)
remove_from_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 # Run every 3 hours
@periodic_task(run_every=crontab(minute=0, hour="*/3")) @periodic_task(run_every=crontab(minute=0, hour="*/3"))

View File

@ -13,8 +13,8 @@ DISCORD_URL = "https://discordapp.com/api"
class DiscordAPIManager: class DiscordAPIManager:
def __init__(self, server_id, email, password): def __init__(self, server_id, email, password, user=None):
self.token = DiscordAPIManager.get_token_by_user(email, password) self.token = DiscordAPIManager.get_token_by_user(email, password, user)
self.email = email self.email = email
self.password = password self.password = password
self.server_id = server_id self.server_id = server_id
@ -131,9 +131,8 @@ class DiscordAPIManager:
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@staticmethod def accept_invite(self, invite_id):
def accept_invite(invite_id, token): custom_headers = {'accept': 'application/json', 'authorization': self.token}
custom_headers = {'accept': 'application/json', 'authorization': token}
path = DISCORD_URL + "/invite/" + str(invite_id) path = DISCORD_URL + "/invite/" + str(invite_id)
r = requests.post(path, headers=custom_headers) r = requests.post(path, headers=custom_headers)
logger.debug("Received status code %s after accepting invite." % r.status_code) 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) raise KeyError('Group not found on server: ' + group_name)
@staticmethod @staticmethod
def get_token_by_user(email, password): def get_token_by_user(email, password, user):
if DiscordAuthToken.objects.filter(email=email).exists(): 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) 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): if DiscordAPIManager.validate_token(auth.token):
logger.debug("Token still valid. Returning token starting with %s" % auth.token[0:5]) logger.debug("Token still valid. Returning token starting with %s" % auth.token[0:5])
return auth.token return auth.token
else: else:
logger.debug("Token has expired. Deleting.") logger.debug("Token has expired. Deleting.")
auth.delete() 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 = { data = {
"email" : email, "email" : email,
"password": password, "password": password,
@ -244,11 +246,19 @@ class DiscordAPIManager:
logger.debug("Received status code %s after generating auth token for custom user." % r.status_code) logger.debug("Received status code %s after generating auth token for custom user." % r.status_code)
r.raise_for_status() r.raise_for_status()
token = r.json()['token'] token = r.json()['token']
auth = DiscordAuthToken(email=email, token=token) auth = DiscordAuthToken(email=email, token=token, user=user)
auth.save() auth.save()
logger.debug("Created cached token for email starting with %s" % email[0:3]) logger.debug("Created cached token for email starting with %s" % email[0:3])
return token 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 @staticmethod
def get_user_profile(email, password): def get_user_profile(email, password):
token = DiscordAPIManager.get_token_by_user(email, password) token = DiscordAPIManager.get_token_by_user(email, password)
@ -371,22 +381,21 @@ class DiscordManager:
return current_password return current_password
@staticmethod @staticmethod
def add_user(email, password): def add_user(email, password, user):
try: try:
logger.debug("Adding new user to discord with email %s and password of length %s" % (email[0:3], len(password))) logger.debug("Adding new user %s to discord with email %s and password of length %s" % (user, email[0:3], len(password)))
api = DiscordAPIManager(settings.DISCORD_SERVER_ID, settings.DISCORD_USER_EMAIL, settings.DISCORD_USER_PASSWORD) server_api = DiscordAPIManager(settings.DISCORD_SERVER_ID, settings.DISCORD_USER_EMAIL, settings.DISCORD_USER_PASSWORD)
profile = DiscordAPIManager.get_user_profile(email, 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) logger.debug("Got profile for user: %s" % profile)
user_id = profile['id'] user_id = profile['id']
logger.debug("Determined user id: %s" % user_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) logger.debug("User is currently banned. Unbanning %s" % user_id)
api.unban_user(user_id) server_api.unban_user(user_id)
invite_code = api.create_invite()['code'] invite_code = server_api.create_invite()['code']
logger.debug("Generated invite code beginning with %s" % invite_code[0:5]) logger.debug("Generated invite code beginning with %s" % invite_code[0:5])
token = DiscordAPIManager.get_token_by_user(email, password) user_api.accept_invite(invite_code)
logger.debug("Got auth token for supplied credentials beginning with %s" % token[0:5])
DiscordAPIManager.accept_invite(invite_code, token)
logger.info("Added user to discord server %s with id %s" % (settings.DISCORD_SERVER_ID, user_id)) logger.info("Added user to discord server %s with id %s" % (settings.DISCORD_SERVER_ID, user_id))
return user_id return user_id
except: except:

View File

@ -1,5 +1,5 @@
from django.db import models from django.db import models
from django.contrib.auth.models import Group from django.contrib.auth.models import Group, User
class TSgroup(models.Model): class TSgroup(models.Model):
ts_group_id = models.IntegerField(primary_key=True) ts_group_id = models.IntegerField(primary_key=True)
@ -32,7 +32,9 @@ class UserTSgroup(models.Model):
return self.user.name return self.user.name
class DiscordAuthToken(models.Model): 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) token = models.CharField(max_length=254)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
def __str__(self): def __str__(self):
return self.email output = "Discord Token for email %s user %s" % (self.email, self.user)
return output.encode('utf-8')

View File

@ -436,7 +436,7 @@ def activate_discord(request):
password = form.cleaned_data['password'] password = form.cleaned_data['password']
logger.debug("Form contains password of length %s" % len(password)) logger.debug("Form contains password of length %s" % len(password))
try: 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) logger.debug("Received discord uid %s" % user_id)
if user_id != "": if user_id != "":
AuthServicesInfoManager.update_user_discord_info(user_id, request.user) AuthServicesInfoManager.update_user_discord_info(user_id, request.user)