Projet

Général

Profil

0001-auth_oidc-verify-and-store-id_token-nonce-fixes-2900.patch

Benjamin Dauvergne, 15 décembre 2018 09:47

Télécharger (11,4 ko)

Voir les différences:

Subject: [PATCH 1/2] auth_oidc: verify and store id_token nonce  (fixes
 #29009)

 src/authentic2/utils.py              |  8 ++++----
 src/authentic2_auth_oidc/backends.py |  9 +++++++--
 src/authentic2_auth_oidc/views.py    |  6 ++++--
 tests/test_auth_oidc.py              | 27 ++++++++++++++++++---------
 4 files changed, 33 insertions(+), 17 deletions(-)
src/authentic2/utils.py
341 341
    return nonce
342 342

  
343 343

  
344
def record_authentication_event(request, how):
344
def record_authentication_event(request, how, nonce=None):
345 345
    '''Record an authentication event in the session and in the database, in
346 346
       later version the database persistence can be removed'''
347 347
    from . import models
......
362 362
        'who': unicode(request.user)[:80],
363 363
        'how': how,
364 364
    }
365
    nonce = get_nonce(request)
365
    nonce = nonce or get_nonce(request)
366 366
    if nonce:
367 367
        kwargs['nonce'] = nonce
368 368
        event['nonce'] = nonce
......
388 388
    return None
389 389

  
390 390

  
391
def login(request, user, how, service_slug=None, **kwargs):
391
def login(request, user, how, service_slug=None, nonce=None, **kwargs):
392 392
    '''Login a user model, record the authentication event and redirect to next
393 393
       URL or settings.LOGIN_REDIRECT_URL.'''
394 394
    from . import hooks
......
400 400
    if constants.LAST_LOGIN_SESSION_KEY not in request.session:
401 401
        request.session[constants.LAST_LOGIN_SESSION_KEY] = \
402 402
            localize(to_current_timezone(last_login), True)
403
    record_authentication_event(request, how)
403
    record_authentication_event(request, how, nonce=nonce)
404 404
    hooks.call_hooks('event', name='login', user=user, how=how, service=service_slug)
405 405
    return continue_to_next_url(request, **kwargs)
406 406

  
src/authentic2_auth_oidc/backends.py
6 6
from jwcrypto.jwt import JWT
7 7
from jwcrypto.jwk import JWK
8 8

  
9
from django.core.exceptions import MultipleObjectsReturned
10 9
from django.utils.timezone import now
11 10
from django.contrib.auth import get_user_model
12 11
from django.contrib.auth.backends import ModelBackend
......
20 19

  
21 20

  
22 21
class OIDCBackend(ModelBackend):
23
    def authenticate(self, access_token=None, id_token=None, **kwargs):
22
    def authenticate(self, access_token=None, id_token=None, nonce=None, **kwargs):
24 23
        logger = logging.getLogger(__name__)
25 24
        if id_token is None:
26 25
            return
......
96 95
                               duration)
97 96
                return None
98 97

  
98
        id_token_nonce = getattr(id_token, 'nonce', None)
99
        if nonce and nonce != id_token_nonce:
100
            logger.warning('auth_oidc: id_token nonce %r != expected nonce %r',
101
                           id_token_nonce, nonce)
102
            return None
103

  
99 104
        User = get_user_model()
100 105
        user = None
101 106
        if provider.strategy == models.OIDCProvider.STRATEGY_FIND_UUID:
src/authentic2_auth_oidc/views.py
94 94
            return redirect(request, settings.LOGIN_REDIRECT_URL)
95 95
        try:
96 96
            issuer = oidc_state.get('issuer')
97
            oidc_request = oidc_state.get('request')
98
            nonce = oidc_request.get('nonce')
97 99
            provider = get_provider_by_issuer(issuer)
98 100
        except models.OIDCProvider.DoesNotExist:
99 101
            messages.warning(request, _('Unknown OpenID connect issuer'))
......
176 178
            return self.continue_to_next_url()
177 179
        logger.info(u'got token response %s', result)
178 180
        access_token = result.get('access_token')
179
        user = authenticate(access_token=access_token, id_token=result['id_token'])
181
        user = authenticate(access_token=access_token, nonce=nonce, id_token=result['id_token'])
180 182
        if user:
181 183
            # remember last tokens for logout
182 184
            tokens = request.session.setdefault('auth_oidc', {}).setdefault('tokens', [])
......
185 187
                'provider_pk': provider.pk,
186 188
            })
187 189
            request.session.modified = True
188
            login(request, user, 'oidc')
190
            login(request, user, 'oidc', nonce=nonce)
189 191
        else:
190 192
            messages.warning(request, _('No user found'))
191 193
        return self.continue_to_next_url()
