Projet

Général

Profil

0001-auth_oidc-add-a-STRATEGY_FIND_EMAIL-user-matching-pr.patch

Paul Marillonnet, 20 mai 2022 11:40

Télécharger (13,5 ko)

Voir les différences:

Subject: [PATCH] auth_oidc: add a STRATEGY_FIND_EMAIL user-matching provider
 option (#63729)

 src/authentic2_auth_oidc/backends.py          | 147 +++++++++++-------
 .../migrations/0004_auto_20171017_1522.py     |   4 +
 src/authentic2_auth_oidc/models.py            |   5 +
 tests/test_auth_oidc.py                       |  55 +++++++
 4 files changed, 151 insertions(+), 60 deletions(-)
src/authentic2_auth_oidc/backends.py
140 140
            logger.warning('auth_oidc: id_token nonce %r != expected nonce %r', id_token_nonce, nonce)
141 141
            return None
142 142

  
143
        User = get_user_model()
144
        user = None
145
        if provider.strategy == models.OIDCProvider.STRATEGY_FIND_UUID:
146
            # use the OP sub to find an user by UUUID
147
            # it means OP and RP share the same account base and OP is passing its UUID as sub
148
            try:
149
                user = User.objects.get(uuid=id_token.sub, is_active=True)
150
            except User.DoesNotExist:
151
                pass
152
            else:
153
                logger.info('auth_oidc: found user using UUID (=sub) "%s": %s', id_token.sub, user)
154
        elif provider.strategy == models.OIDCProvider.STRATEGY_FIND_USERNAME:
155
            users = User.objects.filter(username=id_token.sub, is_active=True).order_by('pk')
156
            if not users:
157
                logger.warning('auth_oidc: user with username (=sub) "%s" not found', id_token.sub)
158
            else:
159
                user = users[0]
160
                logger.info('auth_oidc: found user using username (=sub) "%s": %s', id_token.sub, user)
161
        else:
162
            try:
163
                user = User.objects.get(
164
                    oidc_account__provider=provider, oidc_account__sub=id_token.sub, is_active=True
165
                )
166
            except User.DoesNotExist:
167
                pass
168
            else:
169
                logger.info('auth_oidc: found user using with sub "%s": %s', id_token.sub, user)
143
        # map claims to attributes or user fields
144
        # mapping is done before eventual creation of user as EMAIL_IS_UNIQUE needs to know if the
145
        # mapping will provide some mail to us
146
        ou_map = {ou.slug: ou for ou in OrganizationalUnit.cached()}
147
        user_ou = provider.ou
148
        user_info = None
149
        save_user = False
150
        mappings = []
151
        context = id_token_content.copy()
170 152
        need_user_info = False
171 153
        for claim_mapping in provider.claim_mappings.all():
172 154
            need_user_info = need_user_info or not claim_mapping.idtoken_claim
173 155

  
174
        user_info = None
175 156
        if need_user_info:
176 157
            if not access_token:
177 158
                logger.warning('auth_oidc: need user info for some claims, but no access token was returned')
......
193 174
                    logger.warning('auth_oidc: bad JSON in user info response, %s (%r)', e, response.content)
194 175
                else:
195 176
                    logger.debug('auth_oidc: user_info content %s', user_info)
196

  
197
        # check for required claims
198
        for claim_mapping in provider.claim_mappings.all():
199
            claim = claim_mapping.claim
200
            if claim_mapping.required:
201
                if '{{' in claim or '{%' in claim:
202
                    logger.warning('claim \'%r\' is templated, it cannot be set as required')
203
                elif claim_mapping.idtoken_claim and claim not in id_token:
204
                    logger.warning(
205
                        'auth_oidc: cannot create user missing required claim %r in id_token (%r)',
206
                        claim,
207
                        id_token,
208
                    )
209
                    return None
210
                elif not user_info or claim not in user_info:
211
                    logger.warning(
212
                        'auth_oidc: cannot create user missing required claim %r in user_info (%r)',
213
                        claim,
214
                        user_info,
215
                    )
216
                    return None
217

  
218
        # map claims to attributes or user fields
219
        # mapping is done before eventual creation of user as EMAIL_IS_UNIQUE needs to know if the
220
        # mapping will provide some mail to us
221
        ou_map = {ou.slug: ou for ou in OrganizationalUnit.cached()}
222
        user_ou = provider.ou
223
        save_user = False
224
        mappings = []
225
        context = id_token_content.copy()
226
        if need_user_info:
227
            context.update(user_info or {})
177
                    context.update(user_info or {})
228 178

  
229 179
        for claim_mapping in provider.claim_mappings.all():
230 180
            claim = claim_mapping.claim
......
251 201
                verified = True
252 202
            mappings.append((attribute, value, verified))
253 203

  
204
        # check for required claims
205
        for claim_mapping in provider.claim_mappings.all():
206
            claim = claim_mapping.claim
207
            if claim_mapping.required:
208
                if '{{' in claim or '{%' in claim:
209
                    logger.warning('claim \'%r\' is templated, it cannot be set as required')
210
                elif claim_mapping.idtoken_claim and claim not in id_token:
211
                    logger.warning(
212
                        'auth_oidc: cannot create user missing required claim %r in id_token (%r)',
213
                        claim,
214
                        id_token,
215
                    )
216
                    return None
217
                elif not user_info or claim not in user_info:
218
                    logger.warning(
219
                        'auth_oidc: cannot create user missing required claim %r in user_info (%r)',
220
                        claim,
221
                        user_info,
222
                    )
223
                    return None
224

  
254 225
        # find en email in mappings
255 226
        email = None
256 227
        for attribute, value, verified in mappings:
257 228
            if attribute == 'email':
258 229
                email = value
259 230

  
231
        User = get_user_model()
232
        user = None
233
        if provider.strategy == models.OIDCProvider.STRATEGY_FIND_UUID:
234
            # use the OP sub to find an user by UUUID
235
            # it means OP and RP share the same account base and OP is passing its UUID as sub
236
            try:
237
                user = User.objects.get(uuid=id_token.sub, is_active=True)
238
            except User.DoesNotExist:
239
                pass
240
            else:
241
                logger.info('auth_oidc: found user using UUID (=sub) "%s": %s', id_token.sub, user)
242
        elif provider.strategy == models.OIDCProvider.STRATEGY_FIND_USERNAME:
243
            users = User.objects.filter(username=id_token.sub, is_active=True).order_by('pk')
244
            if not users:
245
                logger.warning('auth_oidc: user with username (=sub) "%s" not found', id_token.sub)
246
            else:
247
                user = users[0]
248
                logger.info('auth_oidc: found user using username (=sub) "%s": %s', id_token.sub, user)
249
        elif provider.strategy == models.OIDCProvider.STRATEGY_FIND_EMAIL:
250
            if email and id_token.sub != email:
251
                logger.warning(
252
                    'auth_oidc: email claim (%s) does not match subject identifier (%s) yet STRATEGY_FIND_EMAIL, fallback on email claim.',
253
                    email,
254
                    id_token.sub,
255
                )
256
            elif not email:
257
                logger.warning(
258
                    'auth_oidc: email claim absent yet STRATEGY_FIND_EMAIL is set, using subject identifier (%s) instead',
259
                    id_token.sub,
260
                )
261
                email = id_token.sub
262

  
263
            if not email:
264
                logger.error(
265
                    'auth_oidc: email lookup activated for provider "%s" yet no email received', provider
266
                )
267
            users = User.objects.filter(email__iexact=email, is_active=True)
268
            if not app_settings.A2_EMAIL_IS_UNIQUE and provider.ou:
269
                users = users.filter(ou=provider.ou)
270
            Lock.lock_email(email)
271
            try:
272
                user = users.get()
273
            except User.DoesNotExist:
274
                logger.warning('auth_oidc: user with email "%s" not found', email)
275
            else:
276
                logger.info('auth_oidc: found user using email "%s": %s', email, user)
277
        else:
278
            try:
279
                user = User.objects.get(
280
                    oidc_account__provider=provider, oidc_account__sub=id_token.sub, is_active=True
281
                )
282
            except User.DoesNotExist:
283
                pass
284
            else:
285
                logger.info('auth_oidc: found user using with sub "%s": %s', id_token.sub, user)
286

  
260 287
        # eventually create a new user or link to an existing one based on email
261 288
        created_user = False
262 289
        linked = False
src/authentic2_auth_oidc/migrations/0004_auto_20171017_1522.py
18 18
                    ('create', 'create if standard account matching failed'),
19 19
                    ('find-uuid', 'use sub to find existing user through UUID'),
20 20
                    ('find-username', 'use sub to find existing user through username'),
21
                    (
22
                        'find-email',
23
                        'use email claim (or sub if claim is absent) to find existing user through email',
24
                    ),
21 25
                    ('none', 'none'),
22 26
                ],
