Projet

Général

Profil

0001-idp_oidc-implement-front-channel-logout-fixes-22483.patch

Benjamin Dauvergne, 16 mars 2018 13:25

Télécharger (13 ko)

Voir les différences:

Subject: [PATCH] idp_oidc: implement front-channel logout (fixes #22483)

 src/authentic2_idp_oidc/__init__.py                | 23 +++++++++++--
 src/authentic2_idp_oidc/app_settings.py            |  4 +++
 .../migrations/0009_auto_20180313_1156.py          | 24 ++++++++++++++
 src/authentic2_idp_oidc/models.py                  |  7 ++++
 .../authentic2_idp_oidc/logout_fragment.html       |  6 ++++
 src/authentic2_idp_oidc/utils.py                   | 38 ++++++++++++++++++++++
 src/authentic2_idp_oidc/views.py                   | 10 ++++--
 tests/test_idp_oidc.py                             | 35 +++++++++++++-------
 8 files changed, 131 insertions(+), 16 deletions(-)
 create mode 100644 src/authentic2_idp_oidc/migrations/0009_auto_20180313_1156.py
 create mode 100644 src/authentic2_idp_oidc/templates/authentic2_idp_oidc/logout_fragment.html
src/authentic2_idp_oidc/__init__.py
1
from django.template.loader import render_to_string
1 2
from django.utils.translation import ugettext_lazy as _
2 3

  
3 4
default_app_config = 'authentic2_idp_oidc.apps.AppConfig'
......
11 12
    def get_apps(self):
12 13
        return [__name__]
13 14

  
14
    def redirect_logout_list(self, request, next=None):
15
        return []
15
    def logout_list(self, request):
16
        from .utils import get_oidc_sessions
17
        from . import app_settings
18

  
19
        fragments = []
20

  
21
        oidc_sessions = get_oidc_sessions(request)
22
        for key, value in oidc_sessions.iteritems():
23
            if 'frontchannel_logout_uri' not in value:
24
                continue
25
            ctx = {
26
                'url': value['frontchannel_logout_uri'],
27
                'name': value['name'],
28
                'iframe_timeout': value.get('frontchannel_timeout') or app_settings.DEFAULT_FRONTCHANNEL_TIMEOUT,
29
            }
30
            fragments.append(
31
                render_to_string(
32
                    'authentic2_idp_oidc/logout_fragment.html',
33
                    ctx))
34
        return fragments
16 35

  
17 36
    def get_admin_modules(self):
18 37
        from admin_tools.dashboard import modules
src/authentic2_idp_oidc/app_settings.py
26 26
    def SCOPES(self):
27 27
        return self._setting('SCOPES', [])
28 28

  
29
    @property
30
    def DEFAULT_FRONTCHANNEL_TIMEOUT(self):
31
        return self._setting('DEFAULT_FRONTCHANNEL_TIMEOUT', 10000)
32

  
29 33
    @property
30 34
    def IDTOKEN_DURATION(self):
31 35
        return self._setting('IDTOKEN_DURATION', 30)
src/authentic2_idp_oidc/migrations/0009_auto_20180313_1156.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('authentic2_idp_oidc', '0008_oidcclient_idtoken_duration'),
11
    ]
12

  
13
    operations = [
14
        migrations.AddField(
15
            model_name='oidcclient',
16
            name='frontchannel_logout_uri',
17
            field=models.URLField(verbose_name='frontchannel logout URI', blank=True),
18
        ),
19
        migrations.AddField(
20
            model_name='oidcclient',
21
            name='frontchannel_timeout',
22
            field=models.PositiveIntegerField(null=True, verbose_name='frontchannel timeout', blank=True),
23
        ),
24
    ]
src/authentic2_idp_oidc/models.py
115 115
    has_api_access = models.BooleanField(
116 116
        verbose_name=_('has API access'),
117 117
        default=False)
118
    frontchannel_logout_uri = models.URLField(
119
        verbose_name=_('frontchannel logout URI'),
120
        blank=True)
121
    frontchannel_timeout = models.PositiveIntegerField(
122
        verbose_name=_('frontchannel timeout'),
123
        null=True,
124
        blank=True)
