auth_oidc: render templated claim values during authn (#37871)
This commit is contained in:
parent
556f3e169e
commit
2b46edf4c5
|
@ -1,5 +1,5 @@
|
||||||
# authentic2 - versatile identity manager
|
# authentic2 - versatile identity manager
|
||||||
# Copyright (C) 2010-2019 Entr'ouvert
|
# Copyright (C) 2010-2020 Entr'ouvert
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
# under the terms of the GNU Affero General Public License as published
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -31,6 +31,7 @@ from django_rbac.utils import get_ou_model
|
||||||
|
|
||||||
from authentic2.crypto import base64url_encode
|
from authentic2.crypto import base64url_encode
|
||||||
from authentic2 import app_settings, hooks
|
from authentic2 import app_settings, hooks
|
||||||
|
from authentic2.utils.template import Template
|
||||||
|
|
||||||
from . import models, utils
|
from . import models, utils
|
||||||
|
|
||||||
|
@ -167,7 +168,9 @@ class OIDCBackend(ModelBackend):
|
||||||
for claim_mapping in provider.claim_mappings.all():
|
for claim_mapping in provider.claim_mappings.all():
|
||||||
claim = claim_mapping.claim
|
claim = claim_mapping.claim
|
||||||
if claim_mapping.required:
|
if claim_mapping.required:
|
||||||
if claim_mapping.idtoken_claim and claim not in id_token:
|
if '{{' in claim or '{%' in claim:
|
||||||
|
logger.warning(u'claim \'%r\' is templated, it cannot be set as required')
|
||||||
|
elif claim_mapping.idtoken_claim and claim not in id_token:
|
||||||
logger.warning(u'auth_oidc: cannot create user missing required claim %r in '
|
logger.warning(u'auth_oidc: cannot create user missing required claim %r in '
|
||||||
u'id_token (%r)',
|
u'id_token (%r)',
|
||||||
claim, id_token)
|
claim, id_token)
|
||||||
|
@ -184,25 +187,33 @@ class OIDCBackend(ModelBackend):
|
||||||
user_ou = provider.ou
|
user_ou = provider.ou
|
||||||
save_user = False
|
save_user = False
|
||||||
mappings = []
|
mappings = []
|
||||||
|
context = id_token.as_dict(provider)
|
||||||
|
if need_user_info:
|
||||||
|
context.update(user_info or {})
|
||||||
|
|
||||||
for claim_mapping in provider.claim_mappings.all():
|
for claim_mapping in provider.claim_mappings.all():
|
||||||
claim = claim_mapping.claim
|
claim = claim_mapping.claim
|
||||||
if claim_mapping.idtoken_claim:
|
if claim_mapping.idtoken_claim:
|
||||||
source = id_token
|
source = id_token
|
||||||
else:
|
else:
|
||||||
source = user_info
|
source = user_info
|
||||||
if claim not in source:
|
if claim not in source and not ('{{' in claim or '{%' in claim):
|
||||||
continue
|
continue
|
||||||
value = source.get(claim)
|
verified = False
|
||||||
attribute = claim_mapping.attribute
|
attribute = claim_mapping.attribute
|
||||||
|
if '{{' in claim or '{%' in claim:
|
||||||
|
template = Template(claim)
|
||||||
|
value = template.render(context=context)
|
||||||
|
# xxx missing verification logic for templated claims
|
||||||
|
else:
|
||||||
|
value = source.get(claim)
|
||||||
|
if claim_mapping.verified == models.OIDCClaimMapping.VERIFIED_CLAIM:
|
||||||
|
verified = bool(source.get(claim + '_verified', False))
|
||||||
if attribute == 'ou__slug' and value in ou_map:
|
if attribute == 'ou__slug' and value in ou_map:
|
||||||
user_ou = ou_map[value]
|
user_ou = ou_map[value]
|
||||||
continue
|
continue
|
||||||
if claim_mapping.verified == models.OIDCClaimMapping.VERIFIED_CLAIM:
|
if claim_mapping.verified == models.OIDCClaimMapping.ALWAYS_VERIFIED:
|
||||||
verified = bool(source.get(claim + '_verified', False))
|
|
||||||
elif claim_mapping.verified == models.OIDCClaimMapping.ALWAYS_VERIFIED:
|
|
||||||
verified = True
|
verified = True
|
||||||
else:
|
|
||||||
verified = False
|
|
||||||
mappings.append((attribute, value, verified))
|
mappings.append((attribute, value, verified))
|
||||||
|
|
||||||
# find en email in mappings
|
# find en email in mappings
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# authentic2 - versatile identity manager
|
# authentic2 - versatile identity manager
|
||||||
# Copyright (C) 2010-2019 Entr'ouvert
|
# Copyright (C) 2010-2020 Entr'ouvert
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
# under the terms of the GNU Affero General Public License as published
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -42,6 +42,7 @@ from authentic2_auth_oidc.utils import (
|
||||||
parse_id_token, IDToken, get_providers, has_providers, register_issuer,
|
parse_id_token, IDToken, get_providers, has_providers, register_issuer,
|
||||||
IDTokenError)
|
IDTokenError)
|
||||||
from authentic2_auth_oidc.models import OIDCProvider, OIDCClaimMapping
|
from authentic2_auth_oidc.models import OIDCProvider, OIDCClaimMapping
|
||||||
|
from authentic2.models import Attribute
|
||||||
from authentic2.models import AttributeValue
|
from authentic2.models import AttributeValue
|
||||||
from authentic2.utils import timestamp_from_datetime, last_authentication_event
|
from authentic2.utils import timestamp_from_datetime, last_authentication_event
|
||||||
from authentic2.a2_rbac.utils import get_default_ou
|
from authentic2.a2_rbac.utils import get_default_ou
|
||||||
|
@ -242,6 +243,7 @@ def oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, extra_id_token
|
||||||
'iat': timestamp_from_datetime(now()),
|
'iat': timestamp_from_datetime(now()),
|
||||||
'aud': str(oidc_provider.client_id),
|
'aud': str(oidc_provider.client_id),
|
||||||
'exp': timestamp_from_datetime(now() + datetime.timedelta(seconds=10)),
|
'exp': timestamp_from_datetime(now() + datetime.timedelta(seconds=10)),
|
||||||
|
'name': 'doe',
|
||||||
}
|
}
|
||||||
if nonce:
|
if nonce:
|
||||||
id_token['nonce'] = nonce
|
id_token['nonce'] = nonce
|
||||||
|
@ -290,6 +292,8 @@ def oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, extra_id_token
|
||||||
'given_name': 'John',
|
'given_name': 'John',
|
||||||
'family_name': 'Doe',
|
'family_name': 'Doe',
|
||||||
'email': 'john.doe@example.com',
|
'email': 'john.doe@example.com',
|
||||||
|
'phone_number': '0123456789',
|
||||||
|
'nickname': 'Hefty',
|
||||||
}
|
}
|
||||||
if extra_user_info:
|
if extra_user_info:
|
||||||
user_info.update(extra_user_info)
|
user_info.update(extra_user_info)
|
||||||
|
@ -665,3 +669,73 @@ def test_invalid_kid(app, caplog, code, oidc_provider_rsa,
|
||||||
with utils.check_log(caplog, message='Missing Key ID', levelname='WARNING'):
|
with utils.check_log(caplog, message='Missing Key ID', levelname='WARNING'):
|
||||||
with oidc_provider_mock(oidc_provider_rsa, oidc_provider_jwkset, code, nonce=nonce, kid=None):
|
with oidc_provider_mock(oidc_provider_rsa, oidc_provider_jwkset, code, nonce=nonce, kid=None):
|
||||||
response = app.get(login_callback_url(oidc_provider_rsa), params={'code': code, 'state': query['state']})
|
response = app.get(login_callback_url(oidc_provider_rsa), params={'code': code, 'state': query['state']})
|
||||||
|
|
||||||
|
|
||||||
|
def test_templated_claim_mapping(app, caplog, code, oidc_provider, oidc_provider_jwkset):
|
||||||
|
get_providers.cache.clear()
|
||||||
|
has_providers.cache.clear()
|
||||||
|
|
||||||
|
Attribute.objects.create(
|
||||||
|
name='pro_phone',
|
||||||
|
label='professonial phone',
|
||||||
|
kind='phone_number',
|
||||||
|
asked_on_registration=True
|
||||||
|
)
|
||||||
|
# no default mapping
|
||||||
|
OIDCClaimMapping.objects.all().delete()
|
||||||
|
|
||||||
|
OIDCClaimMapping.objects.create(
|
||||||
|
provider=oidc_provider,
|
||||||
|
attribute='username',
|
||||||
|
idtoken_claim=False,
|
||||||
|
claim='{{ given_name }} "{{ nickname }}" {{ family_name }}',
|
||||||
|
)
|
||||||
|
OIDCClaimMapping.objects.create(
|
||||||
|
provider=oidc_provider,
|
||||||
|
attribute='pro_phone',
|
||||||
|
idtoken_claim=False,
|
||||||
|
claim='(prefix +33) {{ phone_number }}',
|
||||||
|
)
|
||||||
|
OIDCClaimMapping.objects.create(
|
||||||
|
provider=oidc_provider,
|
||||||
|
attribute='email',
|
||||||
|
idtoken_claim=False,
|
||||||
|
claim='{{ given_name }}@foo.bar',
|
||||||
|
)
|
||||||
|
# last one, with an idtoken claim
|
||||||
|
OIDCClaimMapping.objects.create(
|
||||||
|
provider=oidc_provider,
|
||||||
|
attribute='last_name',
|
||||||
|
idtoken_claim=True,
|
||||||
|
claim='{{ name|upper }}',
|
||||||
|
)
|
||||||
|
# typo in template string
|
||||||
|
OIDCClaimMapping.objects.create(
|
||||||
|
provider=oidc_provider,
|
||||||
|
attribute='first_name',
|
||||||
|
idtoken_claim=True,
|
||||||
|
claim='{{ given_name',
|
||||||
|
)
|
||||||
|
oidc_provider.save()
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
assert User.objects.count() == 0
|
||||||
|
|
||||||
|
response = app.get('/').maybe_follow()
|
||||||
|
response = response.click(oidc_provider.name)
|
||||||
|
location = urlparse.urlparse(response.location)
|
||||||
|
query = check_simple_qs(urlparse.parse_qs(location.query))
|
||||||
|
nonce = app.session['auth_oidc'][query['state']]['request']['nonce']
|
||||||
|
|
||||||
|
with oidc_provider_mock(oidc_provider, oidc_provider_jwkset, code, nonce=nonce):
|
||||||
|
response = app.get(login_callback_url(oidc_provider), params={'code': code, 'state': query['state']}).maybe_follow()
|
||||||
|
|
||||||
|
assert User.objects.count() == 1
|
||||||
|
user = User.objects.first()
|
||||||
|
|
||||||
|
assert user.username == 'John "Hefty" Doe'
|
||||||
|
assert user.attributes.pro_phone == '(prefix +33) 0123456789'
|
||||||
|
assert user.email == 'John@foo.bar'
|
||||||
|
assert user.last_name == 'DOE'
|
||||||
|
# typo in template string, no rendering
|
||||||
|
assert user.first_name == '{{ given_name'
|
||||||
|
|
Loading…
Reference in New Issue