0005-auth_oidc-render-templated-claim-values-during-authn.patch
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 |
- |