118 125

  
119 126
    authorizations = GenericRelation('OIDCAuthorization',
120 127
                                     content_type_field='client_ct',
src/authentic2_idp_oidc/templates/authentic2_idp_oidc/logout_fragment.html
1
{% load i18n %}
2
<div>{% blocktrans %}Sending logout to {{ name }}...{% endblocktrans %}
3
  <iframe src="{{ url }}" marginwidth="0" marginheight="0" scrolling="no" style="border: none"
4
    width="16" height="16" onload="setTimeout(function () { window.iframe_count -= 1; }, {{ iframe_timeout }})">
5
  </iframe>
6
</div>
src/authentic2_idp_oidc/utils.py
9 9

  
10 10
from django.core.exceptions import ImproperlyConfigured
11 11
from django.conf import settings
12
from django.utils.encoding import smart_bytes
12 13

  
13 14
from authentic2 import hooks, crypto
14 15

  
......
173 174
            })
174 175
    hooks.call_hooks('idp_oidc_modify_user_info', client, user, scope_set, user_info)
175 176
    return user_info
177

  
178

  
179
def get_issuer(request):
180
    return request.build_absolute_uri('/')
181

  
182

  
183
def get_session_id(request, client):
184
    '''Derive an OIDC Session Id from the real session identifier, the sector
185
       identifier of the RP and the secret key of the Django instance'''
186
    session_key = smart_bytes(request.session.session_key)
187
    sector_identifier = smart_bytes(get_sector_identifier(client))
188
    secret_key = smart_bytes(settings.SECRET_KEY)
189
    return hashlib.md5(session_key + sector_identifier + secret_key).hexdigest()
190

  
191

  
192
def get_oidc_sessions(request):
193
    return request.session.get('oidc_sessions', {})
194

  
195

  
196
def add_oidc_session(request, client):
197
    oidc_sessions = request.session.setdefault('oidc_sessions', {})
198
    if not client.frontchannel_logout_uri:
199
        return
200
    uri = client.frontchannel_logout_uri
201
    oidc_session = {
202
        'frontchannel_logout_uri': uri,
203
        'frontchannel_timeout': client.frontchannel_timeout,
204
        'name': client.name,
205
        'sid': get_session_id(request, client),
206
        'iss': get_issuer(request),
207
    }
208
    if oidc_sessions.get(uri) == oidc_session:
209
        # already present
210
        return
211
    oidc_sessions[uri] = oidc_session
212
    # force session save
213
    request.session.modified = True
src/authentic2_idp_oidc/views.py
27 27
@setting_enabled('ENABLE', settings=app_settings)
28 28
def openid_configuration(request, *args, **kwargs):
29 29
    metadata = {
30
        'issuer': request.build_absolute_uri('/'),
30
        'issuer': utils.get_issuer(request),
31 31
        'authorization_endpoint': request.build_absolute_uri(reverse('oidc-authorize')),
32 32
        'token_endpoint': request.build_absolute_uri(reverse('oidc-token')),
33 33
        'jwks_uri': request.build_absolute_uri(reverse('oidc-certs')),
......
41 41
            'RS256', 'HS256',
42 42
        ],
43 43
        'userinfo_endpoint': request.build_absolute_uri(reverse('oidc-user-info')),
44
        'frontchannel_logout_supported': True,
45
        'frontchannel_logout_session_supported': True,
44 46
    }
45 47
    return HttpResponse(json.dumps(metadata), content_type='application/json')
46 48

  
......
279 281
            acr = '1'
280 282
        id_token = utils.create_user_info(client, request.user, scopes, id_token=True)
281 283
        id_token.update({
282
            'iss': request.build_absolute_uri('/'),
284
            'iss': utils.get_issuer(request),
283 285
            'aud': client.client_id,
284 286
            'exp': timestamp_from_datetime(start + idtoken_duration(client)),
285 287
            'iat': timestamp_from_datetime(start),
286 288
            'auth_time': last_auth['when'],
287 289
            'acr': acr,
290
            'sid': utils.get_session_id(request, client),
288 291
        })
289 292
        if nonce is not None:
290 293
            id_token['nonce'] = nonce
......
302 305
        # query is transfered through the hashtag
303 306
        response = redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False)
