From 4b3f3e78c5d7293ed75dd81be31be344456118a7 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 19 Sep 2018 16:23:55 +0200 Subject: [PATCH] auth_oidc: add support for "claims" parameter (fixes #26565) It allows A2 to signal to OIDC OP that some claims are required, see : https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter --- ...oidcprovider_claims_parameter_supported.py | 19 ++++++++++++++ src/authentic2_auth_oidc/models.py | 18 +++++++++++++ src/authentic2_auth_oidc/utils.py | 4 ++- src/authentic2_auth_oidc/views.py | 3 +++ tests/test_auth_oidc.py | 26 ++++++++++++++++--- 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 src/authentic2_auth_oidc/migrations/0005_oidcprovider_claims_parameter_supported.py diff --git a/src/authentic2_auth_oidc/migrations/0005_oidcprovider_claims_parameter_supported.py b/src/authentic2_auth_oidc/migrations/0005_oidcprovider_claims_parameter_supported.py new file mode 100644 index 00000000..aadc5823 --- /dev/null +++ b/src/authentic2_auth_oidc/migrations/0005_oidcprovider_claims_parameter_supported.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentic2_auth_oidc', '0004_auto_20171017_1522'), + ] + + operations = [ + migrations.AddField( + model_name='oidcprovider', + name='claims_parameter_supported', + field=models.BooleanField(default=False, verbose_name='Claims parameter supported'), + ), + ] diff --git a/src/authentic2_auth_oidc/models.py b/src/authentic2_auth_oidc/models.py index 53a5d201..9ee5f9ec 100644 --- a/src/authentic2_auth_oidc/models.py +++ b/src/authentic2_auth_oidc/models.py @@ -92,6 +92,9 @@ class OIDCProvider(models.Model): default=ALGO_RSA, choices=ALGO_CHOICES, verbose_name=_('IDToken signature algorithm')) + claims_parameter_supported = models.BooleanField( + verbose_name=_('Claims parameter supported'), + default=False) # ou where new users should be created strategy = models.CharField( @@ -138,6 +141,21 @@ class OIDCProvider(models.Model): def __unicode__(self): return self.name + def authorization_claims_parameter(self): + idtoken_claims = {} + userinfo_claims = {} + for claim_mapping in self.claim_mappings.all(): + d = idtoken_claims if claim_mapping.idtoken_claim else userinfo_claims + value = {} + if claim_mapping.required: + value['essential'] = True + value = value or None + d[claim_mapping.claim] = value + return { + 'id_token': idtoken_claims, + 'userinfo': userinfo_claims, + } + def __repr__(self): return '' % self.issuer diff --git a/src/authentic2_auth_oidc/utils.py b/src/authentic2_auth_oidc/utils.py index 71bbd35c..6b5f1baf 100644 --- a/src/authentic2_auth_oidc/utils.py +++ b/src/authentic2_auth_oidc/utils.py @@ -258,6 +258,7 @@ def register_issuer(name, issuer=None, openid_configuration=None, verify=True, t else: raise ValueError(_('no common algorithm found for signing idtokens: %s') % openid_configuration['id_token_signing_alg_values_supported']) + claims_parameter_supported = openid_configuration.get('claims_parameter_supported') is True kwargs = dict( ou=ou or get_default_ou(), name=name, @@ -267,7 +268,8 @@ def register_issuer(name, issuer=None, openid_configuration=None, verify=True, t userinfo_endpoint=openid_configuration['userinfo_endpoint'], jwkset_json=jwkset_json, idtoken_algo=idtoken_algo, - strategy=models.OIDCProvider.STRATEGY_CREATE) + strategy=models.OIDCProvider.STRATEGY_CREATE, + claims_parameter_supported=claims_parameter_supported) if old_pk: models.OIDCProvider.objects.filter(pk=old_pk).update(**kwargs) return models.OIDCProvider.objects.get(pk=old_pk) diff --git a/src/authentic2_auth_oidc/views.py b/src/authentic2_auth_oidc/views.py index 7c61993d..ab384029 100644 --- a/src/authentic2_auth_oidc/views.py +++ b/src/authentic2_auth_oidc/views.py @@ -1,5 +1,6 @@ import uuid import logging +import json import requests @@ -35,6 +36,8 @@ def oidc_login(request, pk, next_url=None, *args, **kwargs): 'state': state, 'nonce': nonce, } + if provider.claims_parameter_supported: + params['claims'] = json.dumps(provider.authorization_claims_parameter()) if 'login_hint' in request.GET: params['login_hint'] = request.GET['login_hint'] if get_language(): diff --git a/tests/test_auth_oidc.py b/tests/test_auth_oidc.py index 48dd341d..cecabbae 100644 --- a/tests/test_auth_oidc.py +++ b/tests/test_auth_oidc.py @@ -88,10 +88,21 @@ def oidc_provider_jwkset(): jwkset.add(key) return jwkset - -@pytest.fixture(params=[OIDCProvider.ALGO_RSA, OIDCProvider.ALGO_HMAC]) +OIDC_PROVIDER_PARAMS = [ + {}, + { + 'idtoken_algo': OIDCProvider.ALGO_HMAC + }, + { + 'claims_parameter_supported': True, + } +] + + +@pytest.fixture(params=OIDC_PROVIDER_PARAMS) def oidc_provider(request, db, oidc_provider_jwkset): - idtoken_algo = request.param + idtoken_algo = request.param.get('idtoken_algo', OIDCProvider.ALGO_RSA) + claims_parameter_supported = request.param.get('claims_parameter_supported', False) from authentic2_auth_oidc.utils import get_provider, get_provider_by_issuer get_provider.cache.clear() get_provider_by_issuer.cache.clear() @@ -113,6 +124,7 @@ def oidc_provider(request, db, oidc_provider_jwkset): strategy=OIDCProvider.STRATEGY_CREATE, jwkset_json=jwkset, idtoken_algo=idtoken_algo, + claims_parameter_supported=claims_parameter_supported, ) provider.full_clean() OIDCClaimMapping.objects.create( @@ -266,6 +278,14 @@ def test_sso(app, caplog, code, oidc_provider, oidc_provider_jwkset, login_url, assert query['scope'] == 'openid' assert query['redirect_uri'] == 'http://testserver' + reverse('oidc-login-callback') + if oidc_provider.claims_parameter_supported: + claims = json.loads(query['claims']) + assert claims['id_token']['sub'] is None + assert claims['userinfo']['email']['essential'] + assert claims['userinfo']['given_name']['essential'] + assert claims['userinfo']['family_name']['essential'] + assert claims['userinfo']['ou'] is None + User = get_user_model() assert User.objects.count() == 0 -- 2.18.0