From b164388e15b8edc46e0ded28ca8d2056051caee7 Mon Sep 17 00:00:00 2001 From: kaveh Date: Fri, 31 Mar 2017 09:34:03 -0700 Subject: [PATCH] Adds per-client consent customization --- CHANGELOG.md | 4 +++ docs/sections/relyingparties.rst | 2 ++ docs/sections/settings.rst | 14 ----------- example_project/myapp/templates/base.html | 2 +- oidc_provider/admin.py | 2 +- .../migrations/0022_auto_20170331_1626.py | 25 +++++++++++++++++++ oidc_provider/models.py | 2 ++ oidc_provider/settings.py | 16 ------------ oidc_provider/tests/app/utils.py | 3 ++- .../tests/test_authorize_endpoint.py | 22 ++++++++-------- oidc_provider/views.py | 4 +-- 11 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 oidc_provider/migrations/0022_auto_20170331_1626.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b10bb..0850514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ##### Added - Signals when user accept/decline the authorization page. +- require_consent and reuse_consent are added to Client model. + +##### Changed +- OIDC_SKIP_CONSENT_ALWAYS and OIDC_SKIP_CONSENT_ENABLE are removed from settings. ##### Fixed - Timestamps with unixtime (instead of django timezone). diff --git a/docs/sections/relyingparties.rst b/docs/sections/relyingparties.rst index d55c58c..d4477e4 100644 --- a/docs/sections/relyingparties.rst +++ b/docs/sections/relyingparties.rst @@ -22,6 +22,8 @@ Properties * ``jwt_alg``: Clients can choose wich algorithm will be used to sign id_tokens. Values are ``HS256`` and ``RS256``. * ``date_created``: Date automatically added when created. * ``redirect_uris``: List of redirect URIs. +* ``require_consent``: If checked, the Server will never ask for consent (only applies to confidential clients). +* ``reuse_consent``: If enabled, the Server will save the user consent given to a specific client, so that user won't be prompted for the same authorization multiple times. Optional information: diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index bba57d7..7edf35b 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -98,20 +98,6 @@ OPTIONAL. ``bool``. Enables OpenID Connect Session Management 1.0 in your provid Default is ``False``. -OIDC_SKIP_CONSENT_ALWAYS -======================== - -OPTIONAL. ``bool``. If enabled, the Server will NEVER ask the user for consent. - -Default is ``False``. - -OIDC_SKIP_CONSENT_ENABLE -======================== - -OPTIONAL. ``bool``. If enabled, the Server will save the user consent given to a specific client, so that user won't be prompted for the same authorization multiple times. - -Default is ``True``. - OIDC_SKIP_CONSENT_EXPIRE ======================== diff --git a/example_project/myapp/templates/base.html b/example_project/myapp/templates/base.html index cca08d0..400cdff 100644 --- a/example_project/myapp/templates/base.html +++ b/example_project/myapp/templates/base.html @@ -35,7 +35,7 @@ -
+
{% block content %}{% endblock %}
diff --git a/oidc_provider/admin.py b/oidc_provider/admin.py index 78478d0..5ff343b 100644 --- a/oidc_provider/admin.py +++ b/oidc_provider/admin.py @@ -51,7 +51,7 @@ class ClientAdmin(admin.ModelAdmin): fieldsets = [ [_(u''), { - 'fields': ('name', 'client_type', 'response_type','_redirect_uris', 'jwt_alg'), + 'fields': ('name', 'client_type', 'response_type','_redirect_uris', 'jwt_alg', 'require_consent', 'reuse_consent'), }], [_(u'Credentials'), { 'fields': ('client_id', 'client_secret'), diff --git a/oidc_provider/migrations/0022_auto_20170331_1626.py b/oidc_provider/migrations/0022_auto_20170331_1626.py new file mode 100644 index 0000000..bad8c93 --- /dev/null +++ b/oidc_provider/migrations/0022_auto_20170331_1626.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-03-31 16:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oidc_provider', '0021_refresh_token_not_unique'), + ] + + operations = [ + migrations.AddField( + model_name='client', + name='require_consent', + field=models.BooleanField(default=True, help_text='If disabled, the Server will NEVER ask the user for consent.', verbose_name='Require Consent?'), + ), + migrations.AddField( + model_name='client', + name='reuse_consent', + field=models.BooleanField(default=True, help_text="If enabled, the Server will save the user consent given to a specific client, so that user won't be prompted for the same authorization multiple times.", verbose_name='Reuse Consent?'), + ), + ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 9c456b5..a196239 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -43,6 +43,8 @@ class Client(models.Model): terms_url = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Terms URL'), help_text=_(u'External reference to the privacy policy of the client.')) contact_email = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Contact Email')) logo = models.FileField(blank=True, default='', upload_to='oidc_provider/clients', verbose_name=_(u'Logo Image')) + reuse_consent = models.BooleanField(default=True, verbose_name=_('Reuse Consent?'), help_text=_('If enabled, the Server will save the user consent given to a specific client, so that user won\'t be prompted for the same authorization multiple times.')) + require_consent = models.BooleanField(default=True, verbose_name=_('Require Consent?'), help_text=_('If disabled, the Server will NEVER ask the user for consent.')) _redirect_uris = models.TextField(default='', verbose_name=_(u'Redirect URIs'), help_text=_(u'Enter each URI on a new line.')) def redirect_uris(): diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 610534c..69f0f3b 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -68,22 +68,6 @@ class DefaultSettings(object): """ return False - @property - def OIDC_SKIP_CONSENT_ALWAYS(self): - """ - OPTIONAL. If enabled, the Server will NEVER ask the user for consent. - """ - return False - - @property - def OIDC_SKIP_CONSENT_ENABLE(self): - """ - OPTIONAL. If enabled, the Server will save the user consent - given to a specific client, so that user won't be prompted for - the same authorization multiple times. - """ - return True - @property def OIDC_SKIP_CONSENT_EXPIRE(self): """ diff --git a/oidc_provider/tests/app/utils.py b/oidc_provider/tests/app/utils.py index 1f2ccc5..8f8f72e 100644 --- a/oidc_provider/tests/app/utils.py +++ b/oidc_provider/tests/app/utils.py @@ -36,7 +36,7 @@ def create_fake_user(): return user -def create_fake_client(response_type, is_public=False): +def create_fake_client(response_type, is_public=False, require_consent=True): """ Create a test client, response_type argument MUST be: 'code', 'id_token' or 'id_token token'. @@ -53,6 +53,7 @@ def create_fake_client(response_type, is_public=False): client.client_secret = str(random.randint(1, 999999)).zfill(6) client.response_type = response_type client.redirect_uris = ['http://example.com/'] + client.require_consent = require_consent client.save() diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 030d50b..71fbb77 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -61,7 +61,9 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin): self.factory = RequestFactory() self.user = create_fake_user() self.client = create_fake_client(response_type='code') + self.client_with_no_consent = create_fake_client(response_type='code', require_consent=False) self.client_public = create_fake_client(response_type='code', is_public=True) + self.client_public_with_no_consent = create_fake_client(response_type='code', is_public=True, require_consent=False) self.state = uuid.uuid4().hex self.nonce = uuid.uuid4().hex @@ -212,8 +214,8 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin): authorization multiple times, the server skip it. """ data = { - 'client_id': self.client.client_id, - 'redirect_uri': self.client.default_redirect_uri, + 'client_id': self.client_with_no_consent.client_id, + 'redirect_uri': self.client_with_no_consent.default_redirect_uri, 'response_type': 'code', 'scope': 'openid email', 'state': self.state, @@ -225,16 +227,15 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin): # Simulate that the user is logged. request.user = self.user - with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True): - response = self._auth_request('post', data, is_user_authenticated=True) + response = self._auth_request('post', data, is_user_authenticated=True) - self.assertIn('code', response['Location'], msg='Code is missing in the returned url.') + self.assertIn('code', response['Location'], msg='Code is missing in the returned url.') response = self._auth_request('post', data, is_user_authenticated=True) is_code_ok = is_code_valid(url=response['Location'], user=self.user, - client=self.client) + client=self.client_with_no_consent) self.assertEqual(is_code_ok, True, msg='Code returned is invalid.') del data['allow'] @@ -242,7 +243,7 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin): is_code_ok = is_code_valid(url=response['Location'], user=self.user, - client=self.client) + client=self.client_with_no_consent) self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.') def test_response_uri_is_properly_constructed(self): @@ -264,15 +265,14 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin): It's recommended not auto-approving requests for non-confidential clients. """ data = { - 'client_id': self.client_public.client_id, + 'client_id': self.client_public_with_no_consent.client_id, 'response_type': 'code', - 'redirect_uri': self.client_public.default_redirect_uri, + 'redirect_uri': self.client_public_with_no_consent.default_redirect_uri, 'scope': 'openid email', 'state': self.state, } - with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True): - response = self._auth_request('get', data, is_user_authenticated=True) + response = self._auth_request('get', data, is_user_authenticated=True) self.assertIn('Request for Permission', response.content.decode('utf-8')) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 6bf142f..996c968 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -65,11 +65,11 @@ class AuthorizeView(View): if hook_resp: return hook_resp - if settings.get('OIDC_SKIP_CONSENT_ALWAYS') and not (authorize.client.client_type == 'public') \ + if not authorize.client.require_consent and not (authorize.client.client_type == 'public') \ and not (authorize.params['prompt'] == 'consent'): return redirect(authorize.create_response_uri()) - if settings.get('OIDC_SKIP_CONSENT_ENABLE'): + if authorize.client.reuse_consent: # Check if user previously give consent. if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public') \ and not (authorize.params['prompt'] == 'consent'):