Projet

Général

Profil

0005-auth_oidc-render-templated-claim-values-during-authn.patch

Paul Marillonnet, 31 mars 2020 14:00

Télécharger (7,98 ko)

Voir les différences:

Subject: [PATCH 5/5] auth_oidc: render templated claim values during authn
 (#37871)

 src/authentic2_auth_oidc/backends.py | 29 +++++++----
 tests/test_auth_oidc.py              | 76 +++++++++++++++++++++++++++-
 2 files changed, 95 insertions(+), 10 deletions(-)
src/authentic2_auth_oidc/backends.py
1 1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
2
# Copyright (C) 2010-2020 Entr'ouvert
3 3
#
4 4
# This program is free software: you can redistribute it and/or modify it
5 5
# under the terms of the GNU Affero General Public License as published
......
31 31

  
32 32
from authentic2.crypto import base64url_encode
33 33
from authentic2 import app_settings, hooks
34
from authentic2.utils.template import Template
34 35

  
35 36
from . import models, utils
36 37

  
......
167 168
        for claim_mapping in provider.claim_mappings.all():
168 169
            claim = claim_mapping.claim
169 170
            if claim_mapping.required:
170
                if claim_mapping.idtoken_claim and claim not in id_token:
171
                if '{{' in claim or '{%' in claim:
172
                    logger.warning(u'claim \'%r\' is templated, it cannot be set as required')
173
                elif claim_mapping.idtoken_claim and claim not in id_token:
171 174
                    logger.warning(u'auth_oidc: cannot create user missing required claim %r in '
172 175
                                   u'id_token (%r)',
173 176
                                   claim, id_token)
......
184 187
        user_ou = provider.ou
185 188
        save_user = False
186 189
        mappings = []
190
        context = id_token.as_dict(provider=provider)
191
        if need_user_info:
192
            context.update(user_info or {})
193

  
187 194
        for claim_mapping in provider.claim_mappings.all():
188 195
            claim = claim_mapping.claim
189 196
            if claim_mapping.idtoken_claim:
190 197
                source = id_token
191 198
            else:
192 199
                source = user_info
193
            if claim not in source:
200
            if claim not in source and not ('{{' in claim or '{%' in claim):
194 201
                continue
195
            value = source.get(claim)
202
            verified = False
196 203
            attribute = claim_mapping.attribute
204
            if '{{' in claim or '{%' in claim:
205
                template = Template(claim)
206
                value = template.render(context=context)
207
                # xxx missing verification logic for templated claims
208
            else:
209
                value = source.get(claim)
210
                if claim_mapping.verified == models.OIDCClaimMapping.VERIFIED_CLAIM:
211
                    verified = bool(source.get(claim + '_verified', False))
197 212
            if attribute == 'ou__slug' and value in ou_map:
198 213
                user_ou = ou_map[value]
199 214
                continue
200
            if claim_mapping.verified == models.OIDCClaimMapping.VERIFIED_CLAIM:
201
                verified = bool(source.get(claim + '_verified', False))
202
            elif claim_mapping.verified == models.OIDCClaimMapping.ALWAYS_VERIFIED:
215
            if claim_mapping.verified == models.OIDCClaimMapping.ALWAYS_VERIFIED:
203 216
                verified = True
204
            else:
205
                verified = False
206 217
            mappings.append((attribute, value, verified))
207 218

  
208 219
        # find en email in mappings
tests/test_auth_oidc.py
1 1
# -*- coding: utf-8 -*-
2 2
# authentic2 - versatile identity manager
3
# Copyright (C) 2010-2019 Entr'ouvert
3
# Copyright (C) 2010-2020 Entr'ouvert
4 4
#
5 5
# This program is free software: you can redistribute it and/or modify it
6 6
# under the terms of the GNU Affero General Public License as published
......
42 42
    parse_id_token, IDToken, get_providers, has_providers, register_issuer,
43 43
    IDTokenError)
