Adds per-client consent customization

This commit is contained in:
kaveh 2017-03-31 09:34:03 -07:00
parent 1727073447
commit b164388e15
11 changed files with 50 additions and 46 deletions

View file

@ -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).

View file

@ -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:

View file

@ -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
========================

View file

@ -35,7 +35,7 @@
</div>
</nav>
<div class="container">
<div class="container" style="margin-top: 150px;">
{% block content %}{% endblock %}
</div>

View file

@ -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'),

View file

@ -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?'),
),
]

View file

@ -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():

View file

@ -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):
"""

View file

@ -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()

View file

@ -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'))

View file

@ -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'):