From 180aad9a367405ab15069d6de97ab373db54d3df Mon Sep 17 00:00:00 2001 From: Maxim Daniline Date: Mon, 5 Feb 2018 15:29:08 +0000 Subject: [PATCH 1/6] Add token introspection endpoint to satisfy https://tools.ietf.org/html/rfc7662 --- docs/sections/settings.rst | 31 +++++ oidc_provider/admin.py | 41 +++++- oidc_provider/lib/endpoints/introspection.py | 86 +++++++++++++ oidc_provider/lib/errors.py | 9 ++ oidc_provider/lib/utils/common.py | 53 ++++++++ oidc_provider/lib/utils/token.py | 10 +- oidc_provider/migrations/0024_resource.py | 35 +++++ oidc_provider/models.py | 44 +++++++ oidc_provider/settings.py | 12 ++ oidc_provider/tests/app/utils.py | 24 +++- .../tests/test_introspection_endpoint.py | 120 ++++++++++++++++++ oidc_provider/urls.py | 1 + oidc_provider/views.py | 40 +++++- 13 files changed, 492 insertions(+), 14 deletions(-) create mode 100644 oidc_provider/lib/endpoints/introspection.py create mode 100644 oidc_provider/migrations/0024_resource.py create mode 100644 oidc_provider/tests/test_introspection_endpoint.py diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index b5457c1..8b5ef02 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -21,6 +21,21 @@ If not specified, it will be automatically generated using ``request.scheme`` an For example ``http://localhost:8000``. +OIDC_RESOURCE_MODEL +=================== + +OPTIONAL. ``str``. Path to a custom API resource model. + +Default is ``oidc_provider.Resource``. + +Similar to the Django custom user model, you can extend the default model by adding ``AbstractResource`` as a mixin. + +For example:: + + class CustomResource(AbstractResource): + custom_field = models.CharField(max_length=255, _(u'Some Custom Field')) + + OIDC_AFTER_USERLOGIN_HOOK ========================= @@ -90,6 +105,22 @@ Default is:: return id_token +OIDC_INTROSPECTION_PROCESSING_HOOK +================================== + +OPTIONAL. ``str`` or ``(list, tuple)``. + +A string with the location of your function hook or ``list`` or ``tuple`` with hook functions. +Here you can add extra dictionary values specific to your valid response value for token introspection. + +The function receives an ``introspection_response`` dictionary, a ``resource`` instance and an ``id_token`` dictionary. + +Default is:: + + def default_introspection_processing_hook(introspection_response, resource, id_token): + + return introspection_response + OIDC_IDTOKEN_SUB_GENERATOR ========================== diff --git a/oidc_provider/admin.py b/oidc_provider/admin.py index 7718897..5c695a6 100644 --- a/oidc_provider/admin.py +++ b/oidc_provider/admin.py @@ -6,7 +6,9 @@ from django.forms import ModelForm from django.contrib import admin from django.utils.translation import ugettext_lazy as _ -from oidc_provider.models import Client, Code, Token, RSAKey +from oidc_provider.models import Client, Code, Token, RSAKey, get_resource_model + +Resource = get_resource_model() class ClientForm(ModelForm): @@ -72,6 +74,43 @@ class ClientAdmin(admin.ModelAdmin): raw_id_fields = ['owner'] +class ResourceForm(ModelForm): + def __init__(self, *args, **kwargs): + super(ResourceForm, self).__init__(*args, **kwargs) + self.fields['resource_secret'].required = False + + def clean_resource_secret(self): + if self.cleaned_data['resource_secret']: + secret = self.cleaned_data['resource_secret'] + else: + secret = sha224(uuid4().hex.encode()).hexdigest() + return secret + + class Meta: + model = Resource + exclude = [] + + +@admin.register(Resource) +class ResourceAdmin(admin.ModelAdmin): + fieldsets = [ + [None, { + 'fields': ('name', 'owner', 'active',), + }], + [_('Credentials'), { + 'fields': ('resource_id', 'resource_secret',), + }], + [_('Permissions'), { + 'fields': ('allowed_clients',), + }], + ] + form = ResourceForm + list_display = ['name', 'resource_id', 'date_created'] + readonly_fields = ['date_created'] + search_fields = ['name'] + raw_id_fields = ['owner'] + + @admin.register(Code) class CodeAdmin(admin.ModelAdmin): diff --git a/oidc_provider/lib/endpoints/introspection.py b/oidc_provider/lib/endpoints/introspection.py new file mode 100644 index 0000000..9f9b9e1 --- /dev/null +++ b/oidc_provider/lib/endpoints/introspection.py @@ -0,0 +1,86 @@ +import logging + +from django.http import JsonResponse + +from oidc_provider.lib.errors import TokenIntrospectionError +from oidc_provider.lib.utils.common import get_basic_client_credentials, run_processing_hook +from oidc_provider.models import Token, get_resource_model + + +Resource = get_resource_model() + +logger = logging.getLogger(__name__) + + +class TokenIntrospectionEndpoint(object): + + def __init__(self, request): + self.request = request + self.params = {} + self._extract_params() + + def _extract_params(self): + # Introspection only supports POST requests + self.params['token'] = self.request.POST.get('token') + resource_id, resource_secret = get_basic_client_credentials(self.request) + self.params['resource_id'] = resource_id + self.params['resource_secret'] = resource_secret + + def validate_params(self): + if not (self.params['resource_id'] and self.params['resource_secret']): + logger.debug('[Introspection] No resource credentials provided') + raise TokenIntrospectionError() + if not self.params['token']: + logger.debug('[Introspection] No token provided') + raise TokenIntrospectionError() + try: + token = Token.objects.get(access_token=self.params['token']) + except Token.DoesNotExist: + logger.debug('[Introspection] Token does not exist: %s', self.params['token']) + raise TokenIntrospectionError() + if token.has_expired(): + logger.debug('[Introspection] Token is not valid: %s', self.params['token']) + raise TokenIntrospectionError() + if not token.id_token: + logger.debug('[Introspection] Token not an authentication token: %s', self.params['token']) + raise TokenIntrospectionError() + + self.id_token = token.id_token + audience = self.id_token.get('aud') + if not audience: + logger.debug('[Introspection] No audience found for token: %s', self.params['token']) + raise TokenIntrospectionError() + + try: + self.resource = Resource.objects.get( + resource_id=self.params['resource_id'], + resource_secret=self.params['resource_secret'], + active=True, + allowed_clients__client_id__contains=audience) + except Resource.DoesNotExist: + logger.debug('[Introspection] No valid resource id and audience: %s, %s', + self.params['resource_id'], audience) + raise TokenIntrospectionError() + + def create_response_dic(self): + response_dic = dict((k, self.id_token[k]) for k in ('sub', 'exp', 'iat', 'iss')) + response_dic['active'] = True + response_dic['client_id'] = self.id_token.get('aud') + response_dic['aud'] = self.resource.resource_id + + response_dic = run_processing_hook(response_dic, 'OIDC_INTROSPECTION_PROCESSING_HOOK', + resource=self.resource, + id_token=self.id_token) + + return response_dic + + @classmethod + def response(cls, dic, status=200): + """ + Create and return a response object. + """ + response = JsonResponse(dic, status=status) + response['Cache-Control'] = 'no-store' + response['Pragma'] = 'no-cache' + + return response diff --git a/oidc_provider/lib/errors.py b/oidc_provider/lib/errors.py index 22d8e9a..318fb96 100644 --- a/oidc_provider/lib/errors.py +++ b/oidc_provider/lib/errors.py @@ -32,6 +32,15 @@ class UserAuthError(Exception): } +class TokenIntrospectionError(Exception): + """ + Specific to the introspection endpoint. This error will be converted + to an "active: false" response, as per the spec. + See https://tools.ietf.org/html/rfc7662 + """ + pass + + class AuthorizeError(Exception): _errors = { diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index 870d3ee..56c7b58 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -1,7 +1,11 @@ +from base64 import b64decode from hashlib import sha224 +from django.http import HttpResponse +from oidc_provider import settings import django from django.http import HttpResponse +import re from oidc_provider import settings @@ -12,6 +16,9 @@ else: from django.core.urlresolvers import reverse +basic_re = re.compile('^Basic\s(.+)$', re.I) + + def redirect(uri): """ Custom Response object for redirecting to a Non-HTTP url scheme. @@ -123,6 +130,17 @@ def default_idtoken_processing_hook(id_token, user): return id_token +def default_introspection_processing_hook(introspection_response, resource, id_token): + """ + Hook to customise the returned data from the token introspection endpoint + :param introspection_response: + :param resource: + :param id_token: + :return: + """ + return introspection_response + + def get_browser_state_or_default(request): """ Determine value to use as session state. @@ -130,3 +148,38 @@ def get_browser_state_or_default(request): key = (request.session.session_key or settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY')) return sha224(key.encode('utf-8')).hexdigest() + + +def get_basic_client_credentials(request): + """ + Get client credentials using HTTP Basic Authentication method. + Or try getting parameters via POST. + See: http://tools.ietf.org/html/rfc6750#section-2.1 + + :param request: + :return: tuple of client_id, client_secret + :rtype: tuple + """ + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + result = basic_re.match(auth_header) + if result: + b64_user_pass = result.group(1) + try: + user_pass = b64decode(b64_user_pass).decode('utf-8').split(':', 1) + client_id, client_secret = tuple(user_pass) + except (ValueError, UnicodeDecodeError): + client_id = client_secret = '' + else: + client_id = request.POST.get('client_id') + client_secret = request.POST.get('client_secret') + return client_id, client_secret + + +def run_processing_hook(subject, hook_settings_name, **kwargs): + processing_hook = settings.get(hook_settings_name) + if isinstance(processing_hook, (list, tuple)): + for hook in processing_hook: + subject = settings.import_from_str(hook)(subject, **kwargs) + else: + subject = settings.import_from_str(processing_hook)(subject, **kwargs) + return subject diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 2da2c13..a413bc8 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -9,7 +9,7 @@ from jwkest.jwk import SYMKey from jwkest.jws import JWS from jwkest.jwt import JWT -from oidc_provider.lib.utils.common import get_issuer +from oidc_provider.lib.utils.common import get_issuer, run_processing_hook from oidc_provider.lib.claims import StandardScopeClaims from oidc_provider.models import ( Code, @@ -62,13 +62,7 @@ def create_id_token(token, user, aud, nonce='', at_hash='', request=None, scope= claims = StandardScopeClaims(token).create_response_dic() dic.update(claims) - processing_hook = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK') - - if isinstance(processing_hook, (list, tuple)): - for hook in processing_hook: - dic = settings.import_from_str(hook)(dic, user=user) - else: - dic = settings.import_from_str(processing_hook)(dic, user=user) + dic = run_processing_hook(dic, 'OIDC_IDTOKEN_PROCESSING_HOOK', user=user) return dic diff --git a/oidc_provider/migrations/0024_resource.py b/oidc_provider/migrations/0024_resource.py new file mode 100644 index 0000000..b1d541d --- /dev/null +++ b/oidc_provider/migrations/0024_resource.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-02-05 14:19 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('oidc_provider', '0023_client_owner'), + ] + + operations = [ + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=100, verbose_name='Name')), + ('resource_id', models.CharField(max_length=255, unique=True, verbose_name='Resource ID')), + ('resource_secret', models.CharField(max_length=255, verbose_name='Resource Secret')), + ('date_created', models.DateField(auto_now_add=True, verbose_name='Date Created')), + ('date_updated', models.DateField(auto_now=True, verbose_name='Date Updated')), + ('active', models.BooleanField(default=False, verbose_name='Is Active')), + ('allowed_clients', models.ManyToManyField(blank=True, help_text='Select which clients can be used to access this resource.', related_name='accessible_resources', to='oidc_provider.Client', verbose_name='Allowed Clients')), + ('owner', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oidc_resource_set', to=settings.AUTH_USER_MODEL, verbose_name='Owner')), + ], + options={ + 'swappable': 'OIDC_RESOURCE_MODEL', + }, + ), + ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 411633c..110d48a 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -4,6 +4,7 @@ import binascii from hashlib import md5, sha256 import json +from django.apps import apps from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -128,6 +129,45 @@ class Client(models.Model): return self.redirect_uris[0] if self.redirect_uris else '' +class AbstractResource(models.Model): + name = models.CharField(max_length=100, default='', verbose_name=_(u'Name')) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, + verbose_name=_(u'Owner'), + blank=True, null=True, default=None, + on_delete=models.SET_NULL, + related_name='oidc_resource_set') + + resource_id = models.CharField(max_length=255, unique=True, verbose_name=_(u'Resource ID')) + resource_secret = models.CharField(max_length=255, verbose_name=_(u'Resource Secret')) + + date_created = models.DateField(auto_now_add=True, verbose_name=_(u'Date Created')) + date_updated = models.DateField(auto_now=True, verbose_name=_(u'Date Updated')) + + active = models.BooleanField(default=False, verbose_name=_(u'Is Active')) + + allowed_clients = models.ManyToManyField(Client, + blank=True, + verbose_name=_(u'Allowed Clients'), + related_name='accessible_resources', + help_text=_(u'Select which clients can be used to access this resource.')) + + def __str__(self): + return u'{0}'.format(self.name) + + def __unicode__(self): + return self.__str__() + + class Meta: + verbose_name = _(u'Resource') + verbose_name_plural = _(u'Resources') + abstract = True + + +class Resource(AbstractResource): + class Meta: + swappable = 'OIDC_RESOURCE_MODEL' + + class BaseCodeTokenModel(models.Model): client = models.ForeignKey(Client, verbose_name=_(u'Client'), on_delete=models.CASCADE) @@ -232,3 +272,7 @@ class RSAKey(models.Model): @property def kid(self): return u'{0}'.format(md5(self.key.encode('utf-8')).hexdigest() if self.key else '') + + +def get_resource_model(): + return apps.get_model(getattr(settings, 'OIDC_RESOURCE_MODEL', 'oidc_provider.Resource')) diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 8c1729d..66cf0c3 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -129,6 +129,10 @@ class DefaultSettings(object): """ return 'oidc_provider.lib.utils.common.default_idtoken_processing_hook' + @property + def OIDC_INTROSPECTION_PROCESSING_HOOK(self): + return 'oidc_provider.lib.utils.common.default_introspection_processing_hook' + @property def OIDC_GRANT_TYPE_PASSWORD_ENABLE(self): """ @@ -152,6 +156,14 @@ class DefaultSettings(object): 'error': 'oidc_provider/error.html' } + @property + def OIDC_RESOURCE_MODEL(self): + """ + Model w + :return: + """ + return 'oidc_provider.Resource' + default_settings = DefaultSettings() diff --git a/oidc_provider/tests/app/utils.py b/oidc_provider/tests/app/utils.py index 47e09da..e3f9472 100644 --- a/oidc_provider/tests/app/utils.py +++ b/oidc_provider/tests/app/utils.py @@ -11,7 +11,9 @@ from django.contrib.auth.models import User from oidc_provider.models import ( Client, Code, - Token) + Token, get_resource_model) + +Resource = get_resource_model() FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d' @@ -63,9 +65,22 @@ def create_fake_client(response_type, is_public=False, require_consent=True): return client +def create_fake_resource(allowed_clients, active=True): + resource = Resource(name='Some API', + resource_id=str(random.randint(1, 999999)).zfill(6), + resource_secret=str(random.randint(1, 999999)).zfill(6), + active=active) + resource.name = 'Some API' + resource.save() + resource.allowed_clients.add(*allowed_clients) + resource.save() + + return resource + + def create_fake_token(user, scopes, client): expires_at = timezone.now() + timezone.timedelta(seconds=60) - token = Token(user=user, client=client, expires_at=expires_at) + token = Token(user=user, client=client, expires_at=expires_at, access_token=str(random.randint(1, 999999)).zfill(6)) token.scope = scopes token.save() @@ -126,3 +141,8 @@ def fake_idtoken_processing_hook2(id_token, user): id_token['test_idtoken_processing_hook2'] = FAKE_RANDOM_STRING id_token['test_idtoken_processing_hook_user_email2'] = user.email return id_token + + +def fake_introspection_processing_hook(response_dict, resource, id_token): + response_dict['test_introspection_processing_hook'] = FAKE_RANDOM_STRING + return response_dict diff --git a/oidc_provider/tests/test_introspection_endpoint.py b/oidc_provider/tests/test_introspection_endpoint.py new file mode 100644 index 0000000..bfb0b12 --- /dev/null +++ b/oidc_provider/tests/test_introspection_endpoint.py @@ -0,0 +1,120 @@ +import time +from mock import patch + +from django.utils.encoding import force_text + +from oidc_provider.lib.utils.token import create_id_token + +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + +from django.core.management import call_command +from django.test import TestCase, RequestFactory, override_settings +from django.core.urlresolvers import reverse +from django.utils import timezone + +from oidc_provider.tests.app.utils import ( + create_fake_user, + create_fake_client, + create_fake_resource, + create_fake_token, + FAKE_RANDOM_STRING) +from oidc_provider.views import TokenIntrospectionView + + +class IntrospectionTestCase(TestCase): + + def setUp(self): + call_command('creatersakey') + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='id_token token') + self.resource = create_fake_resource(allowed_clients=[self.client]) + self.scopes = ['openid', 'profile'] + self.token = create_fake_token(self.user, self.scopes, self.client) + self.now = time.time() + with patch('oidc_provider.lib.utils.token.time.time') as time_func: + time_func.return_value = self.now + self.token.id_token = create_id_token(self.user, self.client.client_id) + self.token.save() + + def test_no_client_params_returns_inactive(self): + response = self._make_request(client_id='') + self._assert_inactive(response) + + def test_no_client_secret_returns_inactive(self): + response = self._make_request(client_secret='') + self._assert_inactive(response) + + def test_invalid_client_returns_inactive(self): + response = self._make_request(client_id='invalid') + self._assert_inactive(response) + + def test_token_not_found_returns_inactive(self): + response = self._make_request(access_token='invalid') + self._assert_inactive(response) + + def test_no_allowed_clients_returns_inactive(self): + self.resource.allowed_clients.clear() + self.resource.save() + response = self._make_request() + self._assert_inactive(response) + + def test_resource_inactive_returns_inactive(self): + self.resource.active = False + self.resource.save() + response = self._make_request() + self._assert_inactive(response) + + def test_token_expired_returns_inactive(self): + self.token.expires_at = timezone.now() - timezone.timedelta(seconds=60) + self.token.save() + response = self._make_request() + self._assert_inactive(response) + + def test_valid_request_returns_default_properties(self): + response = self._make_request() + self.assertEqual(response.status_code, 200) + self.assertJSONEqual(force_text(response.content), { + 'active': True, + 'aud': self.resource.resource_id, + 'client_id': self.client.client_id, + 'sub': str(self.user.pk), + 'iat': int(self.now), + 'exp': int(self.now + 600), + 'iss': 'http://localhost:8000/openid', + }) + + @override_settings( + OIDC_INTROSPECTION_PROCESSING_HOOK='oidc_provider.tests.app.utils.fake_introspection_processing_hook') + def test_custom_introspection_hook_called_on_valid_request(self): + response = self._make_request() + self.assertEqual(response.status_code, 200) + self.assertJSONEqual(force_text(response.content), { + 'active': True, + 'aud': self.resource.resource_id, + 'client_id': self.client.client_id, + 'sub': str(self.user.pk), + 'iat': int(self.now), + 'exp': int(self.now + 600), + 'iss': 'http://localhost:8000/openid', + 'test_introspection_processing_hook': FAKE_RANDOM_STRING + }) + + def _assert_inactive(self, response): + self.assertEqual(response.status_code, 200) + self.assertJSONEqual(force_text(response.content), {'active': False}) + + def _make_request(self, **kwargs): + url = reverse('oidc_provider:token-introspection') + data = { + 'client_id': kwargs.get('client_id', self.resource.resource_id), + 'client_secret': kwargs.get('client_secret', self.resource.resource_secret), + 'token': kwargs.get('access_token', self.token.access_token), + } + + request = self.factory.post(url, data=urlencode(data), content_type='application/x-www-form-urlencoded') + + return TokenIntrospectionView.as_view()(request) diff --git a/oidc_provider/urls.py b/oidc_provider/urls.py index 0ad05b0..44cc914 100644 --- a/oidc_provider/urls.py +++ b/oidc_provider/urls.py @@ -17,6 +17,7 @@ urlpatterns = [ url(r'^end-session/?$', views.EndSessionView.as_view(), name='end-session'), url(r'^\.well-known/openid-configuration/?$', views.ProviderInfoView.as_view(), name='provider-info'), + url(r'^introspect/?$', views.TokenIntrospectionView.as_view(), name='token-introspection'), url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'), ] diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 7a19bcc..e66903d 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -1,5 +1,6 @@ import logging +from oidc_provider.lib.endpoints.introspection import TokenIntrospectionEndpoint try: from urllib import urlencode from urlparse import urlsplit, parse_qs, urlunsplit @@ -34,7 +35,8 @@ from oidc_provider.lib.errors import ( ClientIdError, RedirectUriError, TokenError, - UserAuthError) + UserAuthError, + TokenIntrospectionError) from oidc_provider.lib.utils.common import ( redirect, get_site_url, @@ -50,6 +52,25 @@ from oidc_provider.models import ( from oidc_provider import settings from oidc_provider import signals +try: + from urllib import urlencode + from urlparse import urlsplit, parse_qs, urlunsplit +except ImportError: + from urllib.parse import urlsplit, parse_qs, urlunsplit, urlencode + +from Cryptodome.PublicKey import RSA +from django.contrib.auth.views import ( + redirect_to_login, + logout, +) + +import django +if django.VERSION >= (1, 11): + from django.urls import reverse +else: + from django.core.urlresolvers import reverse + + logger = logging.getLogger(__name__) OIDC_TEMPLATES = settings.get('OIDC_TEMPLATES') @@ -230,10 +251,10 @@ class TokenView(View): @protected_resource_view(['openid']) def userinfo(request, *args, **kwargs): """ - Create a diccionary with all the requested claims about the End-User. + Create a dictionary with all the requested claims about the End-User. See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse - Return a diccionary. + Return a dictionary. """ token = kwargs['token'] @@ -267,6 +288,7 @@ class ProviderInfoView(View): dic['token_endpoint'] = site_url + reverse('oidc_provider:token') dic['userinfo_endpoint'] = site_url + reverse('oidc_provider:userinfo') dic['end_session_endpoint'] = site_url + reverse('oidc_provider:end-session') + dic['introspection_endpoint'] = site_url + reverse('oidc_provider:token-introspection') types_supported = [x[0] for x in RESPONSE_TYPE_CHOICES] dic['response_types_supported'] = types_supported @@ -356,3 +378,15 @@ class CheckSessionIframeView(View): def get(self, request, *args, **kwargs): return render(request, 'oidc_provider/check_session_iframe.html', kwargs) + + +class TokenIntrospectionView(View): + def post(self, request, *args, **kwargs): + introspection = TokenIntrospectionEndpoint(request) + + try: + introspection.validate_params() + dic = introspection.create_response_dic() + return TokenIntrospectionEndpoint.response(dic) + except TokenIntrospectionError: + return TokenIntrospectionEndpoint.response({'active': False}) From 00f3efa158e7afdfc5b8d7f3dbd4a045f8522526 Mon Sep 17 00:00:00 2001 From: Maxim Daniline Date: Mon, 5 Feb 2018 16:56:37 +0000 Subject: [PATCH 2/6] Skip csrf protection on introspection endpoint --- oidc_provider/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index e66903d..c0c3abb 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -1,5 +1,7 @@ import logging +from django.views.decorators.csrf import csrf_exempt + from oidc_provider.lib.endpoints.introspection import TokenIntrospectionEndpoint try: from urllib import urlencode @@ -381,6 +383,10 @@ class CheckSessionIframeView(View): class TokenIntrospectionView(View): + @method_decorator(csrf_exempt) + def dispatch(self, request, *args, **kwargs): + return super(TokenIntrospectionView, self).dispatch(request, *args, **kwargs) + def post(self, request, *args, **kwargs): introspection = TokenIntrospectionEndpoint(request) From 8eeaf5cf3366d01a2aa122fc82632d4b39723894 Mon Sep 17 00:00:00 2001 From: Maxim Daniline Date: Mon, 23 Apr 2018 14:59:56 +0100 Subject: [PATCH 3/6] Remove the Resource model --- oidc_provider/admin.py | 41 +------------- oidc_provider/lib/endpoints/introspection.py | 56 +++++++++++-------- oidc_provider/lib/utils/common.py | 36 +----------- oidc_provider/migrations/0024_resource.py | 35 ------------ oidc_provider/models.py | 44 --------------- oidc_provider/settings.py | 20 ++++--- oidc_provider/tests/app/utils.py | 21 +------ .../test_introspection_endpoint.py | 43 +++++++------- oidc_provider/views.py | 18 ------ 9 files changed, 74 insertions(+), 240 deletions(-) delete mode 100644 oidc_provider/migrations/0024_resource.py rename oidc_provider/tests/{ => cases}/test_introspection_endpoint.py (75%) diff --git a/oidc_provider/admin.py b/oidc_provider/admin.py index 5c695a6..7718897 100644 --- a/oidc_provider/admin.py +++ b/oidc_provider/admin.py @@ -6,9 +6,7 @@ from django.forms import ModelForm from django.contrib import admin from django.utils.translation import ugettext_lazy as _ -from oidc_provider.models import Client, Code, Token, RSAKey, get_resource_model - -Resource = get_resource_model() +from oidc_provider.models import Client, Code, Token, RSAKey class ClientForm(ModelForm): @@ -74,43 +72,6 @@ class ClientAdmin(admin.ModelAdmin): raw_id_fields = ['owner'] -class ResourceForm(ModelForm): - def __init__(self, *args, **kwargs): - super(ResourceForm, self).__init__(*args, **kwargs) - self.fields['resource_secret'].required = False - - def clean_resource_secret(self): - if self.cleaned_data['resource_secret']: - secret = self.cleaned_data['resource_secret'] - else: - secret = sha224(uuid4().hex.encode()).hexdigest() - return secret - - class Meta: - model = Resource - exclude = [] - - -@admin.register(Resource) -class ResourceAdmin(admin.ModelAdmin): - fieldsets = [ - [None, { - 'fields': ('name', 'owner', 'active',), - }], - [_('Credentials'), { - 'fields': ('resource_id', 'resource_secret',), - }], - [_('Permissions'), { - 'fields': ('allowed_clients',), - }], - ] - form = ResourceForm - list_display = ['name', 'resource_id', 'date_created'] - readonly_fields = ['date_created'] - search_fields = ['name'] - raw_id_fields = ['owner'] - - @admin.register(Code) class CodeAdmin(admin.ModelAdmin): diff --git a/oidc_provider/lib/endpoints/introspection.py b/oidc_provider/lib/endpoints/introspection.py index 9f9b9e1..91b0a5a 100644 --- a/oidc_provider/lib/endpoints/introspection.py +++ b/oidc_provider/lib/endpoints/introspection.py @@ -3,32 +3,35 @@ import logging from django.http import JsonResponse from oidc_provider.lib.errors import TokenIntrospectionError -from oidc_provider.lib.utils.common import get_basic_client_credentials, run_processing_hook -from oidc_provider.models import Token, get_resource_model - - -Resource = get_resource_model() +from oidc_provider.lib.utils.common import run_processing_hook +from oidc_provider.lib.utils.oauth2 import extract_client_auth +from oidc_provider.models import Token, Client +from oidc_provider import settings logger = logging.getLogger(__name__) +INTROSPECTION_SCOPE = 'token_introspection' + class TokenIntrospectionEndpoint(object): def __init__(self, request): self.request = request self.params = {} + self.id_token = None + self.client = None self._extract_params() def _extract_params(self): # Introspection only supports POST requests self.params['token'] = self.request.POST.get('token') - resource_id, resource_secret = get_basic_client_credentials(self.request) - self.params['resource_id'] = resource_id - self.params['resource_secret'] = resource_secret + client_id, client_secret = extract_client_auth(self.request) + self.params['client_id'] = client_id + self.params['client_secret'] = client_secret def validate_params(self): - if not (self.params['resource_id'] and self.params['resource_secret']): - logger.debug('[Introspection] No resource credentials provided') + if not (self.params['client_id'] and self.params['client_secret']): + logger.debug('[Introspection] No client credentials provided') raise TokenIntrospectionError() if not self.params['token']: logger.debug('[Introspection] No token provided') @@ -42,7 +45,8 @@ class TokenIntrospectionEndpoint(object): logger.debug('[Introspection] Token is not valid: %s', self.params['token']) raise TokenIntrospectionError() if not token.id_token: - logger.debug('[Introspection] Token not an authentication token: %s', self.params['token']) + logger.debug('[Introspection] Token not an authentication token: %s', + self.params['token']) raise TokenIntrospectionError() self.id_token = token.id_token @@ -52,24 +56,32 @@ class TokenIntrospectionEndpoint(object): raise TokenIntrospectionError() try: - self.resource = Resource.objects.get( - resource_id=self.params['resource_id'], - resource_secret=self.params['resource_secret'], - active=True, - allowed_clients__client_id__contains=audience) - except Resource.DoesNotExist: - logger.debug('[Introspection] No valid resource id and audience: %s, %s', - self.params['resource_id'], audience) + self.client = Client.objects.get( + client_id=self.params['client_id'], + client_secret=self.params['client_secret']) + except Client.DoesNotExist: + logger.debug('[Introspection] No valid client for id: %s', + self.params['client_id']) + raise TokenIntrospectionError() + if INTROSPECTION_SCOPE not in self.client.scope: + logger.debug('[Introspection] Client %s does not have introspection scope', + self.params['client_id']) + raise TokenIntrospectionError() + if settings.get('OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE') \ + and audience not in self.client.scope: + logger.debug('[Introspection] Client %s does not audience scope %s', + self.params['client_id'], audience) raise TokenIntrospectionError() def create_response_dic(self): response_dic = dict((k, self.id_token[k]) for k in ('sub', 'exp', 'iat', 'iss')) response_dic['active'] = True response_dic['client_id'] = self.id_token.get('aud') - response_dic['aud'] = self.resource.resource_id + response_dic['aud'] = self.client.client_id - response_dic = run_processing_hook(response_dic, 'OIDC_INTROSPECTION_PROCESSING_HOOK', - resource=self.resource, + response_dic = run_processing_hook(response_dic, + 'OIDC_INTROSPECTION_PROCESSING_HOOK', + client=self.client, id_token=self.id_token) return response_dic diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index 56c7b58..b667b38 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -1,11 +1,7 @@ -from base64 import b64decode from hashlib import sha224 -from django.http import HttpResponse -from oidc_provider import settings import django from django.http import HttpResponse -import re from oidc_provider import settings @@ -16,9 +12,6 @@ else: from django.core.urlresolvers import reverse -basic_re = re.compile('^Basic\s(.+)$', re.I) - - def redirect(uri): """ Custom Response object for redirecting to a Non-HTTP url scheme. @@ -130,11 +123,11 @@ def default_idtoken_processing_hook(id_token, user): return id_token -def default_introspection_processing_hook(introspection_response, resource, id_token): +def default_introspection_processing_hook(introspection_response, client, id_token): """ Hook to customise the returned data from the token introspection endpoint :param introspection_response: - :param resource: + :param client: :param id_token: :return: """ @@ -150,31 +143,6 @@ def get_browser_state_or_default(request): return sha224(key.encode('utf-8')).hexdigest() -def get_basic_client_credentials(request): - """ - Get client credentials using HTTP Basic Authentication method. - Or try getting parameters via POST. - See: http://tools.ietf.org/html/rfc6750#section-2.1 - - :param request: - :return: tuple of client_id, client_secret - :rtype: tuple - """ - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - result = basic_re.match(auth_header) - if result: - b64_user_pass = result.group(1) - try: - user_pass = b64decode(b64_user_pass).decode('utf-8').split(':', 1) - client_id, client_secret = tuple(user_pass) - except (ValueError, UnicodeDecodeError): - client_id = client_secret = '' - else: - client_id = request.POST.get('client_id') - client_secret = request.POST.get('client_secret') - return client_id, client_secret - - def run_processing_hook(subject, hook_settings_name, **kwargs): processing_hook = settings.get(hook_settings_name) if isinstance(processing_hook, (list, tuple)): diff --git a/oidc_provider/migrations/0024_resource.py b/oidc_provider/migrations/0024_resource.py deleted file mode 100644 index b1d541d..0000000 --- a/oidc_provider/migrations/0024_resource.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-02-05 14:19 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('oidc_provider', '0023_client_owner'), - ] - - operations = [ - migrations.CreateModel( - name='Resource', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='', max_length=100, verbose_name='Name')), - ('resource_id', models.CharField(max_length=255, unique=True, verbose_name='Resource ID')), - ('resource_secret', models.CharField(max_length=255, verbose_name='Resource Secret')), - ('date_created', models.DateField(auto_now_add=True, verbose_name='Date Created')), - ('date_updated', models.DateField(auto_now=True, verbose_name='Date Updated')), - ('active', models.BooleanField(default=False, verbose_name='Is Active')), - ('allowed_clients', models.ManyToManyField(blank=True, help_text='Select which clients can be used to access this resource.', related_name='accessible_resources', to='oidc_provider.Client', verbose_name='Allowed Clients')), - ('owner', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oidc_resource_set', to=settings.AUTH_USER_MODEL, verbose_name='Owner')), - ], - options={ - 'swappable': 'OIDC_RESOURCE_MODEL', - }, - ), - ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 110d48a..411633c 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -4,7 +4,6 @@ import binascii from hashlib import md5, sha256 import json -from django.apps import apps from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -129,45 +128,6 @@ class Client(models.Model): return self.redirect_uris[0] if self.redirect_uris else '' -class AbstractResource(models.Model): - name = models.CharField(max_length=100, default='', verbose_name=_(u'Name')) - owner = models.ForeignKey(settings.AUTH_USER_MODEL, - verbose_name=_(u'Owner'), - blank=True, null=True, default=None, - on_delete=models.SET_NULL, - related_name='oidc_resource_set') - - resource_id = models.CharField(max_length=255, unique=True, verbose_name=_(u'Resource ID')) - resource_secret = models.CharField(max_length=255, verbose_name=_(u'Resource Secret')) - - date_created = models.DateField(auto_now_add=True, verbose_name=_(u'Date Created')) - date_updated = models.DateField(auto_now=True, verbose_name=_(u'Date Updated')) - - active = models.BooleanField(default=False, verbose_name=_(u'Is Active')) - - allowed_clients = models.ManyToManyField(Client, - blank=True, - verbose_name=_(u'Allowed Clients'), - related_name='accessible_resources', - help_text=_(u'Select which clients can be used to access this resource.')) - - def __str__(self): - return u'{0}'.format(self.name) - - def __unicode__(self): - return self.__str__() - - class Meta: - verbose_name = _(u'Resource') - verbose_name_plural = _(u'Resources') - abstract = True - - -class Resource(AbstractResource): - class Meta: - swappable = 'OIDC_RESOURCE_MODEL' - - class BaseCodeTokenModel(models.Model): client = models.ForeignKey(Client, verbose_name=_(u'Client'), on_delete=models.CASCADE) @@ -272,7 +232,3 @@ class RSAKey(models.Model): @property def kid(self): return u'{0}'.format(md5(self.key.encode('utf-8')).hexdigest() if self.key else '') - - -def get_resource_model(): - return apps.get_model(getattr(settings, 'OIDC_RESOURCE_MODEL', 'oidc_provider.Resource')) diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 66cf0c3..1fddbfa 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -131,8 +131,20 @@ class DefaultSettings(object): @property def OIDC_INTROSPECTION_PROCESSING_HOOK(self): + """ + OPTIONAL. A string with the location of your function. + Used to update the response for a valid introspection token request. + """ return 'oidc_provider.lib.utils.common.default_introspection_processing_hook' + @property + def OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE(self): + """ + OPTIONAL: A boolean to specify whether or not to verify that the introspection + resource has the requesting client id as one of its scopes. + """ + return True + @property def OIDC_GRANT_TYPE_PASSWORD_ENABLE(self): """ @@ -156,14 +168,6 @@ class DefaultSettings(object): 'error': 'oidc_provider/error.html' } - @property - def OIDC_RESOURCE_MODEL(self): - """ - Model w - :return: - """ - return 'oidc_provider.Resource' - default_settings = DefaultSettings() diff --git a/oidc_provider/tests/app/utils.py b/oidc_provider/tests/app/utils.py index e3f9472..fdee81c 100644 --- a/oidc_provider/tests/app/utils.py +++ b/oidc_provider/tests/app/utils.py @@ -11,9 +11,7 @@ from django.contrib.auth.models import User from oidc_provider.models import ( Client, Code, - Token, get_resource_model) - -Resource = get_resource_model() + Token) FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d' @@ -65,22 +63,9 @@ def create_fake_client(response_type, is_public=False, require_consent=True): return client -def create_fake_resource(allowed_clients, active=True): - resource = Resource(name='Some API', - resource_id=str(random.randint(1, 999999)).zfill(6), - resource_secret=str(random.randint(1, 999999)).zfill(6), - active=active) - resource.name = 'Some API' - resource.save() - resource.allowed_clients.add(*allowed_clients) - resource.save() - - return resource - - def create_fake_token(user, scopes, client): expires_at = timezone.now() + timezone.timedelta(seconds=60) - token = Token(user=user, client=client, expires_at=expires_at, access_token=str(random.randint(1, 999999)).zfill(6)) + token = Token(user=user, client=client, expires_at=expires_at) token.scope = scopes token.save() @@ -143,6 +128,6 @@ def fake_idtoken_processing_hook2(id_token, user): return id_token -def fake_introspection_processing_hook(response_dict, resource, id_token): +def fake_introspection_processing_hook(response_dict, client, id_token): response_dict['test_introspection_processing_hook'] = FAKE_RANDOM_STRING return response_dict diff --git a/oidc_provider/tests/test_introspection_endpoint.py b/oidc_provider/tests/cases/test_introspection_endpoint.py similarity index 75% rename from oidc_provider/tests/test_introspection_endpoint.py rename to oidc_provider/tests/cases/test_introspection_endpoint.py index bfb0b12..952a454 100644 --- a/oidc_provider/tests/test_introspection_endpoint.py +++ b/oidc_provider/tests/cases/test_introspection_endpoint.py @@ -1,4 +1,7 @@ import time +import random + +import django from mock import patch from django.utils.encoding import force_text @@ -12,16 +15,18 @@ except ImportError: from django.core.management import call_command from django.test import TestCase, RequestFactory, override_settings -from django.core.urlresolvers import reverse from django.utils import timezone from oidc_provider.tests.app.utils import ( create_fake_user, create_fake_client, - create_fake_resource, create_fake_token, FAKE_RANDOM_STRING) from oidc_provider.views import TokenIntrospectionView +if django.VERSION >= (1, 11): + from django.urls import reverse +else: + from django.core.urlresolvers import reverse class IntrospectionTestCase(TestCase): @@ -31,13 +36,15 @@ class IntrospectionTestCase(TestCase): self.factory = RequestFactory() self.user = create_fake_user() self.client = create_fake_client(response_type='id_token token') - self.resource = create_fake_resource(allowed_clients=[self.client]) - self.scopes = ['openid', 'profile'] - self.token = create_fake_token(self.user, self.scopes, self.client) + self.resource = create_fake_client(response_type='id_token token') + self.resource.scope = ['token_introspection', self.client.client_id] + self.resource.save() + self.token = create_fake_token(self.user, self.client.scope, self.client) + self.token.access_token = str(random.randint(1, 999999)).zfill(6) self.now = time.time() with patch('oidc_provider.lib.utils.token.time.time') as time_func: time_func.return_value = self.now - self.token.id_token = create_id_token(self.user, self.client.client_id) + self.token.id_token = create_id_token(self.token, self.user, self.client.client_id) self.token.save() def test_no_client_params_returns_inactive(self): @@ -56,14 +63,8 @@ class IntrospectionTestCase(TestCase): response = self._make_request(access_token='invalid') self._assert_inactive(response) - def test_no_allowed_clients_returns_inactive(self): - self.resource.allowed_clients.clear() - self.resource.save() - response = self._make_request() - self._assert_inactive(response) - - def test_resource_inactive_returns_inactive(self): - self.resource.active = False + def test_scope_no_audience_returns_inactive(self): + self.resource.scope = ['token_introspection'] self.resource.save() response = self._make_request() self._assert_inactive(response) @@ -79,7 +80,7 @@ class IntrospectionTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertJSONEqual(force_text(response.content), { 'active': True, - 'aud': self.resource.resource_id, + 'aud': self.resource.client_id, 'client_id': self.client.client_id, 'sub': str(self.user.pk), 'iat': int(self.now), @@ -87,14 +88,13 @@ class IntrospectionTestCase(TestCase): 'iss': 'http://localhost:8000/openid', }) - @override_settings( - OIDC_INTROSPECTION_PROCESSING_HOOK='oidc_provider.tests.app.utils.fake_introspection_processing_hook') + @override_settings(OIDC_INTROSPECTION_PROCESSING_HOOK='oidc_provider.tests.app.utils.fake_introspection_processing_hook') # NOQA def test_custom_introspection_hook_called_on_valid_request(self): response = self._make_request() self.assertEqual(response.status_code, 200) self.assertJSONEqual(force_text(response.content), { 'active': True, - 'aud': self.resource.resource_id, + 'aud': self.resource.client_id, 'client_id': self.client.client_id, 'sub': str(self.user.pk), 'iat': int(self.now), @@ -110,11 +110,12 @@ class IntrospectionTestCase(TestCase): def _make_request(self, **kwargs): url = reverse('oidc_provider:token-introspection') data = { - 'client_id': kwargs.get('client_id', self.resource.resource_id), - 'client_secret': kwargs.get('client_secret', self.resource.resource_secret), + 'client_id': kwargs.get('client_id', self.resource.client_id), + 'client_secret': kwargs.get('client_secret', self.resource.client_secret), 'token': kwargs.get('access_token', self.token.access_token), } - request = self.factory.post(url, data=urlencode(data), content_type='application/x-www-form-urlencoded') + request = self.factory.post(url, data=urlencode(data), + content_type='application/x-www-form-urlencoded') return TokenIntrospectionView.as_view()(request) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index c0c3abb..e6f3ddc 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -54,24 +54,6 @@ from oidc_provider.models import ( from oidc_provider import settings from oidc_provider import signals -try: - from urllib import urlencode - from urlparse import urlsplit, parse_qs, urlunsplit -except ImportError: - from urllib.parse import urlsplit, parse_qs, urlunsplit, urlencode - -from Cryptodome.PublicKey import RSA -from django.contrib.auth.views import ( - redirect_to_login, - logout, -) - -import django -if django.VERSION >= (1, 11): - from django.urls import reverse -else: - from django.core.urlresolvers import reverse - logger = logging.getLogger(__name__) From 20a355d9f52b3b48e1ce2b3d6903c999cec9848a Mon Sep 17 00:00:00 2001 From: Maxim Daniline Date: Tue, 24 Apr 2018 10:09:49 +0100 Subject: [PATCH 4/6] Update settings docs to add extra introspection setting --- docs/sections/settings.rst | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 8b5ef02..311c72e 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -21,21 +21,6 @@ If not specified, it will be automatically generated using ``request.scheme`` an For example ``http://localhost:8000``. -OIDC_RESOURCE_MODEL -=================== - -OPTIONAL. ``str``. Path to a custom API resource model. - -Default is ``oidc_provider.Resource``. - -Similar to the Django custom user model, you can extend the default model by adding ``AbstractResource`` as a mixin. - -For example:: - - class CustomResource(AbstractResource): - custom_field = models.CharField(max_length=255, _(u'Some Custom Field')) - - OIDC_AFTER_USERLOGIN_HOOK ========================= @@ -121,6 +106,17 @@ Default is:: return introspection_response + +OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE +========================================== + +OPTIONAL ``bool`` + +A flag which toggles whether the audience is matched against the client resource scope when calling the introspection endpoint. + +Default is ``True``. + + OIDC_IDTOKEN_SUB_GENERATOR ========================== From 5a65ac17f9bd09d34f7fc6aff4c5eccced725db7 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Tue, 24 Apr 2018 11:10:27 -0300 Subject: [PATCH 5/6] Replace resource with client in docs. --- docs/sections/settings.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 311c72e..f6f3131 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -98,11 +98,11 @@ OPTIONAL. ``str`` or ``(list, tuple)``. A string with the location of your function hook or ``list`` or ``tuple`` with hook functions. Here you can add extra dictionary values specific to your valid response value for token introspection. -The function receives an ``introspection_response`` dictionary, a ``resource`` instance and an ``id_token`` dictionary. +The function receives an ``introspection_response`` dictionary, a ``client`` instance and an ``id_token`` dictionary. Default is:: - def default_introspection_processing_hook(introspection_response, resource, id_token): + def default_introspection_processing_hook(introspection_response, client, id_token): return introspection_response From eed58197bd6d699c48d96203de2c1e352a385a82 Mon Sep 17 00:00:00 2001 From: Maxim Daniline Date: Thu, 26 Apr 2018 10:12:28 +0100 Subject: [PATCH 6/6] Adjust import order and method order in introspection tests --- .../cases/test_introspection_endpoint.py | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/oidc_provider/tests/cases/test_introspection_endpoint.py b/oidc_provider/tests/cases/test_introspection_endpoint.py index 952a454..99eab9a 100644 --- a/oidc_provider/tests/cases/test_introspection_endpoint.py +++ b/oidc_provider/tests/cases/test_introspection_endpoint.py @@ -1,32 +1,27 @@ import time import random -import django from mock import patch - -from django.utils.encoding import force_text - -from oidc_provider.lib.utils.token import create_id_token - try: from urllib.parse import urlencode except ImportError: from urllib import urlencode - +from django.utils.encoding import force_text from django.core.management import call_command from django.test import TestCase, RequestFactory, override_settings from django.utils import timezone +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse from oidc_provider.tests.app.utils import ( create_fake_user, create_fake_client, create_fake_token, FAKE_RANDOM_STRING) +from oidc_provider.lib.utils.token import create_id_token from oidc_provider.views import TokenIntrospectionView -if django.VERSION >= (1, 11): - from django.urls import reverse -else: - from django.core.urlresolvers import reverse class IntrospectionTestCase(TestCase): @@ -47,6 +42,23 @@ class IntrospectionTestCase(TestCase): self.token.id_token = create_id_token(self.token, self.user, self.client.client_id) self.token.save() + def _assert_inactive(self, response): + self.assertEqual(response.status_code, 200) + self.assertJSONEqual(force_text(response.content), {'active': False}) + + def _make_request(self, **kwargs): + url = reverse('oidc_provider:token-introspection') + data = { + 'client_id': kwargs.get('client_id', self.resource.client_id), + 'client_secret': kwargs.get('client_secret', self.resource.client_secret), + 'token': kwargs.get('access_token', self.token.access_token), + } + + request = self.factory.post(url, data=urlencode(data), + content_type='application/x-www-form-urlencoded') + + return TokenIntrospectionView.as_view()(request) + def test_no_client_params_returns_inactive(self): response = self._make_request(client_id='') self._assert_inactive(response) @@ -102,20 +114,3 @@ class IntrospectionTestCase(TestCase): 'iss': 'http://localhost:8000/openid', 'test_introspection_processing_hook': FAKE_RANDOM_STRING }) - - def _assert_inactive(self, response): - self.assertEqual(response.status_code, 200) - self.assertJSONEqual(force_text(response.content), {'active': False}) - - def _make_request(self, **kwargs): - url = reverse('oidc_provider:token-introspection') - data = { - 'client_id': kwargs.get('client_id', self.resource.client_id), - 'client_secret': kwargs.get('client_secret', self.resource.client_secret), - 'token': kwargs.get('access_token', self.token.access_token), - } - - request = self.factory.post(url, data=urlencode(data), - content_type='application/x-www-form-urlencoded') - - return TokenIntrospectionView.as_view()(request)