44 44
from authentic2_auth_oidc.models import OIDCProvider, OIDCClaimMapping
45
from authentic2.models import Attribute
45 46
from authentic2.models import AttributeValue
46 47
from authentic2.utils import timestamp_from_datetime, last_authentication_event
47 48
from authentic2.a2_rbac.utils import get_default_ou
......
242 243
                'iat': timestamp_from_datetime(now()),
243 244
                'aud': str(oidc_provider.client_id),
244 245
                'exp': timestamp_from_datetime(now() + datetime.timedelta(seconds=10)),
246
                'name': 'doe',
245 247
            }
246 248
            if nonce:
247 249
                id_token['nonce'] = nonce
......
290 292
            'given_name': 'John',
291 293
            'family_name': 'Doe',
292 294
            'email': 'john.doe@example.com',
295
            'phone_number': '0123456789',
296
            'nickname': 'Hefty',
293 297
        }
294 298
        if extra_user_info:
295 299
            user_info.update(extra_user_info)
......
665 669
    with utils.check_log(caplog, message='Missing Key ID', levelname='WARNING'):
666 670
        with oidc_provider_mock(oidc_provider_rsa, oidc_provider_jwkset, code, nonce=nonce, kid=None):
667 671
            response = app.get(login_callback_url(oidc_provider_rsa), params={'code': code, 'state': query['state']})
672

  
673

  
674
def test_templated_claim_mapping(app, caplog, code, oidc_provider, oidc_provider_jwkset):
675
    get_providers.cache.clear()
676
    has_providers.cache.clear()
677

  
678
    Attribute.objects.create(
679
        name='pro_phone',
680
        label='professonial phone',
681
        kind='phone_number',
682
        asked_on_registration=True
683
    )
684
    # no default mapping
685
    OIDCClaimMapping.objects.all().delete()
686

  
687
    OIDCClaimMapping.objects.create(
688
        provider=oidc_provider,
689
        attribute='username',
690
        idtoken_claim=False,
691
        claim='{{ given_name }} "{{ nickname }}" {{ family_name }}',
692
    )
693
    OIDCClaimMapping.objects.create(
694
        provider=oidc_provider,
695
        attribute='pro_phone',
696
        idtoken_claim=False,
697
        claim='(prefix +33) {{ phone_number }}',
698
    )
699
    OIDCClaimMapping.objects.create(
700
        provider=oidc_provider,
701
        attribute='email',
702
        idtoken_claim=False,
703
        claim='{{ given_name }}@foo.bar',
704
    )
705
    # last one, with an idtoken claim
706
    OIDCClaimMapping.objects.create(
707
        provider=oidc_provider,
708
        attribute='last_name',
709
        idtoken_claim=True,
710
        claim='{{ name|upper }}',
711
    )
712
    # typo in template string
713
    OIDCClaimMapping.objects.create(
714
        provider=oidc_provider,
715
        attribute='first_name',
716
        idtoken_claim=True,
717
        claim='{{ given_name',
718
    )
719
    oidc_provider.save()
720

  
721
    User = get_user_model()
722
    assert User.objects.count() == 0
723

  
724
    response = app.get('/').maybe_follow()
725
    response = response.click(oidc_provider.name)
726
    location = urlparse.urlparse(response.location)
727
    query = check_simple_qs(urlparse.parse_qs(location.query))
728
    nonce = app.session['auth_oidc'][query['state']]['request']['nonce']
729

  
730
    with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
731
        response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': query['state']}).maybe_follow()
732

  
733
    assert User.objects.count() == 1
734
    user = User.objects.first()
735

  
736
    assert user.username == 'John "Hefty" Doe'
737
    assert user.attributes.pro_phone == '(prefix +33) 0123456789'
738
    assert user.email == 'John@foo.bar'
739
    assert user.last_name == 'DOE'
740
    # typo in template string, no rendering
741
    assert first_name == '{{ given_name'
668
-