IdP Mappers ignored when performing external -> internal token exchange
Describe the bug
Keycloak_A has Keycloak_B configured as Identity Provider (IdP). I configured 3 mappers, see attached screenshots.
Only the "UserName Template Importer" of mapper category Preprocessor seems to work - all attribute importers, hardcoded, or based on claims in the IdP user info are ignored.
The exact same mappers work when the user is created by regular login on Keycloak_A using the Keycloak_B as IdP.
In my case I am using the mappers to avoid potential conflict between multiple IdPs to guarantee unique user names through the user name mapper but I also plan to ignore the IdP's email to respect the unicity constraints on the email address.
Version
16.1.1
Expected behavior
I expect the mappers to work during token exchange just as they would during regular user creation/login through the IdP.
Actual behavior
Mappers of type Attribute Importer/Hardcoded Attribute are ignored.
How to Reproduce?
- On a Keycloak instance A configure an IdP which is Keycloak instance B.
- Configure a Hardcoded Attribute mapper on the IdP which maps email to empty - see one of the attached screenshots.
- Create a user with an email address on Keycloak B.
- On Keycloak A login the user through the IdP using for instance the account client.
- Verify the user has no email attribute.
- Delete the user on Keycloak A
- Generate a token for the user directly on Keycloak B.
- Do token exchange against Keycloak A using the token from the previous step.
- Verify that email attribute on user has (erroneously) been set.
curl used for token exchange:
curl --location --request POST 'http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/token' \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
--data-urlencode "client_id=token-exchange-test" \
--data-urlencode "subject_token=${token}" \
--data-urlencode "subject_issuer=${idpName}"
Anything else?
The documentation mentions that token exchange can only work when a user to be imported through the IdP does not exist yet - where I imagine "does not exist" means user name and email as provided by the IdP do not match an existing user.
In my case I created a specific realm for token exchange - and I prefix user names with the IdP name as to make sure that this unicity is guaranteed. Stripping the email is the 2nd required element for this to work for my use case which consists of adding multiple IdPs which I do not control and which may have an overlap on user names and email addresses.
I'd appreciate any work-around on this issue.
@thaDude Thanks for the details.
When doing external -> internal-only the client mappers are executed. The mappers associated with the IdP are not.
I'm labeling this one as an enhancement and we appreciate it if you can contribute to the change. We should be able to prioritize it if more people are looking for this capability.
This sounds like the old issue KEYCLOAK-17793.
We need the attribute importers to work with token exchange in our project too. Currently, we using the workaround with the FixedUserAttributeMapper SPI mentioned in the old Jira issue.
I have this problem too, @twwd do you have an example of your workaround for this ?
@pushm0v see the linked Keycloak Jira issue
Is there an example of how to wire up the OIDC brokered UserAttributeMapper mentioned in KEYCLOAK-17793?
Implementation
src/main/java/de/twwd/keycloak/fixedattributemapper
package de.twwd.keycloak.fixedattributemapper;
import org.keycloak.broker.oidc.mappers.UserAttributeMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
public class FixedUserAttributeMapper extends UserAttributeMapper {
private static final String MAPPER_ID = "fixed-user-attribute-idp-mapper";
@Override
public void importNewUser(
KeycloakSession session, RealmModel realm, UserModel user,
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
super.importNewUser(session, realm, user, mapperModel, context);
this.updateBrokeredUser(session, realm, user, mapperModel, context);
}
@Override
public String getId() {
return MAPPER_ID;
}
@Override
public String getDisplayCategory() {
return "Attribute Importer";
}
@Override
public String getDisplayType() {
return "Fixed Attribute Importer";
}
}
src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
de.twwd.keycloak.fixedattributemapper.FixedUserAttributeMapper
Usage
In the Keycloak admin console, on your IdP, use this fixed-user-attribute-idp-mapper as mapper for all mapped attributes.
Hello, is the problem still present in version 22? I have the same problem. I create an attribute importer (wids) for my OCID provider (Microsoft), but the claims are not included in the access token that Keycloak creates. When I create an Access Token in Azure AD, the claims are included.
If so, how can I implement the fix shown above? I am currently using the official Docker image of Keycloak.
I wanted to bring to your attention that the previously suggested workaround mentioned in this issue is not working anymore on KeyCloak version 22.01.
The issue seems to be caused by changes in BrokeredIdentityContext.ContextData which no longer contains KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN and KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN. Due to this, the getClaimValue function from AbstractClaimMapper class always returns null.
If anyone has found an alternative workaround or a different solution to handle this in the latest release, please share it here.
When federating users through external-internal exchange the UserAttributeMapper is not able to resolve claims fetched from the user info endpoint at the IdP.
It was introduced by https://github.com/keycloak/keycloak/issues/8833.
The method org.keycloak.broker.oidc.mappers.AbstractClaimMapper#getClaimValue is returning before checking if there are claims from the user info endpoint in the brokering context.
@thaDude @kdrach @giuseppCl @twwd @flythebluesky @pushm0v Could you confirm the broker is actually resolving claims from the user info endpoint?
This can happen if you have disabled signature validation in your broker when you send a subject_token of type urn:ietf:params:oauth:token-type:jwt. Or when sending a urn:ietf:params:oauth:token-type:access_token like in the description.
@pedroigor when you say that only the client mappers fire during ext->int Token exchange, are you saying that the calling client has to map from its token to the account? How? I am not seeing this, as the client mapping does in the other direction.. from user account to token claims, not the other way around. This is why the IdP must be there representing the subject token issuer. Therefore if during token exchange this is the first time we are seeing this external token representing a subject, we must fire the IdP mappers to get map any claims into the generated user account. This is essential to external to internal token exchange as the consumer RS may want to know about specific mapped claims. As of release 23.0.1 is still not working. Is anyone actively working on this bug?