tests/test_auth_oidc.py
21 21
                                        has_providers)
22 22
from authentic2_auth_oidc.models import OIDCProvider, OIDCClaimMapping
23 23
from authentic2.models import AttributeValue
24
from authentic2.utils import timestamp_from_datetime
24
from authentic2.utils import timestamp_from_datetime, last_authentication_event
25 25
from authentic2.a2_rbac.utils import get_default_ou
26 26
from authentic2.crypto import base64url_encode
27 27

  
......
166 166

  
167 167

  
168 168
def oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, extra_id_token=None,
169
                       extra_user_info=None, sub='john.doe'):
169
                       extra_user_info=None, sub='john.doe', nonce=None):
170 170
    token_endpoint = urlparse.urlparse(oidc_provider.token_endpoint)
171 171
    userinfo_endpoint = urlparse.urlparse(oidc_provider.userinfo_endpoint)
172 172
    token_revocation_endpoint = urlparse.urlparse(oidc_provider.token_revocation_endpoint)
......
181 181
                'aud': str(oidc_provider.client_id),
182 182
                'exp': timestamp_from_datetime(now() + datetime.timedelta(seconds=10)),
183 183
            }
184
            if nonce:
185
                id_token['nonce'] = nonce
184 186
            if extra_id_token:
185 187
                id_token.update(extra_id_token)
186 188

  
......
277 279
    assert query['client_id'] == str(oidc_provider.client_id)
278 280
    assert query['scope'] == 'openid'
279 281
    assert query['redirect_uri'] == 'http://testserver' + reverse('oidc-login-callback')
282
    # get the nonce
283
    nonce = app.session['auth_oidc'][query['state']]['request']['nonce']
280 284

  
281 285
    if oidc_provider.claims_parameter_supported:
282 286
        claims = json.loads(query['claims'])
......
312 316
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code,
313 317
                                extra_id_token={'aud': 'zz'}):
314 318
            response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
319
    with utils.check_log(caplog, 'expected nonce'):
320
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code):
321
            response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
315 322
    assert not hooks.auth_oidc_backend_modify_user
316 323
    with utils.check_log(caplog, 'created user'):
317
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code):
324
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
318 325
            response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
319 326
    assert len(hooks.auth_oidc_backend_modify_user) == 1
320 327
    assert set(hooks.auth_oidc_backend_modify_user[0]['kwargs']) >= set(['user', 'provider', 'user_info', 'id_token', 'access_token'])
......
330 337
    assert user.attributes.last_name == 'Doe'
331 338
    assert AttributeValue.objects.filter(content='John', verified=True).count() == 1
332 339
    assert AttributeValue.objects.filter(content='Doe', verified=False).count() == 1
340
    assert last_authentication_event(app.session)['nonce'] == nonce
333 341

  
334 342
    with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code,
335
                            extra_user_info={'family_name_verified': True}):
343
                            extra_user_info={'family_name_verified': True}, nonce=nonce):
336 344
        response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
337 345
    assert AttributeValue.objects.filter(content='Doe', verified=False).count() == 0
338 346
    assert AttributeValue.objects.filter(content='Doe', verified=True).count() == 1
339 347

  
340 348
    with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code,
341
                            extra_user_info={'ou': 'cassis'}):
349
                            extra_user_info={'ou': 'cassis'}, nonce=nonce):
342 350
        response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
343 351
    assert User.objects.count() == 1
344 352
    user = User.objects.get()
345 353
    assert user.ou == cassis
346 354

  
347
    with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code):
355
    with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
348 356
        response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
349 357
    assert User.objects.count() == 1
350 358
    user = User.objects.get()
......
403 411
    response = response.click(oidc_provider.name)
404 412
    location = urlparse.urlparse(response.location)
405 413
    query = check_simple_qs(urlparse.parse_qs(location.query))
414
    nonce = app.session['auth_oidc'][query['state']]['request']['nonce']
406 415

  
407 416
    # sub=john.doe, MUST not work
408 417
    with utils.check_log(caplog, 'cannot create user'):
409
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code):
418
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
410 419
            response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
411 420

  
412 421
    # sub=simple_user.uuid MUST work
413 422
    with utils.check_log(caplog, 'found user using UUID'):
414
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, sub=simple_user.uuid):
423
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, sub=simple_user.uuid, nonce=nonce):
415 424
            response = app.get(login_callback_url, params={'code': code, 'state': query['state']})
416 425

  
417 426
    assert urlparse.urlparse(response['Location']).path == '/'
......
427 436

  
428 437
    response = app.get(reverse('account_management'))
429 438
    with utils.check_log(caplog, 'revoked token from OIDC'):
430
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code):
439
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
431 440
            response = response.click(href='logout')
432 441
    assert 'https://idp.example.com/logout' in response.content
433
-