collective / collective/pas.plugins.oidc

Allow merging selected `id_token` claims into `userinfo` response

Open
#84 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
6
Forks
16
PR merge metrics
No merged PRs in 30d

Description

In some OpenID Connect providers (e.g. Microsoft Entra ID / Azure AD), the `userinfo_endpoint` returns a limited subset of the claims that are already present in the `id_token`.

Currently, `pas.plugins.oidc` retrieves the `id_token` claims from the token response, but if a `userinfo_endpoint` is configured, the call to the endpoint replaces the previously extracted claims.

This means that attributes available in the `id_token` may be lost if they are not also returned by the userinfo endpoint.

I am not sure if this is an issue with the configuration of my provider or if I did something wrong, but I overcome this issue by patching [the `get_user_info` function](https://github.com/collective/pas.plugins.oidc/blob/7c3fe5fed7ae4f961a35f0af4d636838023a707a/src/pas/plugins/oidc/utils.py#L177-L207):

```python
def get_user_info(client, state, args, method="POST") -> message.OpenIDSchema | dict:
resp = client.do_access_token_request(
state=state,
request_args=args,
authn_method="client_secret_basic",
)
user_info = {}
if isinstance(resp, message.AccessTokenResponse):
# If it's an AccessTokenResponse the information in the response will be stored
# in the client instance with state as the key for future use.
id_token = user_info = resp.to_dict().get("id_token", {})
if client.userinfo_endpoint:
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
try:
user_info = client.do_user_info_request(method=method, state=state)
except RequestError as exc:
logger.error(
"Authentication failed, probably missing openid scope",
exc_info=exc,
)
user_info = {}

#
_name_mapping = {
"foo": "bar"
}
_extended_user_info = {
_name_mapping.get(key, key): id_token.get(key)
for key in (
"foo",
"preferred_username",
)
if key in id_token and key not in user_info
}
user_info.update(_extended_user_info)
#

# userinfo in an instance of OpenIDSchema or ErrorResponse
# It could also be dict, if there is no userinfo_endpoint
if not (user_info and isinstance(user_info, message.OpenIDSchema | dict)):
logger.error(
"Authentication failed, invalid response %s %s", resp, user_info
)
user_info = {}
elif isinstance(resp, message.TokenErrorResponse):
logger.error("Token error response: %s", resp.to_json())
else:
logger.error("Authentication failed %s", resp)

return user_info
```

It would be nice to have that mapping configurable in the plugin properties.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.