304 307
    hooks.call_hooks('event', name='sso-success', idp='oidc', service=client, user=request.user)
308
    utils.add_oidc_session(request, client)
305 309
    return response
306 310

  
307 311

  
......
384 388
    # prefill id_token with user info
385 389
    id_token = utils.create_user_info(client, oidc_code.user, oidc_code.scope_set(), id_token=True)
386 390
    id_token.update({
387
        'iss': request.build_absolute_uri('/'),
391
        'iss': utils.get_issuer(request),
388 392
        'sub': utils.make_sub(client, oidc_code.user),
389 393
        'aud': client.client_id,
390 394
        'exp': timestamp_from_datetime(start + idtoken_duration(client)),
tests/test_idp_oidc.py
47 47
    from authentic2_idp_oidc.utils import get_jwkset
48 48
    get_jwkset()
49 49

  
50

  
50 51
OIDC_CLIENT_PARAMS = [
51 52
    {
52 53
        'authorization_flow': OIDCClient.FLOW_IMPLICIT,
53 54
    },
54
    {},
55
    {
56
        'post_logout_redirect_uris': 'https://example.com/',
57
    },
55 58
    {
56 59
        'identifier_policy': OIDCClient.POLICY_UUID,
60
        'post_logout_redirect_uris': 'https://example.com/',
57 61
    },
58 62
    {
59 63
        'identifier_policy': OIDCClient.POLICY_EMAIL,
60
        'post_logout_redirect_uris': '',
61 64
    },
62 65
    {
63 66
        'idtoken_algo': OIDCClient.ALGO_HMAC,
......
71 74
    {
72 75
        'authorization_flow': OIDCClient.FLOW_IMPLICIT,
73 76
        'idtoken_duration': datetime.timedelta(hours=1),
77
        'post_logout_redirect_uris': 'https://example.com/',
78
    },
79
    {
80
        'frontchannel_logout_uri': 'https://example.com/southpark/logout/',
81
    },
82
    {
83
        'frontchannel_logout_uri': 'https://example.com/southpark/logout/',
84
        'frontchannel_timeout': 3000,
74 85
    },
75 86
]
76 87

  
......
85 96
    response.form.set('ou', get_default_ou().pk)
86 97
    response.form.set('unauthorized_url', 'https://example.com/southpark/')
87 98
    response.form.set('redirect_uris', 'https://example.com/callback')
88
    response.form.set('post_logout_redirect_uris', 'https://example.com/')
89 99
    for key, value in request.param.iteritems():
90 100
        response.form.set(key, value)
91 101
    response = response.form.submit().follow()
......
233 243
    assert response.json['email_verified'] is True
234 244

  
235 245
    # Now logout
236
    params = {}
237 246
    if oidc_client.post_logout_redirect_uris:
238 247
        params = {
239 248
            'post_logout_redirect_uri': oidc_client.post_logout_redirect_uris,
240 249
            'state': 'xyz',
241 250
        }
242
    logout_url = make_url('oidc-logout', params=params)
243
    response = app.get(logout_url)
244
    if oidc_client.post_logout_redirect_uris:
251
        logout_url = make_url('oidc-logout', params=params)
252
        response = app.get(logout_url)
245 253
        assert 'You have been logged out' in response.content
246 254
        assert 'https://example.com/?state=xyz' in response.content
247 255
        assert '_auth_user_id' not in app.session
248 256
    else:
249
        response = response.maybe_follow()
250
        assert 'You have been logged out' in response.content
251
        assert response.request.environ['HTTP_HOST'] == 'testserver'
252
        assert response.request.environ['PATH_INFO'] == '/login/'
257
        response = app.get(make_url('account_management'))
258
        response = response.click('Logout')
259
        if oidc_client.frontchannel_logout_uri:
260
            iframes = response.pyquery('iframe[src="https://example.com/southpark/logout/"]')
261
            assert iframes
262
            if oidc_client.frontchannel_timeout:
263
                assert iframes.attr('onload').endswith(', %d)' % oidc_client.frontchannel_timeout)
264
            else:
265
                assert iframes.attr('onload').endswith(', 10000)')
253 266

  
254 267

  
255 268
def assert_oidc_error(response, error, error_description=None, fragment=False):
256
-