23 27
            ),
src/authentic2_auth_oidc/models.py
44 44
    STRATEGY_CREATE = 'create'
45 45
    STRATEGY_FIND_UUID = 'find-uuid'
46 46
    STRATEGY_FIND_USERNAME = 'find-username'
47
    STRATEGY_FIND_EMAIL = 'find-email'
47 48
    STRATEGY_NONE = 'none'
48 49

  
49 50
    STRATEGIES = [
50 51
        (STRATEGY_CREATE, _('create if standard account matching failed')),
51 52
        (STRATEGY_FIND_UUID, _('use sub to find existing user through UUID')),
52 53
        (STRATEGY_FIND_USERNAME, _('use sub to find existing user through username')),
54
        (
55
            STRATEGY_FIND_EMAIL,
56
            _('use email claim (or sub if claim is absent) to find existing user through email'),
57
        ),
53 58
        (STRATEGY_NONE, _('none')),
54 59
    ]
55 60
    ALGO_NONE = 0
tests/test_auth_oidc.py
663 663
    assert response.location.startswith('https://server.example.com/logout?')
664 664

  
665 665

  
666
def test_strategy_find_email(app, caplog, code, oidc_provider, oidc_provider_jwkset, simple_user):
667
    OIDCClaimMapping.objects.all().delete()
