From 358f8d446a020c02b5c8b4acb966750357bda7aa Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 16 Nov 2020 03:33:48 +0100 Subject: [PATCH] Use Discord oauth to log in --- lglog/middlewares.py | 78 ++++++++++++++++++++++++++++++++++++++ lglog/settings.py | 30 ++++++++++++--- lglog/templates/index.html | 2 + requirements.txt | 1 + 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 lglog/middlewares.py diff --git a/lglog/middlewares.py b/lglog/middlewares.py new file mode 100644 index 0000000..2664a53 --- /dev/null +++ b/lglog/middlewares.py @@ -0,0 +1,78 @@ +from authlib.integrations.base_client import OAuthError +from authlib.integrations.django_client import OAuth +from authlib.oauth2.rfc6749 import OAuth2Token +from django.conf import settings +from django.shortcuts import redirect +from django.utils.deprecation import MiddlewareMixin + + +class OAuthMiddleware(MiddlewareMixin): + + def __init__(self, get_response=None): + super().__init__(get_response) + self.oauth = OAuth() + + def process_request(self, request): + if settings.OAUTH_URL_WHITELISTS is not None: + for w in settings.OAUTH_URL_WHITELISTS: + if request.path.startswith(w): + return self.get_response(request) + + def update_token(token, refresh_token, access_token): + request.session['token'] = token + return None + + sso_client = self.oauth.register( + settings.OAUTH_CLIENT_NAME, overwrite=True, **settings.OAUTH_CLIENT, update_token=update_token + ) + + if request.path.startswith('/oauth/callback'): + self.clear_session(request) + request.session['token'] = sso_client.authorize_access_token(request) + if self.get_current_user(sso_client, request) is not None: + redirect_uri = request.session.pop('redirect_uri', None) + if redirect_uri is not None: + return redirect(redirect_uri) + return redirect("index") + + if request.session.get('token', None) is not None: + current_user = self.get_current_user(sso_client, request) + if current_user is not None: + return self.get_response(request) + + # remember redirect URI for redirecting to the original URL. + request.session['redirect_uri'] = request.path + return sso_client.authorize_redirect(request, settings.OAUTH_CLIENT['redirect_uri']) + + # fetch current login user info + # 1. check if it's in cache + # 2. fetch from remote API when it's not in cache + @staticmethod + def get_current_user(sso_client, request): + token = request.session.get('token', None) + if token is None or 'access_token' not in token: + return None + + if not OAuth2Token.from_dict(token).is_expired() and 'user' in request.session: + return request.session['user'] + + try: + res = sso_client.get(settings.OAUTH_CLIENT['userinfo_endpoint'], token=OAuth2Token(token)) + if res.ok: + request.session['user'] = res.json() + return res.json() + raise Exception(res, str(res.__dict__)) + except OAuthError as e: + print(e) + return None + + @staticmethod + def clear_session(request): + try: + del request.session['user'] + del request.session['token'] + except KeyError: + pass + + def __del__(self): + print('destroyed') diff --git a/lglog/settings.py b/lglog/settings.py index 92e20f9..4d169db 100644 --- a/lglog/settings.py +++ b/lglog/settings.py @@ -12,10 +12,11 @@ https://docs.djangoproject.com/en/3.1/ref/settings/ from pathlib import Path +from .secrets import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ @@ -27,7 +28,6 @@ DEBUG = True ALLOWED_HOSTS = [] - # Application definition INSTALLED_APPS = [ @@ -39,6 +39,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'crispy_forms', + 'django_openid_auth', 'lg', ] @@ -53,6 +54,7 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware', + 'lglog.middlewares.OAuthMiddleware', ] ROOT_URLCONF = 'lglog.urls' @@ -75,7 +77,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'lglog.wsgi.application' - # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases @@ -86,7 +87,6 @@ DATABASES = { } } - # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators @@ -105,13 +105,13 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] - # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ LANGUAGE_CODE = 'en' from django.utils.translation import gettext_lazy as _ + LANGUAGES = [ ('en', _('English')), ('fr', _('French')), @@ -125,7 +125,6 @@ USE_L10N = True USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ @@ -141,3 +140,22 @@ STATICFILES_DIRS = [ CRISPY_TEMPLATE_PACK = 'bootstrap4' DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html' + +# OAuth Settings +OAUTH_URL_WHITELISTS = [] + +OAUTH_CLIENT_NAME = 'discord' + +OAUTH_CLIENT = { + 'client_id': OAUTH_CLIENT_ID, + 'client_secret': OAUTH_CLIENT_SECRET, + 'access_token_url': 'https://discordapp.com/api/oauth2/token', + 'authorize_url': 'https://discordapp.com/api/oauth2/authorize', + 'api_base_url': 'https://discordapp.com/api/', + 'redirect_uri': 'http://localhost:8000/oauth/callback', + 'client_kwargs': { + 'scope': 'identify email', + 'token_placement': 'header' + }, + 'userinfo_endpoint': 'users/@me', +} diff --git a/lglog/templates/index.html b/lglog/templates/index.html index b49cc26..1d25068 100644 --- a/lglog/templates/index.html +++ b/lglog/templates/index.html @@ -6,4 +6,6 @@ {% block content %} Historique de loup-garou + + {{ request.session.user }} {% endblock %} diff --git a/requirements.txt b/requirements.txt index bfd2d35..a0c78f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +authlib~=0.15 Django~=3.1 django-crispy-forms django-polymorphic~=3.0