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
This commit is contained in:
Benjamin Dauvergne 2018-09-19 16:23:55 +02:00
parent cc8ec18ea4
commit 68f87021c2
5 changed files with 65 additions and 3 deletions

View File

@ -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'),
),
]

View File

@ -98,6 +98,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(
@ -144,6 +147,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 '<OIDCProvider %r>' % self.issuer

View File

@ -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)

View File

@ -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():

View File

@ -88,10 +88,21 @@ def oidc_provider_jwkset():
jwkset.add(key)
return jwkset
OIDC_PROVIDER_PARAMS = [
{},
{
'idtoken_algo': OIDCProvider.ALGO_HMAC
},
{
'claims_parameter_supported': True,
}
]
@pytest.fixture(params=[OIDCProvider.ALGO_RSA, OIDCProvider.ALGO_HMAC])
@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