668
    OIDCClaimMapping.objects.create(
669
        provider=oidc_provider,
670
        claim='email',
671
        attribute='email',
672
        idtoken_claim=False,  # served by user_info endpoint
673
    )
674
    oidc_provider.strategy = oidc_provider.STRATEGY_FIND_EMAIL
675
    oidc_provider.save()
676
    oidc_provider.ou.email_is_unique = True
677
    oidc_provider.ou.save()
678

  
679
    assert User.objects.count() == 1
680

  
681
    response = app.get('/').maybe_follow()
682
    assert oidc_provider.name in response.text
683
    response = response.click(oidc_provider.name)
684
    location = urllib.parse.urlparse(response.location)
685
    query = QueryDict(location.query)
686
    state = query['state']
687
    nonce = query['nonce']
688

  
689
    with utils.check_log(caplog, 'cannot create user'):
690
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
691
            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
692

  
693
    simple_user.email = 'sub@example.com'
694
    simple_user.save()
695

  
696
    with utils.check_log(caplog, 'cannot create user'):
697
        with oidc_provider_mock(
698
            oidc_provider, oidc_provider_jwkset, code, sub='sub@example.com', nonce=nonce
699
        ):
700
            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
701

  
702
    simple_user.email = 'john.doe@example.com'
703
    simple_user.save()
704

  
705
    with utils.check_log(caplog, 'found user using email'):
706
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
707
            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
708

  
709
    assert urllib.parse.urlparse(response['Location']).path == '/'
710
    assert User.objects.count() == 1
711
    user = User.objects.get()
712
    # verify user was not modified
713
    assert user.username == 'user'
714
    assert user.first_name == 'Jôhn'
715
    assert user.last_name == 'Dôe'
716
    assert user.email == 'john.doe@example.com'
717
    assert user.attributes.first_name == 'Jôhn'
718
    assert user.attributes.last_name == 'Dôe'
719

  
720

  
666 721
def test_strategy_create(app, caplog, code, oidc_provider, oidc_provider_jwkset):
667 722
    oidc_provider.ou.email_is_unique = True
668 723
    oidc_provider.ou.save()
669
-