add working oauth and ldap client

This commit is contained in:
Jens Langhammer
2018-11-11 13:41:48 +01:00
parent 935155ce94
commit 5aa245cac0
212 changed files with 198506 additions and 0 deletions

View File

@ -0,0 +1,3 @@
"""passbook oauth_client Header"""
__version__ = '0.0.1-alpha'
default_app_config = 'passbook.oauth_client.apps.PassbookOAuthClientConfig'

View File

@ -0,0 +1,5 @@
"""passbook oauth_client admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_oauth_client')

View File

@ -0,0 +1,23 @@
"""passbook oauth_client config"""
from logging import getLogger
from django.apps import AppConfig
from passbook.lib.config import CONFIG
from importlib import import_module
LOGGER = getLogger(__name__)
class PassbookOAuthClientConfig(AppConfig):
"""passbook oauth_client config"""
name = 'passbook.oauth_client'
label = 'passbook_oauth_client'
verbose_name = 'passbook OAuth Client'
def ready(self):
"""Load source_types from config file"""
source_types_to_load = CONFIG.y('oauth_client.source_tyoes')
for source_type in source_types_to_load:
try:
import_module(source_type)
LOGGER.info("Loaded %s", source_type)
except ImportError as exc:
LOGGER.debug(exc)

View File

@ -0,0 +1,25 @@
"""passbook oauth_client Authorization backend"""
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
class AuthorizedServiceBackend(ModelBackend):
"Authentication backend for users registered with remote OAuth provider."
def authenticate(self, request, source=None, identifier=None):
"Fetch user for a given source by id."
source_q = Q(source__name=source)
if isinstance(source, OAuthSource):
source_q = Q(source=source)
try:
access = UserOAuthSourceConnection.objects.filter(
source_q, identifier=identifier
).select_related('user')[0]
except IndexError:
print('hmm')
return None
else:
print('a')
return access.user

View File

@ -0,0 +1,246 @@
"""OAuth Clients"""
import json
from logging import getLogger
from urllib.parse import parse_qs, urlencode
from django.conf import settings
from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.encoding import force_text
from requests import Session
from requests.exceptions import RequestException
from requests_oauthlib import OAuth1
LOGGER = getLogger(__name__)
class BaseOAuthClient:
"""Base OAuth Client"""
_session = None
def __init__(self, source, token=''):
self.source = source
self.token = token
self._session = Session()
self._session.headers.update({'User-Agent': 'web:passbook:%s' % settings.VERSION})
def get_access_token(self, request, callback=None):
"Fetch access token from callback request."
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
def get_profile_info(self, raw_token):
"Fetch user profile information."
try:
response = self.request('get', self.source.profile_url, token=raw_token)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch user profile: %s', exc)
return None
else:
return response.json() or response.text
def get_redirect_args(self, request, callback):
"Get request parameters for redirect url."
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
def get_redirect_url(self, request, callback, parameters=None):
"Build authentication redirect url."
args = self.get_redirect_args(request, callback=callback)
additional = parameters or {}
args.update(additional)
params = urlencode(args)
LOGGER.info("Redirect args: %s", args)
return '{0}?{1}'.format(self.source.authorization_url, params)
def parse_raw_token(self, raw_token):
"Parse token and secret from raw token response."
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
def request(self, method, url, **kwargs):
"Build remote url request."
return self._session.request(method, url, **kwargs)
@property
def session_key(self):
"""
Return Session Key
"""
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
class OAuthClient(BaseOAuthClient):
"""OAuth1 Client"""
def get_access_token(self, request, callback=None):
"Fetch access token from callback request."
raw_token = request.session.get(self.session_key, None)
verifier = request.GET.get('oauth_verifier', None)
if raw_token is not None and verifier is not None:
data = {'oauth_verifier': verifier}
callback = request.build_absolute_uri(callback or request.path)
callback = force_text(callback)
try:
response = self.request('post', self.source.access_token_url,
token=raw_token, data=data, oauth_callback=callback)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch access token: %s', exc)
return None
else:
return response.text
return None
def get_request_token(self, request, callback):
"Fetch the OAuth request token. Only required for OAuth 1.0."
callback = force_text(request.build_absolute_uri(callback))
try:
response = self.request(
'post', self.source.request_token_url, oauth_callback=callback)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch request token: %s', exc)
return None
else:
return response.text
def get_redirect_args(self, request, callback):
"Get request parameters for redirect url."
callback = force_text(request.build_absolute_uri(callback))
raw_token = self.get_request_token(request, callback)
token, secret = self.parse_raw_token(raw_token)
if token is not None and secret is not None:
request.session[self.session_key] = raw_token
return {
'oauth_token': token,
'oauth_callback': callback,
}
def parse_raw_token(self, raw_token):
"Parse token and secret from raw token response."
if raw_token is None:
return (None, None)
qs = parse_qs(raw_token)
token = qs.get('oauth_token', [None])[0]
secret = qs.get('oauth_token_secret', [None])[0]
return (token, secret)
def request(self, method, url, **kwargs):
"Build remote url request. Constructs necessary auth."
user_token = kwargs.pop('token', self.token)
token, secret = self.parse_raw_token(user_token)
callback = kwargs.pop('oauth_callback', None)
verifier = kwargs.get('data', {}).pop('oauth_verifier', None)
oauth = OAuth1(
resource_owner_key=token,
resource_owner_secret=secret,
client_key=self.source.consumer_key,
client_secret=self.source.consumer_secret,
verifier=verifier,
callback_uri=callback,
)
kwargs['auth'] = oauth
return super(OAuthClient, self).request(method, url, **kwargs)
@property
def session_key(self):
return 'oauth-client-{0}-request-token'.format(self.source.name)
class OAuth2Client(BaseOAuthClient):
"""OAuth2 Client"""
def check_application_state(self, request, callback):
"Check optional state parameter."
stored = request.session.get(self.session_key, None)
returned = request.GET.get('state', None)
check = False
if stored is not None:
if returned is not None:
check = constant_time_compare(stored, returned)
else:
LOGGER.warning('No state parameter returned by the source.')
else:
LOGGER.warning('No state stored in the sesssion.')
return check
def get_access_token(self, request, callback=None, **request_kwargs):
"Fetch access token from callback request."
callback = request.build_absolute_uri(callback or request.path)
if not self.check_application_state(request, callback):
LOGGER.warning('Application state check failed.')
return None
if 'code' in request.GET:
args = {
'client_id': self.source.consumer_key,
'redirect_uri': callback,
'client_secret': self.source.consumer_secret,
'code': request.GET['code'],
'grant_type': 'authorization_code',
}
else:
LOGGER.warning('No code returned by the source')
return None
try:
response = self.request('post', self.source.access_token_url,
data=args, **request_kwargs)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch access token: %s', exc)
return None
else:
return response.text
def get_application_state(self, request, callback):
"Generate state optional parameter."
return get_random_string(32)
def get_redirect_args(self, request, callback):
"Get request parameters for redirect url."
callback = request.build_absolute_uri(callback)
args = {
'client_id': self.source.consumer_key,
'redirect_uri': callback,
'response_type': 'code',
}
state = self.get_application_state(request, callback)
if state is not None:
args['state'] = state
request.session[self.session_key] = state
return args
def parse_raw_token(self, raw_token):
"Parse token and secret from raw token response."
if raw_token is None:
return (None, None)
# Load as json first then parse as query string
try:
token_data = json.loads(raw_token)
except ValueError:
qs = parse_qs(raw_token)
token = qs.get('access_token', [None])[0]
else:
token = token_data.get('access_token', None)
return (token, None)
def request(self, method, url, **kwargs):
"Build remote url request. Constructs necessary auth."
user_token = kwargs.pop('token', self.token)
token, _ = self.parse_raw_token(user_token)
if token is not None:
params = kwargs.get('params', {})
params['access_token'] = token
kwargs['params'] = params
return super(OAuth2Client, self).request(method, url, **kwargs)
@property
def session_key(self):
return 'oauth-client-{0}-request-state'.format(self.source.name)
def get_client(source, token=''):
"Return the API client for the given source."
cls = OAuth2Client
if source.request_token_url:
cls = OAuthClient
return cls(source, token)

View File

@ -0,0 +1,17 @@
"""
Supervisr Mod Oauth Client Errors
"""
class OAuthClientError(Exception):
"""
Base error for all OAuth Client errors
"""
pass
class OAuthClientEmailMissingError(OAuthClientError):
"""
Error which is raised when user is missing email address from profile
"""
pass

View File

@ -0,0 +1,80 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: templates/mod/auth/oauth/client/settings.html:11
msgid "OAuth2"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:16
msgid "Connected Accounts"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:23
msgid "Provider"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:24
msgid "Status"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:25
msgid "Action"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:26
msgid "ID"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:48
msgid "No Providers configured!"
msgstr ""
#: views/core.py:126
#, python-format
msgid "Provider %(name)s didn't provide an E-Mail address."
msgstr ""
#: views/core.py:184 views/core.py:225
#, python-format
msgid "Successfully authenticated with %(provider)s!"
msgstr ""
#: views/core.py:192
msgid "Authentication Failed."
msgstr ""
#: views/core.py:204
#, python-format
msgid "Linked user with OAuth Provider %s"
msgstr ""
#: views/core.py:208
#, python-format
msgid "Successfully linked %(provider)s!"
msgstr ""
#: views/core.py:221
#, python-format
msgid "Authenticated user with OAuth Provider %s"
msgstr ""
#: views/core.py:247
msgid "Connection successfully deleted"
msgstr ""

View File

@ -0,0 +1,79 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-20 10:47+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: templates/mod/auth/oauth/client/settings.html:11
msgid "OAuth2"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:16
msgid "Connected Accounts"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:23
msgid "Provider"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:24
msgid "Status"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:25
msgid "Action"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:26
msgid "ID"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:48
msgid "No Providers configured!"
msgstr ""
#: views/core.py:126
#, python-format
msgid "Provider %(name)s didn't provide an E-Mail address."
msgstr ""
#: views/core.py:184 views/core.py:225
#, python-format
msgid "Successfully authenticated with %(provider)s!"
msgstr ""
#: views/core.py:192
msgid "Authentication Failed."
msgstr ""
#: views/core.py:204
#, python-format
msgid "Linked user with OAuth Provider %s"
msgstr ""
#: views/core.py:208
#, python-format
msgid "Successfully linked %(provider)s!"
msgstr ""
#: views/core.py:221
#, python-format
msgid "Authenticated user with OAuth Provider %s"
msgstr ""
#: views/core.py:247
msgid "Connection successfully deleted"
msgstr ""

View File

@ -0,0 +1,80 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: templates/mod/auth/oauth/client/settings.html:11
msgid "OAuth2"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:16
msgid "Connected Accounts"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:23
msgid "Provider"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:24
msgid "Status"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:25
msgid "Action"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:26
msgid "ID"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:48
msgid "No Providers configured!"
msgstr ""
#: views/core.py:126
#, python-format
msgid "Provider %(name)s didn't provide an E-Mail address."
msgstr ""
#: views/core.py:184 views/core.py:225
#, python-format
msgid "Successfully authenticated with %(provider)s!"
msgstr ""
#: views/core.py:192
msgid "Authentication Failed."
msgstr ""
#: views/core.py:204
#, python-format
msgid "Linked user with OAuth Provider %s"
msgstr ""
#: views/core.py:208
#, python-format
msgid "Successfully linked %(provider)s!"
msgstr ""
#: views/core.py:221
#, python-format
msgid "Authenticated user with OAuth Provider %s"
msgstr ""
#: views/core.py:247
msgid "Connection successfully deleted"
msgstr ""

View File

@ -0,0 +1,80 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: templates/mod/auth/oauth/client/settings.html:11
msgid "OAuth2"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:16
msgid "Connected Accounts"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:23
msgid "Provider"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:24
msgid "Status"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:25
msgid "Action"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:26
msgid "ID"
msgstr ""
#: templates/mod/auth/oauth/client/settings.html:48
msgid "No Providers configured!"
msgstr ""
#: views/core.py:126
#, python-format
msgid "Provider %(name)s didn't provide an E-Mail address."
msgstr ""
#: views/core.py:184 views/core.py:225
#, python-format
msgid "Successfully authenticated with %(provider)s!"
msgstr ""
#: views/core.py:192
msgid "Authentication Failed."
msgstr ""
#: views/core.py:204
#, python-format
msgid "Linked user with OAuth Provider %s"
msgstr ""
#: views/core.py:208
#, python-format
msgid "Successfully linked %(provider)s!"
msgstr ""
#: views/core.py:221
#, python-format
msgid "Authenticated user with OAuth Provider %s"
msgstr ""
#: views/core.py:247
msgid "Connection successfully deleted"
msgstr ""

View File

@ -0,0 +1,47 @@
# Generated by Django 2.1.3 on 2018-11-11 08:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='OAuthSource',
fields=[
('source_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Source')),
('provider_type', models.CharField(max_length=255)),
('request_token_url', models.CharField(blank=True, max_length=255)),
('authorization_url', models.CharField(max_length=255)),
('access_token_url', models.CharField(max_length=255)),
('profile_url', models.CharField(max_length=255)),
('consumer_key', models.TextField()),
('consumer_secret', models.TextField()),
],
options={
'verbose_name': 'OAuth Source',
'verbose_name_plural': 'OAuth Sources',
},
bases=('passbook_core.source',),
),
migrations.CreateModel(
name='UserOAuthSourceConnection',
fields=[
('usersourceconnection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.UserSourceConnection')),
('identifier', models.CharField(max_length=255)),
('access_token', models.TextField(blank=True, default=None, null=True)),
],
options={
'verbose_name': 'User OAuth Source Connection',
'verbose_name_plural': 'User OAuth Source Connections',
},
bases=('passbook_core.usersourceconnection',),
),
]

View File

@ -0,0 +1,46 @@
"""OAuth Client models"""
from django.db import models
from passbook.core.models import Source, UserSourceConnection
from passbook.oauth_client.clients import get_client
class OAuthSource(Source):
"""Configuration for OAuth provider."""
# FIXME: Dynamically load available source_types
provider_type = models.CharField(max_length=255)
request_token_url = models.CharField(blank=True, max_length=255)
authorization_url = models.CharField(max_length=255)
access_token_url = models.CharField(max_length=255)
profile_url = models.CharField(max_length=255)
consumer_key = models.TextField()
consumer_secret = models.TextField()
class Meta:
verbose_name = 'OAuth Source'
verbose_name_plural = 'OAuth Sources'
class UserOAuthSourceConnection(UserSourceConnection):
"""Authorized remote OAuth provider."""
identifier = models.CharField(max_length=255)
access_token = models.TextField(blank=True, null=True, default=None)
def save(self, *args, **kwargs):
self.access_token = self.access_token or None
super().save(*args, **kwargs)
@property
def api_client(self):
"""Get API Client"""
return get_client(self.source, self.access_token or '')
class Meta:
verbose_name = 'User OAuth Source Connection'
verbose_name_plural = 'User OAuth Source Connections'

View File

@ -0,0 +1,2 @@
requests_oauthlib>=0.4.2
oauthlib>=2.0.6

View File

@ -0,0 +1,7 @@
"""
Oauth2 Client Settings
"""
AUTHENTICATION_BACKENDS = [
'passbook.oauth_client.backends.AuthorizedServiceBackend',
]

View File

@ -0,0 +1,61 @@
"""Discord OAuth Views"""
import json
from logging import getLogger
from django.contrib.auth import get_user_model
from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
LOGGER = getLogger(__name__)
@MANAGER.source(kind=RequestKind.redirect, name='discord')
class DiscordOAuthRedirect(OAuthRedirect):
"""Discord OAuth2 Redirect"""
def get_additional_parameters(self, source):
return {
'scope': 'email identify',
}
class DiscordOAuth2Client(OAuth2Client):
"""Discord OAuth2 Client"""
def get_profile_info(self, raw_token):
"Fetch user profile information."
try:
token = json.loads(raw_token)
headers = {
'Authorization': '%s %s' % (token['token_type'], token['access_token'])
}
response = self.request('get', self.source.profile_url,
token=token['access_token'], headers=headers)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch user profile: %s', exc)
return None
else:
return response.json() or response.text
@MANAGER.source(kind=RequestKind.callback, name='discord')
class DiscordOAuth2Callback(OAuthCallback):
"""Discord OAuth2 Callback"""
client_class = DiscordOAuth2Client
def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = {
user.USERNAME_FIELD: info.get('username'),
'email': info.get('email', 'None'),
'first_name': info.get('username'),
'password': None,
}
discord_user = user_get_or_create(user_model=user, **user_data)
return discord_user

View File

@ -0,0 +1,36 @@
"""Facebook OAuth Views"""
from django.contrib.auth import get_user_model
from passbook.oauth_client.errors import OAuthClientEmailMissingError
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
@MANAGER.source(kind=RequestKind.redirect, name='facebook')
class FacebookOAuthRedirect(OAuthRedirect):
"""Facebook OAuth2 Redirect"""
def get_additional_parameters(self, source):
return {
'scope': 'email',
}
@MANAGER.source(kind=RequestKind.callback, name='facebook')
class FacebookOAuth2Callback(OAuthCallback):
"""Facebook OAuth2 Callback"""
def get_or_create_user(self, source, access, info):
if 'email' not in info:
raise OAuthClientEmailMissingError()
user = get_user_model()
user_data = {
user.USERNAME_FIELD: info.get('name'),
'email': info.get('email', ''),
'first_name': info.get('name'),
'password': None,
}
fb_user = user_get_or_create(user_model=user, **user_data)
return fb_user

View File

@ -0,0 +1,27 @@
"""GitHub OAuth Views"""
from django.contrib.auth import get_user_model
from passbook.oauth_client.errors import OAuthClientEmailMissingError
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
@MANAGER.source(kind=RequestKind.callback, name='github')
class GitHubOAuth2Callback(OAuthCallback):
"""GitHub OAuth2 Callback"""
def get_or_create_user(self, source, access, info):
if 'email' not in info or info['email'] == '':
raise OAuthClientEmailMissingError()
user = get_user_model()
print(info)
user_data = {
user.USERNAME_FIELD: info.get('login'),
'email': info.get('email', ''),
'first_name': info.get('name'),
'password': None,
}
gh_user = user_get_or_create(user_model=user, **user_data)
return gh_user

View File

@ -0,0 +1,32 @@
"""Google OAuth Views"""
from django.contrib.auth import get_user_model
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
@MANAGER.source(kind=RequestKind.redirect, name='google')
class GoogleOAuthRedirect(OAuthRedirect):
"""Google OAuth2 Redirect"""
def get_additional_parameters(self, source):
return {
'scope': 'email profile',
}
@MANAGER.source(kind=RequestKind.callback, name='google')
class GoogleOAuth2Callback(OAuthCallback):
"""Google OAuth2 Callback"""
def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = {
user.USERNAME_FIELD: info.get('email'),
'email': info.get('email', ''),
'first_name': info.get('name'),
'password': None,
}
google_user = user_get_or_create(user_model=user, **user_data)
return google_user

View File

@ -0,0 +1,43 @@
"""Source type manager"""
from logging import getLogger
from enum import Enum
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
LOGGER = getLogger(__name__)
class RequestKind(Enum):
"""Enum of OAuth Request types"""
callback = 'callback'
redirect = 'redirect'
class SourceTypeManager:
"""Manager to hold all Source types."""
__source_types = {}
def source(self, kind, name):
"""Class decorator to register classes inline."""
def inner_wrapper(cls):
if kind not in self.__source_types:
self.__source_types[kind] = {}
self.__source_types[kind][name] = cls
LOGGER.debug("Registered source '%s' for '%s'", cls.__name__, kind)
return cls
return inner_wrapper
def find(self, source, kind):
"""Find fitting Source Type"""
if kind in self.__source_types:
if source.provider_type in self.__source_types[kind]:
return self.__source_types[kind][source.provider_type]
# Return defaults
if kind == RequestKind.callback:
return OAuthCallback
if kind == RequestKind.redirect:
return OAuthRedirect
raise KeyError
MANAGER = SourceTypeManager()

View File

@ -0,0 +1,71 @@
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
"""Reddit OAuth Views"""
import json
from logging import getLogger
from django.contrib.auth import get_user_model
from requests.auth import HTTPBasicAuth
from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
LOGGER = getLogger(__name__)
@MANAGER.source(kind=RequestKind.redirect, name='reddit')
class RedditOAuthRedirect(OAuthRedirect):
"""Reddit OAuth2 Redirect"""
def get_additional_parameters(self, source):
return {
'scope': 'identity',
'duration': 'permanent',
}
class RedditOAuth2Client(OAuth2Client):
"""Reddit OAuth2 Client"""
def get_access_token(self, request, callback=None, **request_kwargs):
"Fetch access token from callback request."
auth = HTTPBasicAuth(
self.source.consumer_key,
self.source.consumer_secret)
return super(RedditOAuth2Client, self).get_access_token(request, callback, auth=auth)
def get_profile_info(self, raw_token):
"Fetch user profile information."
try:
token = json.loads(raw_token)
headers = {
'Authorization': '%s %s' % (token['token_type'], token['access_token'])
}
response = self.request('get', self.source.profile_url,
token=token['access_token'], headers=headers)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch user profile: %s', exc)
return None
else:
return response.json() or response.text
@MANAGER.source(kind=RequestKind.callback, name='reddit')
class RedditOAuth2Callback(OAuthCallback):
"""Reddit OAuth2 Callback"""
client_class = RedditOAuth2Client
def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = {
user.USERNAME_FIELD: info.get('name'),
'email': None,
'first_name': info.get('name'),
'password': None,
}
reddit_user = user_get_or_create(user_model=user, **user_data)
return reddit_user

View File

@ -0,0 +1,55 @@
"""Supervisr OAuth2 Views"""
import json
from logging import getLogger
from django.contrib.auth import get_user_model
from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
LOGGER = getLogger(__name__)
class SupervisrOAuth2Client(OAuth2Client):
"""Supervisr OAuth2 Client"""
def get_profile_info(self, raw_token):
"Fetch user profile information."
try:
token = json.loads(raw_token)['access_token']
headers = {
'Authorization': 'Bearer:%s' % token
}
response = self.request('get', self.source.profile_url,
token=raw_token, headers=headers)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch user profile: %s', exc)
return None
else:
return response.json() or response.text
@MANAGER.source(kind=RequestKind.callback, name='supervisr')
class SupervisrOAuthCallback(OAuthCallback):
"""Supervisr OAuth2 Callback"""
client_class = SupervisrOAuth2Client
def get_user_id(self, source, info):
return info['pk']
def get_or_create_user(self, source, access, info):
user = get_user_model()
user_data = {
user.USERNAME_FIELD: info.get('username'),
'email': info.get('email', ''),
'first_name': info.get('first_name'),
'password': None,
}
sv_user = user_get_or_create(user_model=user, **user_data)
return sv_user

View File

@ -0,0 +1,50 @@
"""Twitter OAuth Views"""
from logging import getLogger
from django.contrib.auth import get_user_model
from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuthClient
from passbook.oauth_client.errors import OAuthClientEmailMissingError
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
LOGGER = getLogger(__name__)
class TwitterOAuthClient(OAuthClient):
"""Twitter OAuth2 Client"""
def get_profile_info(self, raw_token):
"Fetch user profile information."
try:
response = self.request('get', self.source.profile_url + "?include_email=true",
token=raw_token)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch user profile: %s', exc)
return None
else:
return response.json() or response.text
@MANAGER.source(kind=RequestKind.callback, name='twitter')
class TwitterOAuthCallback(OAuthCallback):
"""Twitter OAuth2 Callback"""
client_class = TwitterOAuthClient
def get_or_create_user(self, source, access, info):
if 'email' not in info:
raise OAuthClientEmailMissingError()
user = get_user_model()
user_data = {
user.USERNAME_FIELD: info.get('screen_name'),
'email': info.get('email', ''),
'first_name': info.get('name'),
'password': None,
}
tw_user = user_get_or_create(user_model=user, **user_data)
return tw_user

View File

@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/></svg>

After

Width:  |  Height:  |  Size: 688 B

View File

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="150"
height="50"
id="svg2"
xml:space="preserve"><defs
id="defs6"><clipPath
id="clipPath20"><path
d="M 0,792 612,792 612,0 0,0 0,792 z"
id="path22" /></clipPath></defs><g
transform="matrix(1.25,0,0,-1.25,-305,520)"
id="g12"><g
id="g16"><g
clip-path="url(#clipPath20)"
id="g18"><g
transform="translate(265.3481,392.7275)"
id="g24"><path
d="m 0,0 c 2.437,0 4.412,-2.383 4.412,-5.324 0,-2.942 -1.975,-5.324 -4.412,-5.324 -2.437,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.437,0 0,0"
id="path26"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(265.3481,392.7275)"
id="g28"><path
d="m 0,0 c 2.437,0 4.412,-2.383 4.412,-5.324 0,-2.942 -1.975,-5.324 -4.412,-5.324 -2.437,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.437,0 0,0 z"
id="path30"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(258.4063,376.5361)"
id="g32"><path
d="m 0,0 c 0.063,0.197 0.108,0.401 0.108,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.108,-0.615 L 0,0 z"
id="path34"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(258.4063,376.5361)"
id="g36"><path
d="m 0,0 c 0.063,0.197 0.108,0.401 0.108,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.108,-0.615 L 0,0 z"
id="path38"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(269.0518,376.5361)"
id="g40"><path
d="m 0,0 c 0.063,0.197 0.107,0.401 0.107,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.107,-0.615 L 0,0 z"
id="path42"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(269.0518,376.5361)"
id="g44"><path
d="m 0,0 c 0.063,0.197 0.107,0.401 0.107,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.107,-0.615 L 0,0 z"
id="path46"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(252.4888,403.9858)"
id="g48"><path
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0"
id="path50"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(252.4888,403.9858)"
id="g52"><path
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0 z"
id="path54"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(274.5913,403.9858)"
id="g56"><path
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0"
id="path58"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(274.5913,403.9858)"
id="g60"><path
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0 z"
id="path62"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(268.3286,413.4697)"
id="g64"><path
d="M 0,0 -5.404,1.272 -7.39,-5.006"
id="path66"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(256.2495,392.7275)"
id="g68"><path
d="m 0,0 c 2.437,0 4.411,-2.383 4.411,-5.324 0,-2.942 -1.974,-5.324 -4.411,-5.324 -2.436,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.436,0 0,0"
id="path70"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(256.2495,392.7275)"
id="g72"><path
d="m 0,0 c 2.437,0 4.411,-2.383 4.411,-5.324 0,-2.942 -1.974,-5.324 -4.411,-5.324 -2.436,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.436,0 0,0 z"
id="path74"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(272.6641,413.0327)"
id="g76"><path
d="m 0,0 c 0,-1.176 -0.954,-2.13 -2.129,-2.13 -1.178,0 -2.131,0.954 -2.131,2.13 0,1.177 0.953,2.13 2.131,2.13 C -0.954,2.13 0,1.177 0,0"
id="path78"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(272.6641,413.0327)"
id="g80"><path
d="m 0,0 c 0,-1.176 -0.954,-2.13 -2.129,-2.13 -1.178,0 -2.131,0.954 -2.131,2.13 0,1.177 0.953,2.13 2.131,2.13 C -0.954,2.13 0,1.177 0,0 z"
id="path82"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(259.2842,376.5332)"
id="g84"><path
d="m 0,0 2.99,0 c 2.656,1.382 4.624,6.34 4.624,12.261 0,6.99 -2.74,12.656 -6.119,12.656 -3.38,0 -6.119,-5.666 -6.119,-12.656 C -4.624,6.34 -2.656,1.382 0,0"
id="path86"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(259.2842,376.5332)"
id="g88"><path
d="m 0,0 2.99,0 c 2.656,1.382 4.624,6.34 4.624,12.261 0,6.99 -2.74,12.656 -6.119,12.656 -3.38,0 -6.119,-5.666 -6.119,-12.656 C -4.624,6.34 -2.656,1.382 0,0 z"
id="path90"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(273.0176,400.2695)"
id="g92"><path
d="m 0,0 c 0,-4.411 -5.479,-7.986 -12.239,-7.986 -6.759,0 -12.238,3.575 -12.238,7.986 0,4.411 5.479,7.987 12.238,7.987 C -5.479,7.987 0,4.411 0,0"
id="path94"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(273.0176,400.2695)"
id="g96"><path
d="m 0,0 c 0,-4.411 -5.479,-7.986 -12.239,-7.986 -6.759,0 -12.238,3.575 -12.238,7.986 0,4.411 5.479,7.987 12.238,7.987 C -5.479,7.987 0,4.411 0,0 z"
id="path98"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(257.5601,376.5361)"
id="g100"><path
d="M 0,0 6.596,0"
id="path102"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(280.6885,392.415)"
id="g104"><path
d="M 0,0 C 0,0.444 0.133,0.797 0.401,1.059 0.669,1.318 0.993,1.45 1.375,1.45 1.764,1.45 2.096,1.322 2.368,1.067 2.642,0.809 2.779,0.455 2.779,0 l 0,-1.441 0.063,0 c 0.421,0.736 0.973,1.401 1.66,1.996 0.687,0.593 1.407,0.907 2.159,0.938 0.422,0 0.794,-0.14 1.12,-0.416 C 8.106,0.8 8.271,0.457 8.271,0.045 8.271,-0.437 8.104,-0.79 7.77,-1.017 7.436,-1.244 6.95,-1.439 6.313,-1.604 5.677,-1.77 5.264,-1.902 5.075,-2.007 3.544,-2.837 2.779,-4.258 2.779,-6.269 l 0,-8.276 c 0,-0.459 -0.129,-0.815 -0.387,-1.067 -0.258,-0.253 -0.577,-0.379 -0.956,-0.379 -0.401,0 -0.741,0.132 -1.019,0.397 C 0.138,-15.327 0,-14.954 0,-14.479 L 0,0 z"
id="path106"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(280.6885,392.415)"
id="g108"><path
d="M 0,0 C 0,0.444 0.133,0.797 0.401,1.059 0.669,1.318 0.993,1.45 1.375,1.45 1.764,1.45 2.096,1.322 2.368,1.067 2.642,0.809 2.779,0.455 2.779,0 l 0,-1.441 0.063,0 c 0.421,0.736 0.973,1.401 1.66,1.996 0.687,0.593 1.407,0.907 2.159,0.938 0.422,0 0.794,-0.14 1.12,-0.416 C 8.106,0.8 8.271,0.457 8.271,0.045 8.271,-0.437 8.104,-0.79 7.77,-1.017 7.436,-1.244 6.95,-1.439 6.313,-1.604 5.677,-1.77 5.264,-1.902 5.075,-2.007 3.544,-2.837 2.779,-4.258 2.779,-6.269 l 0,-8.276 c 0,-0.459 -0.129,-0.815 -0.387,-1.067 -0.258,-0.253 -0.577,-0.379 -0.956,-0.379 -0.401,0 -0.741,0.132 -1.019,0.397 C 0.138,-15.327 0,-14.954 0,-14.479 L 0,0 z"
id="path110"
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(292.5103,386.2129)"
id="g112"><path
d="M 0,0 9.404,0 C 9.374,1.569 8.956,2.826 8.151,3.781 7.346,4.734 6.244,5.228 4.843,5.259 3.417,5.259 2.27,4.796 1.399,3.876 0.529,2.954 0.063,1.661 0,0 m -2.845,-0.894 c 0.063,1.46 0.399,2.843 1.005,4.155 0.607,1.308 1.472,2.368 2.596,3.183 1.123,0.814 2.444,1.23 3.962,1.251 1.473,0 2.782,-0.397 3.927,-1.192 1.143,-0.795 2.026,-1.847 2.648,-3.157 0.623,-1.312 0.934,-2.68 0.934,-4.11 0,-0.916 -0.523,-1.375 -1.569,-1.375 L 0,-2.139 c 0.021,-1.136 0.263,-2.097 0.729,-2.879 0.465,-0.782 1.087,-1.366 1.869,-1.754 0.781,-0.387 1.646,-0.58 2.598,-0.58 1.667,0 3.199,0.589 4.593,1.769 0.48,0.347 0.833,0.519 1.058,0.519 0.314,0 0.557,-0.117 0.733,-0.35 0.175,-0.233 0.262,-0.516 0.262,-0.846 0,-0.357 -0.138,-0.7 -0.414,-1.029 -0.629,-0.667 -1.52,-1.25 -2.673,-1.75 -1.153,-0.5 -2.394,-0.75 -3.722,-0.75 -1.392,0 -2.602,0.261 -3.629,0.787 -1.028,0.523 -1.855,1.222 -2.483,2.088 -0.629,0.867 -1.086,1.805 -1.374,2.812 -0.287,1.004 -0.439,2.022 -0.454,3.051 0.027,0.052 0.043,0.091 0.051,0.113 0.007,0.025 0.011,0.038 0.011,0.044"
id="path114"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(292.5103,386.2129)"
id="g116"><path
d="M 0,0 9.404,0 C 9.374,1.569 8.956,2.826 8.151,3.781 7.346,4.734 6.244,5.228 4.843,5.259 3.417,5.259 2.27,4.796 1.399,3.876 0.529,2.954 0.063,1.661 0,0 z m -2.845,-0.894 c 0.063,1.46 0.399,2.843 1.005,4.155 0.607,1.308 1.472,2.368 2.596,3.183 1.123,0.814 2.444,1.23 3.962,1.251 1.473,0 2.782,-0.397 3.927,-1.192 1.143,-0.795 2.026,-1.847 2.648,-3.157 0.623,-1.312 0.934,-2.68 0.934,-4.11 0,-0.916 -0.523,-1.375 -1.569,-1.375 L 0,-2.139 c 0.021,-1.136 0.263,-2.097 0.729,-2.879 0.465,-0.782 1.087,-1.366 1.869,-1.754 0.781,-0.387 1.646,-0.58 2.598,-0.58 1.667,0 3.199,0.589 4.593,1.769 0.48,0.347 0.833,0.519 1.058,0.519 0.314,0 0.557,-0.117 0.733,-0.35 0.175,-0.233 0.262,-0.516 0.262,-0.846 0,-0.357 -0.138,-0.7 -0.414,-1.029 -0.629,-0.667 -1.52,-1.25 -2.673,-1.75 -1.153,-0.5 -2.394,-0.75 -3.722,-0.75 -1.392,0 -2.602,0.261 -3.629,0.787 -1.028,0.523 -1.855,1.222 -2.483,2.088 -0.629,0.867 -1.086,1.805 -1.374,2.812 -0.287,1.004 -0.439,2.022 -0.454,3.051 0.027,0.052 0.043,0.091 0.051,0.113 0.007,0.025 0.011,0.038 0.011,0.044 z"
id="path118"
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(309.4863,385.2158)"
id="g120"><path
d="m 0,0 c 0,-1.734 0.374,-3.213 1.124,-4.436 0.747,-1.222 1.865,-1.863 3.348,-1.918 1.182,0 2.139,0.305 2.876,0.92 0.73,0.613 1.255,1.393 1.567,2.338 0.316,0.946 0.484,1.956 0.513,3.033 0,1.239 -0.2,2.34 -0.603,3.304 C 8.425,4.204 7.866,4.949 7.153,5.472 6.438,5.994 5.631,6.256 4.729,6.256 3.665,6.256 2.783,5.941 2.078,5.32 1.374,4.695 0.857,3.909 0.529,2.963 0.203,2.018 0.027,1.029 0,0 m 9.107,6.389 0,8.415 c 0,0.44 0.137,0.784 0.41,1.035 0.274,0.252 0.598,0.377 0.968,0.377 0.413,0 0.755,-0.125 1.031,-0.376 0.274,-0.251 0.412,-0.616 0.412,-1.096 l 0,-22.088 c 0,-0.455 -0.142,-0.808 -0.424,-1.065 -0.281,-0.256 -0.622,-0.383 -1.019,-0.383 -0.353,0 -0.673,0.127 -0.956,0.382 -0.281,0.255 -0.422,0.589 -0.422,0.999 l 0,0.922 -0.065,0 C 8.739,-7.104 8.244,-7.631 7.554,-8.075 6.865,-8.518 5.934,-8.756 4.761,-8.792 c -1.505,0 -2.828,0.367 -3.963,1.104 -1.137,0.735 -2.017,1.76 -2.645,3.077 -0.624,1.314 -0.948,2.806 -0.974,4.478 0,1.196 0.174,2.333 0.524,3.413 0.351,1.082 0.846,2.025 1.484,2.833 0.638,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.933,0.686 1.917,0 3.462,-0.768 4.627,-2.303 l 0.065,0 z"
id="path122"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(309.4863,385.2158)"
id="g124"><path
d="m 0,0 c 0,-1.734 0.374,-3.213 1.124,-4.436 0.747,-1.222 1.865,-1.863 3.348,-1.918 1.182,0 2.139,0.305 2.876,0.92 0.73,0.613 1.255,1.393 1.567,2.338 0.316,0.946 0.484,1.956 0.513,3.033 0,1.239 -0.2,2.34 -0.603,3.304 C 8.425,4.204 7.866,4.949 7.153,5.472 6.438,5.994 5.631,6.256 4.729,6.256 3.665,6.256 2.783,5.941 2.078,5.32 1.374,4.695 0.857,3.909 0.529,2.963 0.203,2.018 0.027,1.029 0,0 z m 9.107,6.389 0,8.415 c 0,0.44 0.137,0.784 0.41,1.035 0.274,0.252 0.598,0.377 0.968,0.377 0.413,0 0.755,-0.125 1.031,-0.376 0.274,-0.251 0.412,-0.616 0.412,-1.096 l 0,-22.088 c 0,-0.455 -0.142,-0.808 -0.424,-1.065 -0.281,-0.256 -0.622,-0.383 -1.019,-0.383 -0.353,0 -0.673,0.127 -0.956,0.382 -0.281,0.255 -0.422,0.589 -0.422,0.999 l 0,0.922 -0.065,0 C 8.739,-7.104 8.244,-7.631 7.554,-8.075 6.865,-8.518 5.934,-8.756 4.761,-8.792 c -1.505,0 -2.828,0.367 -3.963,1.104 -1.137,0.735 -2.017,1.76 -2.645,3.077 -0.624,1.314 -0.948,2.806 -0.974,4.478 0,1.196 0.174,2.333 0.524,3.413 0.351,1.082 0.846,2.025 1.484,2.833 0.638,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.933,0.686 1.917,0 3.462,-0.768 4.627,-2.303 l 0.065,0 z"
id="path126"
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(327.3467,385.2158)"
id="g128"><path
d="m 0,0 c 0,-1.734 0.373,-3.213 1.123,-4.436 0.749,-1.222 1.865,-1.863 3.349,-1.918 1.182,0 2.141,0.305 2.872,0.92 0.735,0.613 1.258,1.393 1.571,2.338 0.314,0.946 0.485,1.956 0.512,3.033 0,1.239 -0.2,2.34 -0.601,3.304 C 8.426,4.204 7.867,4.949 7.153,5.472 6.438,5.994 5.63,6.256 4.729,6.256 3.665,6.256 2.781,5.941 2.077,5.32 1.372,4.695 0.858,3.909 0.53,2.963 0.202,2.018 0.026,1.029 0,0 m 9.104,6.389 0,8.415 c 0,0.44 0.138,0.784 0.414,1.035 0.274,0.252 0.595,0.377 0.966,0.377 0.412,0 0.758,-0.125 1.031,-0.376 0.275,-0.251 0.411,-0.616 0.411,-1.096 l 0,-22.088 c 0,-0.455 -0.141,-0.808 -0.422,-1.065 -0.282,-0.256 -0.624,-0.383 -1.02,-0.383 -0.354,0 -0.673,0.127 -0.955,0.382 -0.282,0.255 -0.425,0.589 -0.425,0.999 l 0,0.922 -0.062,0 C 8.738,-7.104 8.243,-7.631 7.554,-8.075 6.866,-8.518 5.935,-8.756 4.761,-8.792 c -1.506,0 -2.827,0.367 -3.964,1.104 -1.138,0.735 -2.018,1.76 -2.643,3.077 -0.624,1.314 -0.95,2.806 -0.975,4.478 0,1.196 0.175,2.333 0.525,3.413 0.351,1.082 0.844,2.025 1.482,2.833 0.639,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.932,0.686 1.918,0 3.462,-0.768 4.629,-2.303 l 0.062,0 z"
id="path130"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(327.3467,385.2158)"
id="g132"><path
d="m 0,0 c 0,-1.734 0.373,-3.213 1.123,-4.436 0.749,-1.222 1.865,-1.863 3.349,-1.918 1.182,0 2.141,0.305 2.872,0.92 0.735,0.613 1.258,1.393 1.571,2.338 0.314,0.946 0.485,1.956 0.512,3.033 0,1.239 -0.2,2.34 -0.601,3.304 C 8.426,4.204 7.867,4.949 7.153,5.472 6.438,5.994 5.63,6.256 4.729,6.256 3.665,6.256 2.781,5.941 2.077,5.32 1.372,4.695 0.858,3.909 0.53,2.963 0.202,2.018 0.026,1.029 0,0 z m 9.104,6.389 0,8.415 c 0,0.44 0.138,0.784 0.414,1.035 0.274,0.252 0.595,0.377 0.966,0.377 0.412,0 0.758,-0.125 1.031,-0.376 0.275,-0.251 0.411,-0.616 0.411,-1.096 l 0,-22.088 c 0,-0.455 -0.141,-0.808 -0.422,-1.065 -0.282,-0.256 -0.624,-0.383 -1.02,-0.383 -0.354,0 -0.673,0.127 -0.955,0.382 -0.282,0.255 -0.425,0.589 -0.425,0.999 l 0,0.922 -0.062,0 C 8.738,-7.104 8.243,-7.631 7.554,-8.075 6.866,-8.518 5.935,-8.756 4.761,-8.792 c -1.506,0 -2.827,0.367 -3.964,1.104 -1.138,0.735 -2.018,1.76 -2.643,3.077 -0.624,1.314 -0.95,2.806 -0.975,4.478 0,1.196 0.175,2.333 0.525,3.413 0.351,1.082 0.844,2.025 1.482,2.833 0.639,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.932,0.686 1.918,0 3.462,-0.768 4.629,-2.303 l 0.062,0 z"
id="path134"
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(351.7471,391.2998)"
id="g136"><path
d="m 0,0 -1.245,0 c -0.4,0 -0.721,0.114 -0.963,0.341 -0.24,0.227 -0.379,0.509 -0.421,0.843 0.083,0.836 0.545,1.254 1.384,1.254 l 1.245,0 0,3.814 c 0,0.416 0.134,0.74 0.4,0.97 0.267,0.228 0.591,0.343 0.978,0.343 0.41,0 0.755,-0.116 1.031,-0.348 C 2.684,6.984 2.824,6.64 2.824,6.182 l 0,-3.744 1.243,0 C 4.496,2.438 4.822,2.322 5.046,2.093 5.271,1.863 5.384,1.562 5.384,1.184 5.384,0.864 5.271,0.588 5.055,0.354 4.834,0.119 4.551,0 4.201,0 l -1.377,0 0,-13.488 c 0,-0.429 -0.149,-0.768 -0.443,-1.017 -0.293,-0.248 -0.651,-0.371 -1.065,-0.371 -0.37,0 -0.685,0.123 -0.935,0.371 C 0.129,-14.256 0,-13.896 0,-13.427 L 0,0 z"
id="path138"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(351.7471,391.2998)"
id="g140"><path
d="m 0,0 -1.245,0 c -0.4,0 -0.721,0.114 -0.963,0.341 -0.24,0.227 -0.379,0.509 -0.421,0.843 0.083,0.836 0.545,1.254 1.384,1.254 l 1.245,0 0,3.814 c 0,0.416 0.134,0.74 0.4,0.97 0.267,0.228 0.591,0.343 0.978,0.343 0.41,0 0.755,-0.116 1.031,-0.348 C 2.684,6.984 2.824,6.64 2.824,6.182 l 0,-3.744 1.243,0 C 4.496,2.438 4.822,2.322 5.046,2.093 5.271,1.863 5.384,1.562 5.384,1.184 5.384,0.864 5.271,0.588 5.055,0.354 4.834,0.119 4.551,0 4.201,0 l -1.377,0 0,-13.488 c 0,-0.429 -0.149,-0.768 -0.443,-1.017 -0.293,-0.248 -0.651,-0.371 -1.065,-0.371 -0.37,0 -0.685,0.123 -0.935,0.371 C 0.129,-14.256 0,-13.896 0,-13.427 L 0,0 z"
id="path142"
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(346.8604,398.5088)"
id="g144"><path
d="m 0,0 c 0,-1.275 -1.034,-2.309 -2.309,-2.309 -1.275,0 -2.308,1.034 -2.308,2.309 0,1.275 1.033,2.309 2.308,2.309 C -1.034,2.309 0,1.275 0,0"
id="path146"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(346.8604,398.5088)"
id="g148"><path
d="m 0,0 c 0,-1.275 -1.034,-2.309 -2.309,-2.309 -1.275,0 -2.308,1.034 -2.308,2.309 0,1.275 1.033,2.309 2.308,2.309 C -1.034,2.309 0,1.275 0,0 z"
id="path150"
style="fill:none;stroke:#000000;stroke-width:1.60000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(344.5557,392.5674)"
id="g152"><path
d="M 0,0 0,-14.792"
id="path154"
style="fill:none;stroke:#000000;stroke-width:3.3599999;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(257.9604,401.8286)"
id="g156"><path
d="m 0,0 c 0,-0.79 -0.641,-1.43 -1.43,-1.43 -0.791,0 -1.431,0.64 -1.431,1.43 0,0.791 0.64,1.431 1.431,1.431 C -0.641,1.431 0,0.791 0,0"
id="path158"
style="fill:#ff4500;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(257.9604,401.8286)"
id="g160"><path
d="m 0,0 c 0,-0.79 -0.641,-1.43 -1.43,-1.43 -0.791,0 -1.431,0.64 -1.431,1.43 0,0.791 0.64,1.431 1.431,1.431 C -0.641,1.431 0,0.791 0,0 z"
id="path162"
style="fill:none;stroke:#ff4500;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(266.583,401.8286)"
id="g164"><path
d="m 0,0 c 0,-0.79 -0.64,-1.43 -1.43,-1.43 -0.789,0 -1.43,0.64 -1.43,1.43 0,0.791 0.641,1.431 1.43,1.431 C -0.64,1.431 0,0.791 0,0"
id="path166"
style="fill:#ff4500;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
transform="translate(266.583,401.8286)"
id="g168"><path
d="m 0,0 c 0,-0.79 -0.64,-1.43 -1.43,-1.43 -0.789,0 -1.43,0.64 -1.43,1.43 0,0.791 0.641,1.431 1.43,1.431 C -0.64,1.431 0,0.791 0,0 z"
id="path170"
style="fill:none;stroke:#ff4500;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(256.6104,396.5933)"
id="g172"><path
d="M 0,0 C 1.066,-1.066 2.786,-1.271 4.212,-1.271"
id="path174"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
transform="translate(265.0654,396.5933)"
id="g176"><path
d="M 0,0 C -1.067,-1.066 -2.785,-1.271 -4.212,-1.271"
id="path178"
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,6 @@
{% load supervisr_oauth_client %}
{% any_provider as enabled %}
{% if enabled %}
<div class="btn-group btn-primary btn-block">
{% endif %}

View File

@ -0,0 +1,6 @@
{% load supervisr_oauth_client %}
{% provider_exists 'facebook' as facebook_enabled %}
{% if facebook_enabled %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='facebook' %}" class="btn" style="background-color:#4267b2;color:white;margin-top:10px;width:100%;"><i class="fa fa-facebook-official" aria-hidden="true"></i></a>
{% endif %}

View File

@ -0,0 +1,6 @@
{% load supervisr_oauth_client %}
{% provider_exists 'twitter' as twitter_enabled %}
{% if twitter_enabled %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='twitter' %}" class="btn" style="background-color:#55ACEE;color:white;margin-top:10px;width:100%;"><i class="fa fa-twitter" aria-hidden="true"></i></a>
{% endif %}

View File

@ -0,0 +1,7 @@
{% load supervisr_oauth_client %}
{% load static %}
{% provider_exists 'google' as google_enabled %}
{% if google_enabled %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='google' %}" class="btn" style="background-color:white;color:black;margin-top:10px;width:100%;"><img src="{% static 'img/google.svg' %}" style="height:12px"></a>
{% endif %}

View File

@ -0,0 +1,6 @@
{% load supervisr_oauth_client %}
{% provider_exists 'github' as github_enabled %}
{% if github_enabled %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='github' %}" class="btn" style="background-color:#444444;color:white;margin-top:10px;width:100%;"><i class="fa fa-github" aria-hidden="true"></i></a>
{% endif %}

View File

@ -0,0 +1,7 @@
{% load supervisr_oauth_client %}
{% load static %}
{% provider_exists 'discord' as discord_enabled %}
{% if discord_enabled %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='discord' %}" class="btn" style="background-color:#2C2F33;color:white;margin-top:10px;width:100%;"><img src="{% static 'img/discord.svg' %}" style="height:12px"></a>
{% endif %}

View File

@ -0,0 +1,7 @@
{% load supervisr_oauth_client %}
{% load static %}
{% provider_exists 'reddit' as reddit_enabled %}
{% if reddit_enabled %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='reddit' %}" class="btn" style="background-color:#ff4500;color:white;margin-top:10px;width:100%;"><img src="{% static 'img/reddit.svg' %}" style="height:20px;margin-top:-5px;"></a>
{% endif %}

View File

@ -0,0 +1,6 @@
{% load supervisr_oauth_client %}
{% any_provider as enabled %}
{% if enabled %}
</div>
{% endif %}

View File

@ -0,0 +1,54 @@
{% extends "user/base.html" %}
{% load supervisr_utils %}
{% load i18n %}
{% block title %}
{% title "Overview" %}
{% endblock %}
{% block content %}
<h1><clr-icon shape="connect" size="48"></clr-icon>{% trans "OAuth2" %}</h1>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
{% trans "Connected Accounts" %}
</div>
<div class="card-footer">
{% if provider_state %}
<table class="table">
<thead>
<th>
<th>{% trans 'Provider' %}</th>
<th>{% trans 'Status' %}</th>
<th>{% trans 'Action' %}</th>
<th>{% trans 'ID' %}</th>
</th>
</thead>
<tbody>
{% for data in provider_state %}
<tr>
<td></td>
<td>{% trans data.provider.ui_name %}</td>
<td>{{ data.state|yesno:"Connected,Not Connected" }}</td>
<td>
{% if data.state == False %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider=data.provider.name %}">Connect</a>
{% else %}
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-disconnect' provider=data.provider.name %}">Disconnect</a>
{% endif %}
</td>
<td>{{ data.aas.first.identifier }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>{% trans "No Providers configured!" %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,49 @@
"""passbook oauth_client urls"""
from django.urls import path
from passbook.oauth_client.source_types.manager import RequestKind
# from passbook.oauth_client.views import core, settings
from passbook.oauth_client.views import dispatcher
# from passbook.oauth_client.views.providers import (discord, facebook, github,
# google, reddit, supervisr,
# twitter)
urlpatterns = [
# # Supervisr
# url(r'^callback/(?P<provider>supervisr)/$',
# supervisr.SupervisrOAuthCallback.as_view(), name='oauth-client-callback'),
# # Twitter
# url(r'^callback/(?P<provider>twitter)/$',
# twitter.TwitterOAuthCallback.as_view(), name='oauth-client-callback'),
# # GitHub
# url(r'^callback/(?P<provider>github)/$',
# github.GitHubOAuth2Callback.as_view(), name='oauth-client-callback'),
# # Facebook
# url(r'^callback/(?P<provider>facebook)/$',
# facebook.FacebookOAuth2Callback.as_view(), name='oauth-client-callback'),
# url(r'^login/(?P<provider>facebook)/$',
# facebook.FacebookOAuthRedirect.as_view(), name='oauth-client-login'),
# # Discord
# url(r'^callback/(?P<provider>discord)/$',
# discord.DiscordOAuth2Callback.as_view(), name='oauth-client-callback'),
# url(r'^login/(?P<provider>discord)/$',
# discord.DiscordOAuthRedirect.as_view(), name='oauth-client-login'),
# # Reddit
# url(r'^callback/(?P<provider>reddit)/$',
# reddit.RedditOAuth2Callback.as_view(), name='oauth-client-callback'),
# url(r'^login/(?P<provider>reddit)/$',
# reddit.RedditOAuthRedirect.as_view(), name='oauth-client-login'),
# # Google
# url(r'^callback/(?P<provider>google)/$',
# google.GoogleOAuth2Callback.as_view(), name='oauth-client-callback'),
# url(r'^login/(?P<provider>google)/$',
# google.GoogleOAuthRedirect.as_view(), name='oauth-client-login'),
path('login/<slug:source_slug>/', dispatcher.DispatcherView.as_view(
kind=RequestKind.redirect), name='oauth-client-login'),
path('callback/<slug:source_slug>/', dispatcher.DispatcherView.as_view(
kind=RequestKind.callback), name='oauth-client-callback'),
# path('disconnect/<slug:source_slug>/', core.disconnect,
# name='oauth-client-disconnect'),
]

View File

@ -0,0 +1,17 @@
"""
OAuth Client User Creation Utils
"""
from django.contrib.auth import get_user_model
from django.db.utils import IntegrityError
def user_get_or_create(user_model=None, **kwargs):
"""Create user or return existing user"""
if user_model is None:
user_model = get_user_model()
try:
new_user = user_model.objects.create_user(**kwargs)
except IntegrityError:
new_user = user_model.objects.get(username=kwargs['username'])
return new_user

View File

View File

@ -0,0 +1,256 @@
"""Core Oauth Views"""
import base64
import hashlib
from logging import getLogger
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate, get_user_model, login
from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.encoding import force_text, smart_bytes
from django.utils.translation import ugettext as _
from django.views.generic import RedirectView, View
from passbook.oauth_client.clients import get_client
from passbook.oauth_client.errors import (OAuthClientEmailMissingError,
OAuthClientError)
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
LOGGER = getLogger(__name__)
# pylint: disable=too-few-public-methods, too-many-locals
class OAuthClientMixin:
"Mixin for getting OAuth client for a source."
client_class = None
def get_client(self, source):
"Get instance of the OAuth client for this source."
if self.client_class is not None:
# pylint: disable=not-callable
return self.client_class(source)
return get_client(source)
class OAuthRedirect(OAuthClientMixin, RedirectView):
"Redirect user to OAuth source to enable access."
permanent = False
params = None
# pylint: disable=unused-argument
def get_additional_parameters(self, source):
"Return additional redirect parameters for this source."
return self.params or {}
def get_callback_url(self, source):
"Return the callback url for this source."
return reverse('oauth-client-callback', kwargs={'source_slug': source.slug})
def get_redirect_url(self, **kwargs):
"Build redirect url for a given source."
slug = kwargs.get('source_slug', '')
try:
source = OAuthSource.objects.get(slug=slug)
except OAuthSource.DoesNotExist:
raise Http404("Unknown OAuth source '%s'." % slug)
else:
if not source.enabled:
raise Http404('source %s is not enabled.' % slug)
client = self.get_client(source)
callback = self.get_callback_url(source)
params = self.get_additional_parameters(source)
return client.get_redirect_url(self.request, callback=callback, parameters=params)
class OAuthCallback(OAuthClientMixin, View):
"Base OAuth callback view."
source_id = None
source = None
# pylint: disable=too-many-return-statements
def get(self, request, *args, **kwargs):
"""View Get handler"""
slug = kwargs.get('source_slug', '')
try:
self.source = OAuthSource.objects.get(slug=slug)
except OAuthSource.DoesNotExist:
raise Http404("Unknown OAuth source '%s'." % slug)
else:
if not self.source.enabled:
raise Http404('source %s is not enabled.' % slug)
client = self.get_client(self.source)
callback = self.get_callback_url(self.source)
# Fetch access token
raw_token = client.get_access_token(self.request, callback=callback)
if raw_token is None:
return self.handle_login_failure(self.source, "Could not retrieve token.")
# Fetch profile info
info = client.get_profile_info(raw_token)
if info is None:
return self.handle_login_failure(self.source, "Could not retrieve profile.")
identifier = self.get_user_id(self.source, info)
if identifier is None:
return self.handle_login_failure(self.source, "Could not determine id.")
# Get or create access record
defaults = {
'access_token': raw_token,
}
existing = UserOAuthSourceConnection.objects.filter(
source=self.source, identifier=identifier)
if existing.exists():
connection = existing.first()
connection.access_token = raw_token
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(**defaults)
else:
connection = UserOAuthSourceConnection(
source=self.source,
identifier=identifier,
access_token=raw_token
)
user = authenticate(source=self.source, identifier=identifier, request=request)
if user is None:
try:
return self.handle_new_user(self.source, connection, info)
except OAuthClientEmailMissingError as exc:
return render(request, 'common/error.html', {
'code': 500,
'exc_message': _("source %(name)s didn't provide an E-Mail address." % {
'name': self.source.name
}),
})
except OAuthClientError as exc:
return render(request, 'common/error.html', {
'code': 500,
'exc_message': str(exc),
})
return self.handle_existing_user(self.source, user, connection, info)
# pylint: disable=unused-argument
def get_callback_url(self, source):
"Return callback url if different than the current url."
return False
# pylint: disable=unused-argument
def get_error_redirect(self, source, reason):
"Return url to redirect on login failure."
return settings.LOGIN_URL
# pylint: disable=unused-argument
def get_login_redirect(self, source, user, access, new=False):
"Return url to redirect authenticated users."
return 'overview'
# pylint: disable=unused-argument
def get_or_create_user(self, source, access, info):
"Create a shell auth.User."
digest = hashlib.sha1(smart_bytes(access)).digest()
# Base 64 encode to get below 30 characters
# Removed padding characters
username = force_text(base64.urlsafe_b64encode(digest)).replace('=', '')
# pylint: disable=invalid-name
User = get_user_model() # noqa
kwargs = {
User.USERNAME_FIELD: username,
'email': '',
'password': None
}
return User.objects.create_user(**kwargs)
# pylint: disable=unused-argument
def get_user_id(self, source, info):
"Return unique identifier from the profile info."
id_key = self.source_id or 'id'
result = info
try:
for key in id_key.split('.'):
result = result[key]
return result
except KeyError:
return None
# pylint: disable=unused-argument
def handle_existing_user(self, source, user, access, info):
"Login user and redirect."
login(self.request, user)
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name
}))
return redirect(self.get_login_redirect(source, user, access))
def handle_login_failure(self, source, reason):
"Message user and redirect on error."
LOGGER.warning('Authentication Failure: %s', reason)
messages.error(self.request, _('Authentication Failed.'))
return redirect(self.get_error_redirect(source, reason))
def handle_new_user(self, source, access, info):
"Create a shell auth.User and redirect."
if self.request.user.is_authenticated: # pylint: disable=no-else-return
# there's already a user logged in, just link them up
user = self.request.user
access.user = user
access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
# Event.create(
# user=user,
# message=_("Linked user with OAuth source %s" % self.source.name),
# request=self.request,
# hidden=True,
# current=False)
messages.success(self.request, _("Successfully linked %(source)s!" % {
'source': self.source.name
}))
return redirect(reverse('user_settings'))
else:
user = self.get_or_create_user(source, access, info)
access.user = user
access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
user = authenticate(source=access.source,
identifier=access.identifier, request=self.request)
login(self.request, user)
# Event.create(
# user=user,
# message=_("Authenticated user with OAuth source %s" % self.source.name),
# request=self.request,
# hidden=True,
# current=False)
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name
}))
return redirect(self.get_login_redirect(source, user, access, True))
@login_required
def disconnect(request: HttpRequest, source: str) -> HttpResponse:
"""Delete connection with source"""
source = OAuthSource.objects.filter(name=source)
if not source.exists():
raise Http404
r_source = source.first()
aas = UserOAuthSourceConnection.objects.filter(source=r_source, user=request.user)
if not aas.exists():
raise Http404
r_aas = aas.first()
if request.method == 'POST' and 'confirmdelete' in request.POST:
# User confirmed deletion
r_aas.delete()
messages.success(request, _('Connection successfully deleted'))
return redirect(reverse('user_settings'))
return render(request, 'generic/delete.html', {
'object': 'OAuth Connection with %s' % r_source.name,
'delete_url': reverse('oauth-client-disconnect', kwargs={
'source': r_source.name,
})
})

View File

@ -0,0 +1,22 @@
"""Dispatch OAuth views to respective views"""
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.views import View
from passbook.oauth_client.models import OAuthSource
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
class DispatcherView(View):
"""Dispatch OAuth Redirect/Callback views to their proper class based on URL parameters"""
kind = ''
def dispatch(self, *args, **kwargs):
"""Find Source by slug and forward request"""
slug = kwargs.get('source_slug', None)
if not slug:
raise Http404
source = get_object_or_404(OAuthSource, slug=slug)
view = MANAGER.find(source, kind=RequestKind(self.kind))
return view.as_view()(*args, **kwargs)