Skip to content

Allow merging selected id_token claims into userinfo response #84

@ale-rt

Description

@ale-rt

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:

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 = {}

        # <monkey>
        _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)
        # </monkey>

        # 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions