idp_oidc: serve user profile information in UserInfo endpoint (#69416)

This commit is contained in:
Paul Marillonnet 2022-09-23 11:41:52 +02:00
parent 3e7ae285a7
commit 25a7c66673
2 changed files with 160 additions and 6 deletions

View File

@ -492,6 +492,7 @@ def authorize_for_client(request, client, redirect_uri):
scopes=' '.join(scopes),
session_key=request.session.session_key,
expired=expired,
profile=profile,
)
acr = '0'
if nonce is not None and last_auth.get('nonce') == nonce:
@ -634,6 +635,7 @@ def idtoken_from_user_credential(request):
)
username = request.POST.get('username')
scope = request.POST.get('scope')
profile_id = request.POST.get('profile')
# scope is ignored, we used the configured scope
@ -695,11 +697,28 @@ def idtoken_from_user_credential(request):
expires_in = datetime.timedelta(seconds=app_settings.ACCESS_TOKEN_DURATION)
else:
expires_in = client.access_token_duration
profile = None
if profile_id:
if not client.activate_user_profiles:
raise AccessDenied(
_('User profile requested yet client does not manage profiles.'), client=client
)
try:
profile = Profile.objects.get(id=profile_id, user=user)
except Profile.DoesNotExist:
raise AccessDenied(_('Invalid profile'), client=client)
access_token = models.OIDCAccessToken.objects.create(
client=client, user=user, scopes=' '.join(scopes), session_key='', expired=iat + expires_in
client=client,
user=user,
scopes=' '.join(scopes),
session_key='',
expired=iat + expires_in,
profile=profile,
)
# make id_token
id_token = utils.create_user_info(request, client, user, scopes, id_token=True)
id_token = utils.create_user_info(request, client, user, scopes, profile=profile, id_token=True)
exp = iat + idtoken_duration(client)
id_token.update(
{
@ -748,6 +767,7 @@ def tokens_from_authz_code(request):
scopes=oidc_code.scopes,
session_key=oidc_code.session_key,
expired=expired,
profile=oidc_code.profile,
)
start = now()
acr = '0'
@ -835,7 +855,11 @@ def user_info(request, *args, **kwargs):
try:
access_token = authenticate_access_token(request)
user_info = utils.create_user_info(
request, access_token.client, access_token.user, access_token.scope_set()
request,
access_token.client,
access_token.user,
access_token.scope_set(),
profile=access_token.profile,
)
return JsonResponse(user_info)
except OIDCException as e:

View File

@ -30,11 +30,19 @@ from jwcrypto.jwt import JWT
from authentic2.custom_user.models import Profile, ProfileType
from authentic2.utils.misc import make_url
from authentic2_idp_oidc.models import OIDCAccessToken, OIDCCode
from authentic2_idp_oidc.utils import get_jwkset, make_pairwise_sub, make_sub, reverse_pairwise_sub
from authentic2_idp_oidc.models import OIDCAccessToken, OIDCClient, OIDCCode
from authentic2_idp_oidc.utils import (
base64url,
get_first_ec_sig_key,
get_first_rsa_sig_key,
get_jwkset,
make_pairwise_sub,
make_sub,
reverse_pairwise_sub,
)
from .. import utils
from .conftest import client_authentication_headers
from .conftest import bearer_authentication_headers, client_authentication_headers
User = get_user_model()
@ -219,6 +227,17 @@ def test_login_profile_selection(app, oidc_client, profile_user, profile_setting
assert claims['entity_data_au'] == 'ie'
assert claims['entity_data_ts'] == 'rn'
user_info_url = make_url('oidc-user-info')
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
assert response.json['profile_type'] == 'one-delegate-type'
assert response.json['profile_identifier'] == 'Entity 1011'
assert response.json['email'] == 'delegate@example1011.org'
assert response.json['sub'] == claims['sub']
assert response.json['entity_name'] == 'Foobar'
assert response.json['entity_data_au'] == 'ie'
assert response.json['entity_data_ts'] == 'rn'
def test_login_implicit(app, oidc_client, profile_user, profile_settings):
oidc_client.idtoken_algo = oidc_client.ALGO_HMAC
@ -277,6 +296,17 @@ def test_login_implicit(app, oidc_client, profile_user, profile_settings):
assert claims['entity_data_au'] == 'ie'
assert claims['entity_data_ts'] == 'rn'
user_info_url = make_url('oidc-user-info')
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
assert response.json['profile_type'] == 'one-delegate-type'
assert response.json['profile_identifier'] == 'Entity 1011'
assert response.json['email'] == 'delegate@example1011.org'
assert response.json['sub'] == claims['sub']
assert response.json['entity_name'] == 'Foobar'
assert response.json['entity_data_au'] == 'ie'
assert response.json['entity_data_ts'] == 'rn'
def test_login_profile_reversible_sub(app, oidc_client, profile_user, profile_settings):
oidc_client.idtoken_algo = oidc_client.ALGO_EC
@ -428,3 +458,103 @@ def test_modify_user_info_hook(app, oidc_client, profile_settings, profile_user,
assert claims['email'] == 'def@ad.dre.ss'
assert claims['profile'] == profile_user.profiles.first().id
assert claims['customclaim'] == 'whatever'
def test_profile_selection_user_credentials_grant(app, oidc_client, admin, simple_user):
oidc_client.authorization_flow = OIDCClient.FLOW_RESOURCE_OWNER_CRED
oidc_client.scope = 'openid email'
oidc_client.save()
token_url = make_url('oidc-token')
if oidc_client.idtoken_algo == OIDCClient.ALGO_HMAC:
k = base64url(oidc_client.client_secret.encode('utf-8'))
jwk = JWK(kty='oct', k=force_text(k))
elif oidc_client.idtoken_algo == OIDCClient.ALGO_RSA:
jwk = get_first_rsa_sig_key()
elif oidc_client.idtoken_algo == OIDCClient.ALGO_EC:
jwk = get_first_ec_sig_key()
profile_type = ProfileType.objects.create(
name='One Manager Type',
slug='one-manager-type',
)
profile = Profile.objects.create(
user=simple_user,
identifier='abc',
profile_type=profile_type,
email='profile@ad.dr.ess',
)
params = {
'client_id': oidc_client.client_id,
'client_secret': oidc_client.client_secret,
'grant_type': 'password',
'username': simple_user.username,
'password': simple_user.username,
# profile id yet client doesn't active profile management
'profile': profile.id,
}
response = app.post(token_url, params=params, status=400)
assert response.json['error'] == 'access_denied'
assert response.json['error_description'] == 'User profile requested yet client does not manage profiles.'
oidc_client.activate_user_profiles = True
oidc_client.save()
# wrong profile id
params['profile'] = profile.id + 1
response = app.post(token_url, params=params, status=400)
assert response.json['error'] == 'access_denied'
assert response.json['error_description'] == 'Invalid profile'
another_profile = Profile.objects.create(
user=admin,
identifier='def',
profile_type=profile_type,
email='admin@ad.dr.ess',
)
# another user's profile
params['profile'] = another_profile.id
response = app.post(token_url, params=params, status=400)
assert response.json['error'] == 'access_denied'
# the oidc provider doesn't reveal that this is a valid profile id, linked to another user:
assert response.json['error_description'] == 'Invalid profile'
# correct profile
params['profile'] = profile.id
response = app.post(token_url, params=params)
token = response.json['id_token']
jwt = JWT()
jwt.deserialize(token, key=jwk)
claims = json.loads(jwt.claims)
assert set(claims) == {
'acr',
'aud',
'auth_time',
'exp',
'iat',
'iss',
'sub',
# profile-related claims in the id token:
'profile_type',
'profile_identifier',
'email',
'email_verified',
}
assert claims['profile_type'] == 'one-manager-type'
assert claims['profile_identifier'] == 'abc'
assert claims['email'] == 'profile@ad.dr.ess'
access_token = response.json['access_token']
user_info_url = make_url('oidc-user-info')
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
assert response.json['profile_type'] == 'one-manager-type'
assert response.json['profile_identifier'] == 'abc'
assert response.json['email'] == 'profile@ad.dr.ess'
assert response.json['sub'] == claims['sub']