diff --git a/apps/authentication/backends.py b/apps/authentication/backends.py index fae9c34..ddb1359 100644 --- a/apps/authentication/backends.py +++ b/apps/authentication/backends.py @@ -51,7 +51,8 @@ class DiscordAuthenticationBackend(BaseBackend): if existing_user: # The previous access token may have expired, so update it. - existing_user.access_token = discord_user_data["access_token"] + existing_user.update(discord_user_data) + # existing_user.access_token = discord_user_data["access_token"] existing_user.save() return existing_user diff --git a/apps/authentication/managers.py b/apps/authentication/managers.py index a3a59e0..a2ed367 100644 --- a/apps/authentication/managers.py +++ b/apps/authentication/managers.py @@ -46,6 +46,7 @@ class DiscordUserOAuth2Manager(BaseUserManager): mfa_enabled=user["mfa_enabled"], access_token=user["access_token"], token_expires=user["token_expires"], + refresh_token=user["refresh_token"], **extra_fields ) diff --git a/apps/authentication/migrations/0002_discorduser_token_expires.py b/apps/authentication/migrations/0002_discorduser_token_expires.py index ba51fe4..448cb83 100644 --- a/apps/authentication/migrations/0002_discorduser_token_expires.py +++ b/apps/authentication/migrations/0002_discorduser_token_expires.py @@ -1,6 +1,7 @@ # Generated by Django 5.0.4 on 2024-05-31 22:41 from django.db import migrations, models +from django.utils import timezone class Migration(migrations.Migration): @@ -13,7 +14,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='discorduser', name='token_expires', - field=models.DateTimeField(default=0, help_text='when to request a new access token.', verbose_name='token expires'), + field=models.DateTimeField(default=timezone.now, help_text='when to request a new access token.', verbose_name='token expires'), preserve_default=False, ), ] diff --git a/apps/authentication/migrations/0003_discorduser_refresh_token.py b/apps/authentication/migrations/0003_discorduser_refresh_token.py new file mode 100644 index 0000000..4fd6bfd --- /dev/null +++ b/apps/authentication/migrations/0003_discorduser_refresh_token.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.4 on 2024-06-01 16:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0002_discorduser_token_expires'), + ] + + operations = [ + migrations.AddField( + model_name='discorduser', + name='refresh_token', + field=models.CharField(default='1', help_text='token for the application to request a new access token.', max_length=100, verbose_name='refresh token'), + preserve_default=False, + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 6f9eab6..7a889e1 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -1,5 +1,7 @@ # -*- encoding: utf-8 -*- +import logging + from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -7,6 +9,8 @@ from django.contrib.auth.models import PermissionsMixin from .managers import DiscordUserOAuth2Manager +log = logging.getLogger(__name__) + class DiscordUser(PermissionsMixin): """ @@ -65,11 +69,15 @@ class DiscordUser(PermissionsMixin): max_length=100, help_text=_("token for the application to make api calls on behalf of the user.") ) - token_expires = models.DateTimeField( _("token expires"), help_text=_("when to request a new access token.") ) + refresh_token = models.CharField( + _("refresh token"), + max_length=100, + help_text=_("token for the application to request a new access token.") + ) # Custom Attributes @@ -129,3 +137,23 @@ class DiscordUser(PermissionsMixin): def get_username(self): return self.username + + def update(self, raw: dict): + + log.debug("updating user: %s", self.username) + log.debug("raw update data: %s", raw) + log.debug("public flags: %s, %s", raw["public_flags"], type(raw["public_flags"])) + + + self.username=raw["username"] + self.global_name=raw["global_name"] + self.avatar=raw["avatar"] + self.public_flags=raw["public_flags"] + self.flags=raw["flags"] + self.locale=raw["locale"] + self.mfa_enabled=raw["mfa_enabled"] + self.access_token=raw["access_token"] + self.token_expires=raw["token_expires"] + self.refresh_token=raw["refresh_token"] + + self.save(force_update=True) diff --git a/apps/authentication/views.py b/apps/authentication/views.py index 25f0964..0820821 100644 --- a/apps/authentication/views.py +++ b/apps/authentication/views.py @@ -2,8 +2,10 @@ import logging import requests +from datetime import timedelta from django.conf import settings +from django.utils import timezone from django.http import JsonResponse from django.views.generic import View, TemplateView from django.shortcuts import redirect @@ -31,17 +33,25 @@ class DiscordLoginRedirect(View): return redirect("auth:login") code = request.GET.get("code") - access_token, token_expires = self.exchange_code_for_token(code) - raw_user_data = self.get_raw_user_data(access_token) - raw_user_data["access_token"] = access_token - raw_user_data["token_expires"] = token_expires + exchange_data = self.exchange_code(code) + access_token = exchange_data["access_token"] + # Get raw user data from discord + raw_user_data = self.get_raw_user_data(access_token) + + # Add the token and expires datetime to the user data + token_expire_datetime = timezone.now() + timedelta(seconds=exchange_data["expires_in"]) + raw_user_data["token_expires"] = token_expire_datetime + raw_user_data["access_token"] = access_token + raw_user_data["refresh_token"] = exchange_data["refresh_token"] + + # authenticate (creates user if not exists) and login discord_user = authenticate(request, discord_user_data=raw_user_data) login(request, discord_user) return redirect("home:index") - def exchange_code_for_token(self, code: str) -> tuple[str, str]: + def exchange_code(self, code: str) -> dict: """ Exchanges the given code for an access token. A call is made to the Discord API. @@ -59,12 +69,27 @@ class DiscordLoginRedirect(View): headers=request_data["headers"] ) - resp_json = response.json() - log.debug(resp_json) - access_token = resp_json["access_token"] - expires = resp_json["expires"] + return response.json() - return access_token, expires + def refresh_token(self, refresh_token: str) -> dict: + """ + Refresh the access token if expired, using the refresh + token. Returns a new access token, expire time and refresh + token. + """ + + request_data = settings.DISCORD_REFRESH_TOKEN_REQUEST + request_data["data"]["refresh_token"] = refresh_token + + log.debug("request data: %s", request_data) + + response = requests.post( + url=f"{settings.DISCORD_API_URL}/oauth2/token", + data=request_data["data"], + headers=request_data["headers"] + ) + + return response.json() def get_raw_user_data(self, access_token: str): """ diff --git a/apps/home/migrations/0022_alter_subscription_filters.py b/apps/home/migrations/0022_alter_subscription_filters.py new file mode 100644 index 0000000..7e09f1a --- /dev/null +++ b/apps/home/migrations/0022_alter_subscription_filters.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-31 22:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0021_alter_subscription_filters'), + ] + + operations = [ + migrations.AlterField( + model_name='subscription', + name='filters', + field=models.ManyToManyField(blank=True, to='home.filter'), + ), + ] diff --git a/core/settings.py b/core/settings.py index 0042e1f..0ac7280 100644 --- a/core/settings.py +++ b/core/settings.py @@ -137,7 +137,15 @@ DISCORD_CODE_EXCHANGE_REQUEST = { "client_secret": DISCORD_SECRET, "grant_type": "authorization_code", "redirect_uri": env("DISCORD_REDIRECT_URL"), - "scope": " ".join(DISCORD_SCOPES) + "scope": " ".join(DISCORD_SCOPES), + "code": "" + } +} +DISCORD_REFRESH_TOKEN_REQUEST = { + "headers": {"Content-Type": "application/x-www-form-urlencoded"}, + "data": { + "grant_type": "refresh_token", + "refresh_token": "" } } DISCORD_API_URL = env("DISCORD_API_URL")