@ -0,0 +1,4 @@ | |||||
.idea/ | |||||
db.sqlite3 | |||||
venv2/ | |||||
@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2021 Songrong Jiang | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@ -0,0 +1,33 @@ | |||||
# Wikimedia OAuth2 Django Demo | |||||
A minimal prototype showing how to authenticate wikimedia | |||||
users with OAuth2 | |||||
## Quick Start | |||||
1. Create virtual environment | |||||
```bash | |||||
virtualenv --python=python3.9 venv | |||||
``` | |||||
1. Install requirements | |||||
```bash | |||||
pip install -r requirements.txt | |||||
``` | |||||
1. Run the server | |||||
```bash | |||||
python manage.py migrate | |||||
python manage.py runserver | |||||
``` | |||||
## Behind the scene | |||||
The OAuth2 configuration is located at settings.py | |||||
The default session backend is sqlite3. | |||||
The base for this prototype was done by songrgg, who published | |||||
his code on Microsoft's Github. His Blog Post you can find here: | |||||
https://songrgg.github.io/programming/django-oauth-client-setup | |||||
@ -0,0 +1,21 @@ | |||||
#!/usr/bin/env python | |||||
"""Django's command-line utility for administrative tasks.""" | |||||
import os | |||||
import sys | |||||
def main(): | |||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oauth_demo.settings') | |||||
try: | |||||
from django.core.management import execute_from_command_line | |||||
except ImportError as exc: | |||||
raise ImportError( | |||||
"Couldn't import Django. Are you sure it's installed and " | |||||
"available on your PYTHONPATH environment variable? Did you " | |||||
"forget to activate a virtual environment?" | |||||
) from exc | |||||
execute_from_command_line(sys.argv) | |||||
if __name__ == '__main__': | |||||
main() |
@ -0,0 +1,16 @@ | |||||
""" | |||||
ASGI config for oauth_demo project. | |||||
It exposes the ASGI callable as a module-level variable named ``application``. | |||||
For more information on this file, see | |||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ | |||||
""" | |||||
import os | |||||
from django.core.asgi import get_asgi_application | |||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oauth_demo.settings') | |||||
application = get_asgi_application() |
@ -0,0 +1,84 @@ | |||||
from authlib.integrations.base_client import OAuthError | |||||
from authlib.integrations.django_client import OAuth | |||||
from authlib.oauth2.rfc6749 import OAuth2Token | |||||
from django.shortcuts import redirect | |||||
from django.utils.deprecation import MiddlewareMixin | |||||
from oauth_demo import settings | |||||
from oauth_demo import views | |||||
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 | |||||
print('oioi') | |||||
print('oi 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'): | |||||
print('oi') | |||||
self.clear_session(request) | |||||
request.session['token'] = sso_client.authorize_access_token(request) | |||||
print('blub', request.session['token']) | |||||
print('user', self.get_current_user(sso_client, 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(views.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)) | |||||
print('json oi oi' , res.json()) | |||||
if res.ok: | |||||
request.session['user'] = res.json() | |||||
return res.json() | |||||
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') |
@ -0,0 +1,145 @@ | |||||
""" | |||||
Django settings for oauth_demo project. | |||||
Generated by 'django-admin startproject' using Django 3.0.5. | |||||
For more information on this file, see | |||||
https://docs.djangoproject.com/en/3.0/topics/settings/ | |||||
For the full list of settings and their values, see | |||||
https://docs.djangoproject.com/en/3.0/ref/settings/ | |||||
""" | |||||
import os | |||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) | |||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |||||
# Quick-start development settings - unsuitable for production | |||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ | |||||
# SECURITY WARNING: keep the secret key used in production secret! | |||||
SECRET_KEY = '#jk+74g_ilb4h)f!_20mmcg^5-+veuj2(v%0ufymq+r%mc3im-' | |||||
# SECURITY WARNING: don't run with debug turned on in production! | |||||
DEBUG = True | |||||
ALLOWED_HOSTS = [] | |||||
# Application definition | |||||
INSTALLED_APPS = [ | |||||
'django.contrib.admin', | |||||
'django.contrib.auth', | |||||
'django.contrib.contenttypes', | |||||
'django.contrib.sessions', | |||||
'django.contrib.messages', | |||||
'django.contrib.staticfiles', | |||||
] | |||||
MIDDLEWARE = [ | |||||
'django.middleware.security.SecurityMiddleware', | |||||
'django.contrib.sessions.middleware.SessionMiddleware', | |||||
'django.middleware.common.CommonMiddleware', | |||||
'django.middleware.csrf.CsrfViewMiddleware', | |||||
'django.contrib.auth.middleware.AuthenticationMiddleware', | |||||
'django.contrib.messages.middleware.MessageMiddleware', | |||||
'django.middleware.clickjacking.XFrameOptionsMiddleware', | |||||
'oauth_demo.middleware.oauth.OAuthMiddleware' | |||||
] | |||||
ROOT_URLCONF = 'oauth_demo.urls' | |||||
TEMPLATES = [ | |||||
{ | |||||
'BACKEND': 'django.template.backends.django.DjangoTemplates', | |||||
'DIRS': [os.path.join(BASE_DIR, 'templates')] | |||||
, | |||||
'APP_DIRS': True, | |||||
'OPTIONS': { | |||||
'context_processors': [ | |||||
'django.template.context_processors.debug', | |||||
'django.template.context_processors.request', | |||||
'django.contrib.auth.context_processors.auth', | |||||
'django.contrib.messages.context_processors.messages', | |||||
], | |||||
}, | |||||
}, | |||||
] | |||||
WSGI_APPLICATION = 'oauth_demo.wsgi.application' | |||||
# Database | |||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases | |||||
DATABASES = { | |||||
'default': { | |||||
'ENGINE': 'django.db.backends.sqlite3', | |||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), | |||||
} | |||||
} | |||||
# Password validation | |||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators | |||||
AUTH_PASSWORD_VALIDATORS = [ | |||||
{ | |||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | |||||
}, | |||||
{ | |||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | |||||
}, | |||||
{ | |||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | |||||
}, | |||||
{ | |||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | |||||
}, | |||||
] | |||||
# Internationalization | |||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/ | |||||
LANGUAGE_CODE = 'en-us' | |||||
TIME_ZONE = 'UTC' | |||||
USE_I18N = True | |||||
USE_L10N = True | |||||
USE_TZ = True | |||||
# Static files (CSS, JavaScript, Images) | |||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/ | |||||
STATIC_URL = '/static/' | |||||
# OAuth Settings | |||||
OAUTH_URL_WHITELISTS = [] | |||||
OAUTH_CLIENT_NAME = '<name-of-the-configured-wikimedia-app>' | |||||
OAUTH_CLIENT = { | |||||
'client_id': '<client-application-key-of-wikimedia-app>', | |||||
'client_secret': '<client-application-secret-of-wikimedia-app>', | |||||
'access_token_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/access_token', | |||||
'authorize_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/authorize', | |||||
'api_base_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/resource', | |||||
'redirect_uri': 'http://localhost:8000/oauth/callback', | |||||
'client_kwargs': { | |||||
'scope': 'basic', | |||||
'token_placement': 'header' | |||||
}, | |||||
'userinfo_endpoint': 'resource/profile', | |||||
} | |||||
OAUTH_COOKIE_SESSION_ID = 'sso_session_id' | |||||
@ -0,0 +1,23 @@ | |||||
"""oauth_demo URL Configuration | |||||
The `urlpatterns` list routes URLs to views. For more information please see: | |||||
https://docs.djangoproject.com/en/3.0/topics/http/urls/ | |||||
Examples: | |||||
Function views | |||||
1. Add an import: from my_app import views | |||||
2. Add a URL to urlpatterns: path('', views.home, name='home') | |||||
Class-based views | |||||
1. Add an import: from other_app.views import Home | |||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') | |||||
Including another URLconf | |||||
1. Import the include() function: from django.urls import include, path | |||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) | |||||
""" | |||||
from django.contrib import admin | |||||
from django.urls import path | |||||
from oauth_demo.views import index | |||||
urlpatterns = [ | |||||
path('', index), | |||||
path('admin/', admin.site.urls), | |||||
] |
@ -0,0 +1,6 @@ | |||||
from django.http import HttpResponse | |||||
def index(request): | |||||
msg = "Hello %s, you're logined." % request.session['user']['username'] | |||||
return HttpResponse(msg) |
@ -0,0 +1,16 @@ | |||||
""" | |||||
WSGI config for oauth_demo project. | |||||
It exposes the WSGI callable as a module-level variable named ``application``. | |||||
For more information on this file, see | |||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ | |||||
""" | |||||
import os | |||||
from django.core.wsgi import get_wsgi_application | |||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oauth_demo.settings') | |||||
application = get_wsgi_application() |
@ -0,0 +1,16 @@ | |||||
asgiref==3.7.2 | |||||
Authlib==1.2.1 | |||||
certifi==2023.7.22 | |||||
cffi==1.16.0 | |||||
chardet==5.2.0 | |||||
charset-normalizer==3.3.0 | |||||
cryptography==41.0.4 | |||||
Django==4.2.6 | |||||
idna==3.4 | |||||
pycparser==2.21 | |||||
pytz==2023.3.post1 | |||||
requests==2.31.0 | |||||
six==1.16.0 | |||||
sqlparse==0.4.4 | |||||
typing_extensions==4.8.0 | |||||
urllib3==2.0.6 |