Projet

Général

Profil

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

Paul Marillonnet, 28 avril 2022 16:25

Télécharger (14,7 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          | 177 ++++++++++++------
 .../migrations/0004_auto_20171017_1522.py     |   4 +
 src/authentic2_auth_oidc/models.py            |   5 +
 tests/test_auth_oidc.py                       |  55 ++++++
 4 files changed, 181 insertions(+), 60 deletions(-)
src/authentic2_auth_oidc/backends.py
132 132
            logger.warning('auth_oidc: id_token nonce %r != expected nonce %r', id_token_nonce, nonce)
133 133
            return None
134 134

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

  
166
        user_info = None
167 148
        if need_user_info:
168 149
            if not access_token:
169 150
                logger.warning('auth_oidc: need user info for some claims, but no access token was returned')
......
185 166
                    logger.warning('auth_oidc: bad JSON in user info response, %s (%r)', e, response.content)
186 167
                else:
187 168
                    logger.debug('auth_oidc: user_info content %s', user_info)
188

  
189
        # check for required claims
190
        for claim_mapping in provider.claim_mappings.all():
191
            claim = claim_mapping.claim
192
            if claim_mapping.required:
193
                if '{{' in claim or '{%' in claim:
194
                    logger.warning('claim \'%r\' is templated, it cannot be set as required')
195
                elif claim_mapping.idtoken_claim and claim not in id_token:
196
                    logger.warning(
197
                        'auth_oidc: cannot create user missing required claim %r in id_token (%r)',
198
                        claim,
199
                        id_token,
200
                    )
201
                    return None
202
                elif not user_info or claim not in user_info:
203
                    logger.warning(
204
                        'auth_oidc: cannot create user missing required claim %r in user_info (%r)',
205
                        claim,
206
                        user_info,
207
                    )
208
                    return None
209

  
210
        # map claims to attributes or user fields
211
        # mapping is done before eventual creation of user as EMAIL_IS_UNIQUE needs to know if the
212
        # mapping will provide some mail to us
213
        ou_map = {ou.slug: ou for ou in OrganizationalUnit.cached()}
214
        user_ou = provider.ou
215
        save_user = False
216
        mappings = []
217
        context = id_token_content.copy()
218
        if need_user_info:
219
            context.update(user_info or {})
169
                    context.update(user_info or {})
220 170

  
221 171
        for claim_mapping in provider.claim_mappings.all():
222 172
            claim = claim_mapping.claim
......
243 193
                verified = True
244 194
            mappings.append((attribute, value, verified))
245 195

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

  
246 217
        # find en email in mappings
247 218
        email = None
248 219
        for attribute, value, verified in mappings:
249 220
            if attribute == 'email':
250 221
                email = value
251 222

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

  
255
            user = None
256
            if not email:
257
                logger.error(
258
                    'auth_oidc: email lookup activated for provider "%s" yet no email received', provider
259
                )
260
            elif app_settings.A2_EMAIL_IS_UNIQUE:
261
                try:
262
                    user = User.objects.get(email__iexact=email, is_active=True)
263
                except User.DoesNotExist:
264
                    logger.warning('auth_oidc: user with email "%s" not found', email)
265
                else:
266
                    logger.info('auth_oidc: found user using email "%s": %s', email, user)
267
            elif provider.ou and provider.ou.email_is_unique:
268
                logger.info(
269
                    'auth_oidc: searching user with email "%s" within provider %s\'s ou %s',
270
                    email,
271
                    provider,
272
                    provider.ou,
273
                )
274
                try:
275
                    user = User.objects.get(email__iexact=email, ou=provider.ou, is_active=True)
276
                except User.DoesNotExist:
277
                    logger.warning(
278
                        'auth_oidc: user with email "%s" not found with provider %s\'s ou %s',
279
                        email,
280
                        provider,
281
                        provider.ou,
282
                    )
283
                else:
284
                    logger.info(
285
                        'auth_oidc: found user using email "%s": %s, within provider %s\'s ou %s',
286
                        email,
287
                        user,
288
                        provider,
289
                        provider.ou,
290
                    )
291
            else:
292
                logger.warning(
293
                    'auth_oidc: email uniqueness constraints unsufficient for email "%s" lookup, '
294
                    'global uniqueness deactivated and provider "%s"\'s ou-wise uniqueness '
295
                    'deactivated too',
296
                    email,
297
                    provider,
298
                )
299
        else:
300
            try:
301
                user = User.objects.get(
302
                    oidc_account__provider=provider, oidc_account__sub=id_token.sub, is_active=True
303
                )
304
            except User.DoesNotExist:
305
                pass
306
            else:
307
                logger.info('auth_oidc: found user using with sub "%s": %s', id_token.sub, user)
308

  
252 309
        # eventually create a new user or link to an existing one based on email
253 310
        created_user = False
254 311
        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
42 42
    STRATEGY_CREATE = 'create'
43 43
    STRATEGY_FIND_UUID = 'find-uuid'
44 44
    STRATEGY_FIND_USERNAME = 'find-username'
45
    STRATEGY_FIND_EMAIL = 'find-email'
45 46
    STRATEGY_NONE = 'none'
46 47

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

  
680 680

  
681
def test_strategy_find_email(app, caplog, code, oidc_provider, oidc_provider_jwkset, simple_user):
682
    OIDCClaimMapping.objects.all().delete()
683
    OIDCClaimMapping.objects.create(
684
        provider=oidc_provider,
685
        claim='email',
686
        attribute='email',
687
        idtoken_claim=False,  # served by user_info endpoint
688
    )
689
    oidc_provider.strategy = oidc_provider.STRATEGY_FIND_EMAIL
690
    oidc_provider.save()
691
    oidc_provider.ou.email_is_unique = True
692
    oidc_provider.ou.save()
693

  
694
    assert User.objects.count() == 1
695

  
696
    response = app.get('/').maybe_follow()
697
    assert oidc_provider.name in response.text
698
    response = response.click(oidc_provider.name)
699
    location = urllib.parse.urlparse(response.location)
700
    query = QueryDict(location.query)
701
    state = query['state']
702
    nonce = query['nonce']
703

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

  
708
    simple_user.email = 'sub@example.com'
709
    simple_user.save()
710

  
711
    with utils.check_log(caplog, 'cannot create user'):
712
        with oidc_provider_mock(
713
            oidc_provider, oidc_provider_jwkset, code, sub='sub@example.com', nonce=nonce
714
        ):
715
            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
716

  
717
    simple_user.email = 'john.doe@example.com'
718
    simple_user.save()
719

  
720
    with utils.check_log(caplog, 'found user using email'):
721
        with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
722
            response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': state})
723

  
724
    assert urllib.parse.urlparse(response['Location']).path == '/'
725
    assert User.objects.count() == 1
726
    user = User.objects.get()
727
    # verify user was not modified
728
    assert user.username == 'user'
729
    assert user.first_name == 'Jôhn'
730
    assert user.last_name == 'Dôe'
731
    assert user.email == 'john.doe@example.com'
732
    assert user.attributes.first_name == 'Jôhn'
733
    assert user.attributes.last_name == 'Dôe'
734

  
735

  
681 736
def test_strategy_create(app, caplog, code, oidc_provider, oidc_provider_jwkset):
682 737
    oidc_provider.ou.email_is_unique = True
683 738
    oidc_provider.ou.save()
684
-