0001-idp_oidc-implement-front-channel-logout-fixes-22483.patch
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 | ||
18 |
fragments = [] |
|
19 | ||
20 |
oidc_sessions = get_oidc_sessions(request) |
|
21 |
for key, value in oidc_sessions.iteritems(): |
|
22 |
if 'frontchannel_logout_uri' not in value: |
|
23 |
continue |
|
24 |
ctx = { |
|
25 |
'url': value['frontchannel_logout_uri'], |
|
26 |
'name': value['name'], |
|
27 |
'iframe_timeout': value.get('frontchannel_timeout') or 10000, |
|
28 |
} |
|
29 |
fragments.append( |
|
30 |
render_to_string( |
|
31 |
'authentic2_idp_oidc/logout_fragment.html', |
|
32 |
ctx)) |
|
33 |
return fragments |
|
16 | 34 | |
17 | 35 |
def get_admin_modules(self): |
18 | 36 |
from admin_tools.dashboard import modules |
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 |
- |