modify gooyal_oauth to get setting
This commit is contained in:
parent
07184d6be0
commit
b3ac32db5b
9 changed files with 218 additions and 50 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django_mysql.models
|
from django.db.models import JSONField
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
|
||||||
('scope', models.TextField(blank=True)),
|
('scope', models.TextField(blank=True)),
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated', models.DateTimeField(auto_now=True)),
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
('detail', django_mysql.models.JSONField(blank=True, default=dict, null=True)),
|
('detail', JSONField(blank=True, default=dict, null=True)),
|
||||||
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_tokens', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_tokens', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gooyal_oauth2_accesstoken', to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gooyal_oauth2_accesstoken', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
18
apps/gooyal_oauth2/migrations/0007_auto_20200926_0855.py
Normal file
18
apps/gooyal_oauth2/migrations/0007_auto_20200926_0855.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1.1 on 2020-09-26 05:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gooyal_oauth2', '0006_auto_20200825_0615'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accesstoken',
|
||||||
|
name='detail',
|
||||||
|
field=models.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,7 @@ Django models for the gooyal-restrict-scopes package.
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_mysql.models import JSONField
|
from django.db.models import JSONField
|
||||||
from oauth2_provider.models import AbstractApplication, AbstractAccessToken, AbstractGrant, AbstractRefreshToken
|
from oauth2_provider.models import AbstractApplication, AbstractAccessToken, AbstractGrant, AbstractRefreshToken
|
||||||
from oauth2_provider.scopes import get_scopes_backend
|
from oauth2_provider.scopes import get_scopes_backend
|
||||||
from oauth2_provider.settings import oauth2_settings
|
from oauth2_provider.settings import oauth2_settings
|
||||||
|
|
|
||||||
19
apps/gooyal_oauth2/rest_framework.py
Normal file
19
apps/gooyal_oauth2/rest_framework.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements, OAuth2Authentication
|
||||||
|
from rest_framework.permissions import (
|
||||||
|
IsAuthenticated
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger("oauth2_provider")
|
||||||
|
|
||||||
|
|
||||||
|
class IsAuthenticatedOrTokenMatchesOASRequirements(TokenMatchesOASRequirements):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
is_authenticated = IsAuthenticated().has_permission(request, view)
|
||||||
|
oauth2authenticated = False
|
||||||
|
if is_authenticated:
|
||||||
|
oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication)
|
||||||
|
|
||||||
|
token_has_scope = TokenMatchesOASRequirements()
|
||||||
|
return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view)
|
||||||
10
apps/gooyal_oauth2/settings.py
Normal file
10
apps/gooyal_oauth2/settings.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from oauth2_provider.settings import OAuth2ProviderSettings, USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY
|
||||||
|
|
||||||
|
GOOYAL_DEFAULTS = {
|
||||||
|
"RESOURCE_SERVER_CLIENT_ID": None,
|
||||||
|
"RESOURCE_SERVER_CLIENT_SECRET": None
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULTS.update(GOOYAL_DEFAULTS)
|
||||||
|
|
||||||
|
oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY)
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
from oauth2_provider.oauth2_validators import OAuth2Validator
|
import base64
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import service_clients
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from oauth2_provider.settings import oauth2_settings
|
from django.utils.timezone import make_aware
|
||||||
|
from oauth2_provider.models import get_access_token_model
|
||||||
|
from oauth2_provider.oauth2_validators import OAuth2Validator as BaseOAuth2Validator
|
||||||
|
from .settings import oauth2_settings
|
||||||
|
|
||||||
from apps.gooyal_oauth2.models import Resource
|
log = logging.getLogger("oauth2_provider")
|
||||||
|
|
||||||
|
AccessTokenModel = get_access_token_model()
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
USER_MODEL = get_user_model()
|
USER_MODEL = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class MultiGatewayOAuth2Validator(OAuth2Validator): # pylint: disable=w0223
|
class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
||||||
def validate_user(self, username, password, client, request, *args, **kwargs):
|
def validate_user(self, username, password, client, request, *args, **kwargs):
|
||||||
auth_fields = getattr(request, 'auth_fields', 'username:password').split(':')
|
auth_fields = getattr(request, 'auth_fields', 'username:password').split(':')
|
||||||
|
|
||||||
|
|
@ -26,7 +37,7 @@ class MultiGatewayOAuth2Validator(OAuth2Validator): # pylint: disable=w0223
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
user = USER_MODEL.objects.filter(**{user_field:username}).first()
|
user = USER_MODEL.objects.filter(**{user_field: username}).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
|
|
@ -40,42 +51,150 @@ class MultiGatewayOAuth2Validator(OAuth2Validator): # pylint: disable=w0223
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _get_token_from_gooyal_authentication_server(
|
||||||
|
self, token, introspection_url, introspection_token, introspection_credentials, introspection_client_id,
|
||||||
|
introspection_client_secret
|
||||||
|
):
|
||||||
|
"""Use external introspection endpoint to "crack open" the token.
|
||||||
|
:param introspection_url: introspection endpoint URL
|
||||||
|
:param introspection_token: Bearer token
|
||||||
|
:param introspection_credentials: Basic Auth credentials (id,secret)
|
||||||
|
:return: :class:`models.AccessToken`
|
||||||
|
|
||||||
# class IntrospectOAuth2Validator(OAuth2Validator):
|
Some RFC 7662 implementations (including this one) use a Bearer token while others use Basic
|
||||||
# def validate_bearer_token(self, token, scopes, request):
|
Auth. Depending on the external AS's implementation, provide either the introspection_token
|
||||||
# """
|
or the introspection_credentials.
|
||||||
# When users try to access resources, check that provided token is valid
|
|
||||||
# """
|
If the resulting access_token identifies a username (e.g. Authorization Code grant), add
|
||||||
# if not token:
|
that user to the UserModel. Also cache the access_token up until its expiry time or a
|
||||||
# return False
|
configured maximum time.
|
||||||
#
|
|
||||||
# introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL
|
"""
|
||||||
# introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN
|
|
||||||
# introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS
|
headers = None
|
||||||
#
|
if introspection_token:
|
||||||
# try:
|
headers = {"Authorization": "Bearer {}".format(introspection_token)}
|
||||||
# access_token = AccessToken.objects.select_related("application", "user").get(token=token)
|
try:
|
||||||
# except AccessToken.DoesNotExist:
|
response = requests.post(
|
||||||
# access_token = None
|
introspection_url,
|
||||||
#
|
data={"token": token}, headers=headers
|
||||||
# # if there is no token or it's invalid then introspect the token if there's an external OAuth server
|
)
|
||||||
# if not access_token or not access_token.is_valid(scopes):
|
except requests.exceptions.RequestException:
|
||||||
# if introspection_url and (introspection_token or introspection_credentials):
|
log.exception("Introspection: Failed POST to %r in token lookup", introspection_url)
|
||||||
# access_token = self._get_token_from_authentication_server(
|
return None
|
||||||
# token,
|
|
||||||
# introspection_url,
|
elif introspection_credentials:
|
||||||
# introspection_token,
|
client_id = introspection_credentials[0].encode("utf-8")
|
||||||
# introspection_credentials
|
client_secret = introspection_credentials[1].encode("utf-8")
|
||||||
# )
|
basic_auth = base64.b64encode(client_id + b":" + client_secret)
|
||||||
#
|
headers = {"Authorization": "Basic {}".format(basic_auth.decode("utf-8"))}
|
||||||
# if access_token and access_token.is_valid(scopes):
|
try:
|
||||||
# request.client = access_token.application
|
response = requests.post(
|
||||||
# request.user = access_token.user or (access_token.application and access_token.application.user)
|
introspection_url,
|
||||||
# request.scopes = scopes
|
data={"token": token}, headers=headers
|
||||||
#
|
)
|
||||||
# # this is needed by django rest framework
|
except requests.exceptions.RequestException:
|
||||||
# request.access_token = access_token
|
log.exception("Introspection: Failed POST to %r in token lookup", introspection_url)
|
||||||
# return True
|
return None
|
||||||
# else:
|
|
||||||
# self._set_oauth2_error_on_request(request, access_token, scopes)
|
elif introspection_client_id and introspection_client_secret:
|
||||||
# return False
|
client = service_clients.Client(client_id=introspection_client_id,
|
||||||
|
client_secret=introspection_client_secret,
|
||||||
|
grant_type=service_clients.AccountsClient.GRANT_CLIENT_CREDENTIALS,
|
||||||
|
scopes=['introspection'])
|
||||||
|
data = {"token": token}
|
||||||
|
response = client.request(url=introspection_url, method='post', data=data,
|
||||||
|
required_scopes=['introspection'], login_required=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = response.json()
|
||||||
|
except ValueError:
|
||||||
|
log.exception("Introspection: Failed to parse response as json")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "active" in content and content["active"] is True:
|
||||||
|
if "username" in content:
|
||||||
|
headers = {"Authorization": "Bearer {}".format(token)}
|
||||||
|
user_account = requests.get(f'{settings.BASE_ACCOUNTS_URL}/account', headers=headers).json()
|
||||||
|
user_phone_number = user_account['phone_number']
|
||||||
|
user = UserModel.objects.get(
|
||||||
|
**{'phone_number': user_phone_number}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
user = None
|
||||||
|
|
||||||
|
max_caching_time = datetime.now() + timedelta(
|
||||||
|
seconds=oauth2_settings.RESOURCE_SERVER_TOKEN_CACHING_SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
|
if "exp" in content:
|
||||||
|
expires = datetime.utcfromtimestamp(content["exp"])
|
||||||
|
if expires > max_caching_time:
|
||||||
|
expires = max_caching_time
|
||||||
|
else:
|
||||||
|
expires = max_caching_time
|
||||||
|
|
||||||
|
scope = content.get("scope", "")
|
||||||
|
expires = make_aware(expires)
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = AccessTokenModel.objects.select_related("application", "user").get(token=token)
|
||||||
|
except AccessTokenModel.DoesNotExist:
|
||||||
|
access_token = AccessTokenModel.objects.create(
|
||||||
|
user=user,
|
||||||
|
token=token,
|
||||||
|
application=None,
|
||||||
|
scope=scope,
|
||||||
|
expires=expires,
|
||||||
|
detail=content
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
access_token.expires = expires
|
||||||
|
access_token.scope = scope
|
||||||
|
access_token.detail = content
|
||||||
|
access_token.save()
|
||||||
|
|
||||||
|
return access_token
|
||||||
|
|
||||||
|
def validate_bearer_token(self, token, scopes, request):
|
||||||
|
"""
|
||||||
|
When users try to access resources, check that provided token is valid
|
||||||
|
"""
|
||||||
|
if not token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL
|
||||||
|
introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN
|
||||||
|
introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS
|
||||||
|
introspection_client_id = oauth2_settings.RESOURCE_SERVER_CLIENT_ID
|
||||||
|
introspection_client_secret = oauth2_settings.RESOURCE_SERVER_CLIENT_SECRET
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = AccessTokenModel.objects.select_related("application", "user").get(token=token)
|
||||||
|
except AccessTokenModel.DoesNotExist:
|
||||||
|
access_token = None
|
||||||
|
|
||||||
|
# if there is no token or it's invalid then introspect the token if there's an external OAuth server
|
||||||
|
if not access_token or not access_token.is_valid(scopes):
|
||||||
|
if introspection_url and (introspection_token or introspection_credentials or (introspection_client_id and
|
||||||
|
introspection_client_secret)):
|
||||||
|
access_token = self._get_token_from_gooyal_authentication_server(
|
||||||
|
token,
|
||||||
|
introspection_url,
|
||||||
|
introspection_token,
|
||||||
|
introspection_credentials,
|
||||||
|
introspection_client_id,
|
||||||
|
introspection_client_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
if access_token and access_token.is_valid(scopes):
|
||||||
|
request.client = access_token.application
|
||||||
|
request.user = access_token.user
|
||||||
|
request.scopes = scopes
|
||||||
|
|
||||||
|
# this is needed by django rest framework
|
||||||
|
request.access_token = access_token
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self._set_oauth2_error_on_request(request, access_token, scopes)
|
||||||
|
return False
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ OAUTH2_PROVIDER = {
|
||||||
'write': 'Write scope',
|
'write': 'Write scope',
|
||||||
'groups': 'Access to your groups',
|
'groups': 'Access to your groups',
|
||||||
'introspection': 'Introspect token scope'},
|
'introspection': 'Introspect token scope'},
|
||||||
'OAUTH2_VALIDATOR_CLASS': 'apps.gooyal_oauth2.validators.MultiGatewayOAuth2Validator'
|
'OAUTH2_VALIDATOR_CLASS': 'apps.gooyal_oauth2.validators.OAuth2Validator'
|
||||||
}
|
}
|
||||||
|
|
||||||
# GOOYAL_DYNAMIC_SCOPES
|
# GOOYAL_DYNAMIC_SCOPES
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ django-oauth-toolkit
|
||||||
djangorestframework
|
djangorestframework
|
||||||
markdown
|
markdown
|
||||||
django-filter
|
django-filter
|
||||||
django-cors-middleware
|
django-cors-headers
|
||||||
pillow
|
pillow
|
||||||
django-model-utils
|
django-model-utils
|
||||||
mysqlclient
|
mysqlclient
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,18 @@ billiard==3.6.3.0 # via celery
|
||||||
celery==4.4.7 # via -r requirements.in
|
celery==4.4.7 # via -r requirements.in
|
||||||
certifi==2020.6.20 # via requests
|
certifi==2020.6.20 # via requests
|
||||||
chardet==3.0.4 # via requests
|
chardet==3.0.4 # via requests
|
||||||
django-cors-middleware==1.5.0 # via -r requirements.in
|
django-cors-headers==3.5.0 # via -r requirements.in
|
||||||
django-filter==2.3.0 # via -r requirements.in
|
django-filter==2.3.0 # via -r requirements.in
|
||||||
django-model-utils==4.0.0 # via -r requirements.in
|
django-model-utils==4.0.0 # via -r requirements.in
|
||||||
django-mysql==3.8.1 # via -r requirements.in
|
django-mysql==3.8.1 # via -r requirements.in
|
||||||
django-oauth-toolkit==1.3.2 # via -r requirements.in
|
django-oauth-toolkit==1.3.2 # via -r requirements.in
|
||||||
django==3.1.1 # via -r requirements.in, django-filter, django-model-utils, django-mysql, django-oauth-toolkit, djangorestframework
|
django==3.1.1 # via -r requirements.in, django-cors-headers, django-filter, django-model-utils, django-mysql, django-oauth-toolkit, djangorestframework
|
||||||
djangorestframework==3.11.1 # via -r requirements.in
|
djangorestframework==3.11.1 # via -r requirements.in
|
||||||
gevent==20.6.2 # via -r requirements.in
|
gevent==20.6.2 # via -r requirements.in
|
||||||
greenlet==0.4.16 # via gevent
|
greenlet==0.4.16 # via gevent
|
||||||
gunicorn==20.0.4 # via -r requirements.in
|
gunicorn==20.0.4 # via -r requirements.in
|
||||||
idna==2.10 # via requests
|
idna==2.10 # via requests
|
||||||
|
importlib-metadata==2.0.0 # via kombu, markdown
|
||||||
kombu==4.6.11 # via celery
|
kombu==4.6.11 # via celery
|
||||||
markdown==3.2.2 # via -r requirements.in
|
markdown==3.2.2 # via -r requirements.in
|
||||||
mysqlclient==2.0.1 # via -r requirements.in
|
mysqlclient==2.0.1 # via -r requirements.in
|
||||||
|
|
@ -33,6 +34,7 @@ requests==2.24.0 # via django-oauth-toolkit
|
||||||
sqlparse==0.3.1 # via django
|
sqlparse==0.3.1 # via django
|
||||||
urllib3==1.25.10 # via requests
|
urllib3==1.25.10 # via requests
|
||||||
vine==1.3.0 # via amqp, celery
|
vine==1.3.0 # via amqp, celery
|
||||||
|
zipp==3.3.1 # via importlib-metadata
|
||||||
zope.event==4.4 # via gevent
|
zope.event==4.4 # via gevent
|
||||||
zope.interface==5.1.0 # via gevent
|
zope.interface==5.1.0 # via gevent
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue