You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'd like some extra sets of eyes to make sure we didn't miss anything dangerous.
We have been looking for a Django Rest Framework (DRF) solution to go along with oidc-provider for some time. We have a bunch of additional models tied to Client, as I'm sure many of you do, and wanted to be able to provide information about these associated models to their clients via DRF.
So we looked at how django-oidc-provider, mozilla-django-oidc, and django-oauth-toolkit approach protecting endpoints, as well as digging through the docs for DRF, and ended up with what's below. It doesn't modify any files in django-oidc-provider - just adds functionality to protect your DRF views. (Portions of this file are excerpted from those projects.)
Here I'll show the the authentication class, the permission class (to limit to scope), settings, and a basic example. Again, please take a look and let us know if anything looks unsafe or if you have recommendations.
drf.py
import logging
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from rest_framework import authentication
from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
from rest_framework.permissions import BasePermission
from requests.exceptions import HTTPError
from urllib.request import parse_http_list, parse_keqv_list
from oidc_provider.models import Token
from oidc_provider.lib.utils.oauth2 import extract_access_token
LOGGER = logging.getLogger(__name__)
User = get_user_model()
def parse_www_authenticate_header(header):
"""
Convert a WWW-Authentication header into a dict that can be used
in a JSON response.
"""
items = parse_http_list(header)
return parse_keqv_list(items)
class OIDCAuthentication(authentication.BaseAuthentication):
"""
Provide OpenID Connect authentication for DRF.
"""
# used by the authenticate_header method.
www_authenticate_realm = 'api'
def authenticate(self, request):
"""
Authenticate the request and return a tuple of (user, token) or None
if there was no authentication attempt.
"""
access_token = extract_access_token(request)
if not access_token:
return None
try:
try:
token = Token.objects.get(access_token=access_token)
except Token.DoesNotExist:
LOGGER.debug('[UserInfo] Token does not exist: %s', access_token)
raise AuthenticationFailed('Invalid_token')
# if the token has an associated user, return it
# otherwise (e.g. if using client_credentials), get_or_create a default API user
user = token.user
if not user:
try:
# Check for case where the User model is using email rather than username
User._meta.get_field("username")
user = User.objects.get_or_create(username="default_api_user")[0]
except FieldDoesNotExist:
user = User.objects.get_or_create(email="[email protected]")[0]
return user, access_token
except HTTPError as exc:
resp = exc.response
# if the oidc provider returns 401, it means the token is invalid.
# in that case, we want to return the upstream error message (which
# we can get from the www-authentication header) in the response.
if resp.status_code == 401 and 'www-authenticate' in resp.headers:
data = parse_www_authenticate_header(resp.headers['www-authenticate'])
raise AuthenticationFailed(data['error_description'])
# for all other http errors, just re-raise the exception.
raise
except SuspiciousOperation as exc:
LOGGER.info('Login failed: %s', exc)
raise AuthenticationFailed('Login failed')
return None
def authenticate_header(self, request):
"""
If this method returns None, a generic HTTP 403 forbidden response is
returned by DRF when authentication fails.
By making the method return a string, a 401 is returned instead. The
return value will be used as the WWW-Authenticate header.
"""
return 'Bearer realm="%s"' % self.www_authenticate_realm
class TokenHasScope(BasePermission):
"""
The request is authenticated and the token used has the right scope
"""
def has_permission(self, request, view):
try:
token = Token.objects.get(access_token=request.auth) # The Token object retrieved from access_token
if token.has_expired():
self.message = {
"detail": PermissionDenied.default_detail,
"error": 'Token has expired',
}
return False
except Token.DoesNotExist:
self.message = {
"detail": PermissionDenied.default_detail,
"error": "No token provided or token does not exist",
}
return False
if hasattr(token, "scope"):
required_scopes = self.get_scopes(request, view)
LOGGER.debug("Required scopes to access resource: {0}".format(required_scopes))
if set(required_scopes).issubset(set(token.scope)):
return True
# Provide information about required scope
self.message = {
"detail": PermissionDenied.default_detail,
"error": "Permission denied, required_scopes: " + str(list(required_scopes)),
}
return False
return False
def get_scopes(self, request, view):
try:
return getattr(view, "required_scopes")
except AttributeError:
raise ImproperlyConfigured(
"TokenHasScope requires the view to define the required_scopes attribute"
)
Add to settings.py, assuming drf.py is in your project's root:
Here is a basic example which simply serializes a bit of the Client instance for the client which is requesting the API endpoint. In reality, you would use the Client instance to serialize associated models.
serializers.py
class ClientTestSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ('name', 'owner', "date_created", "website_url")
views.py
from my_project.drf import TokenHasScope
class ClientViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing client instances.
"""
permission_classes = [TokenHasScope]
required_scopes = ['read_books']
serializer_class = ClientTestSerializer
queryset = Client.objects.none()
def list(self, request, *args, **kwargs):
access_token = request.META.get('HTTP_AUTHORIZATION').split()[1]
if access_token is None:
queryset = Client.objects.none()
else:
queryset = Client.objects.filter(token__access_token=access_token)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
routers.py
from rest_framework import routers
from my_project.my_app_name.api.views import ClientViewSet
router = routers.DefaultRouter()
router.register(r'^client', ClientViewSet, base_name='client')
Request a token using client_credentials flow:
curl -X POST \
-d "client_id=514698&client_secret=40a9b32864095593d568de1f2a674d76ed3aeaa98a1a7c925ac36fc2&grant_type=client_credentials" \
https://myproject.com/token
Sadly, it doesn't look like it. The repo owner has only made two or three github contributions in the past year, and does not appear to be maintaining this repo any longer. It's a shame, because it's a great package and he clearly put in an incredible amount of work. His LinkedIn suggests he got a new job a year ago, so that probably became a higher priority.
I'd like some extra sets of eyes to make sure we didn't miss anything dangerous.
We have been looking for a Django Rest Framework (DRF) solution to go along with oidc-provider for some time. We have a bunch of additional models tied to Client, as I'm sure many of you do, and wanted to be able to provide information about these associated models to their clients via DRF.
So we looked at how django-oidc-provider, mozilla-django-oidc, and django-oauth-toolkit approach protecting endpoints, as well as digging through the docs for DRF, and ended up with what's below. It doesn't modify any files in django-oidc-provider - just adds functionality to protect your DRF views. (Portions of this file are excerpted from those projects.)
Here I'll show the the authentication class, the permission class (to limit to scope), settings, and a basic example. Again, please take a look and let us know if anything looks unsafe or if you have recommendations.
drf.py
Add to settings.py, assuming drf.py is in your project's root:
Here is a basic example which simply serializes a bit of the Client instance for the client which is requesting the API endpoint. In reality, you would use the Client instance to serialize associated models.
serializers.py
views.py
routers.py
Request a token using client_credentials flow:
Result: {"access_token": "f321ac39b4714a67aa4de38308566403", "expires_in": 3600, "token_type": "bearer", "scope": "read_books"}
Use the resulting access_token to grab the API endpoint:
Result: [{"name":"Test Client","owner":null,"date_created":"2019-06-24","website_url":"https://myotherproject.com/"}]
The text was updated successfully, but these errors were encountered: