Fix issue with access tokens claims not being imported using OIDC IDP Attribute Mappers (#21627)
Closes #9004 Co-authored-by: Armel Soro <armel@rm3l.org>
This commit is contained in:
parent
78262b2b53
commit
a8bca522c1
11 changed files with 124 additions and 19 deletions
|
@ -55,6 +55,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
|||
private scopesInput = "#scopes";
|
||||
private storeTokensSwitch = "#storeTokens";
|
||||
private storedTokensReadable = "#storedTokensReadable";
|
||||
private isAccessTokenJWT = "#isAccessTokenJWT";
|
||||
private acceptsPromptNoneForwardFromClientSwitch = "#acceptsPromptNone";
|
||||
private advancedSettingsToggle = ".pf-c-expandable-section__toggle";
|
||||
private passLoginHintSwitch = "#passLoginHint";
|
||||
|
@ -112,6 +113,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
|||
return this;
|
||||
}
|
||||
|
||||
public clickIsAccessTokenJWTSwitch() {
|
||||
cy.get(this.isAccessTokenJWT).parent().click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public clickAcceptsPromptNoneForwardFromClientSwitch() {
|
||||
cy.get(this.acceptsPromptNoneForwardFromClientSwitch).parent().click();
|
||||
return this;
|
||||
|
@ -219,6 +225,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
|||
return this;
|
||||
}
|
||||
|
||||
public assertIsAccessTokenJWTTurnedOn(isOn: boolean) {
|
||||
super.assertSwitchStateOn(cy.get(this.isAccessTokenJWT).parent(), isOn);
|
||||
return this;
|
||||
}
|
||||
|
||||
public assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(isOn: boolean) {
|
||||
super.assertSwitchStateOn(
|
||||
cy.get(this.acceptsPromptNoneForwardFromClientSwitch).parent(),
|
||||
|
@ -394,6 +405,8 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
|||
this.assertStoreTokensSwitchTurnedOn(true);
|
||||
this.clickStoredTokensReadableSwitch();
|
||||
this.assertStoredTokensReadableTurnedOn(true);
|
||||
this.clickIsAccessTokenJWTSwitch();
|
||||
this.assertIsAccessTokenJWTTurnedOn(true);
|
||||
this.clickTrustEmailSwitch();
|
||||
this.assertTrustEmailSwitchTurnedOn(true);
|
||||
this.clickAccountLinkingOnlySwitch();
|
||||
|
@ -406,8 +419,10 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
|||
this.selectSyncModeOption(SyncModeOption.legacy);
|
||||
|
||||
this.clickRevertBtn();
|
||||
cy.get(this.advancedSettingsToggle).scrollIntoView().click();
|
||||
this.assertStoreTokensSwitchTurnedOn(false);
|
||||
this.assertStoredTokensReadableTurnedOn(false);
|
||||
this.assertIsAccessTokenJWTTurnedOn(false);
|
||||
this.assertTrustEmailSwitchTurnedOn(false);
|
||||
this.assertAccountLinkingOnlySwitchTurnedOn(false);
|
||||
this.assertHideOnLoginPageSwitchTurnedOn(false);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"logoutUrl": "End session endpoint to use to logout user from external IDP.",
|
||||
"backchannelLogout": "Does the external IDP support backchannel logout?",
|
||||
"disableUserInfo": "Disable usage of User Info service to obtain additional user information? Default is to use this OIDC service.",
|
||||
"isAccessTokenJWT": "The Access Token received from the Identity Provider is a JWT and its claims will be accessible for mappers.",
|
||||
"userInfoUrl": "The User Info Url. This is optional.",
|
||||
"issuer": "The issuer identifier for the issuer of the response. If not provided, no validation will be performed.",
|
||||
"scopes": "The scopes to be sent when asking for authorization. It can be a space-separated list of scopes. Defaults to 'openid'.",
|
||||
|
|
|
@ -99,6 +99,7 @@
|
|||
"logoutUrl": "Logout URL",
|
||||
"backchannelLogout": "Backchannel logout",
|
||||
"disableUserInfo": "Disable user info",
|
||||
"isAccessTokenJWT": "Access Token is JWT",
|
||||
"userInfoUrl": "User Info URL",
|
||||
"issuer": "Issuer",
|
||||
"scopes": "Scopes",
|
||||
|
|
|
@ -132,6 +132,9 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
|
|||
<SwitchField field="config.disableUserInfo" label="disableUserInfo" />
|
||||
</>
|
||||
)}
|
||||
{isOIDC && (
|
||||
<SwitchField field="config.isAccessTokenJWT" label="isAccessTokenJWT" />
|
||||
)}
|
||||
<SwitchField field="trustEmail" label="trustEmail" fieldType="boolean" />
|
||||
<SwitchField
|
||||
field="linkOnly"
|
||||
|
|
|
@ -31,8 +31,6 @@ import org.keycloak.jose.jws.JWSInputException;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.adapters.action.AdminAction;
|
||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
|
@ -53,10 +51,9 @@ import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForM
|
|||
*/
|
||||
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||
|
||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
||||
|
||||
public KeycloakOIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
config.setAccessTokenJwt(true); // force access token JWT
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,13 +61,6 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
|||
return new KeycloakEndpoint(callback, realm, event, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
|
||||
// Don't verify audience on accessToken as it may not be there. It was verified on IDToken already
|
||||
JsonWebToken access = validateToken(response.getToken(), true);
|
||||
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
|
||||
}
|
||||
|
||||
protected static class KeycloakEndpoint extends OIDCEndpoint {
|
||||
|
||||
private KeycloakOIDCIdentityProvider provider;
|
||||
|
|
|
@ -97,6 +97,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
|
||||
public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration";
|
||||
public static final String EXCHANGE_PROVIDER = "EXCHANGE_PROVIDER";
|
||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
||||
private static final String BROKER_NONCE_PARAM = "BROKER_NONCE";
|
||||
|
||||
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||
|
@ -257,8 +258,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
}
|
||||
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
|
||||
|
||||
|
||||
// Don't verify audience on accessToken as it may not be there. It was verified on IDToken already
|
||||
if (getConfig().isAccessTokenJwt()) {
|
||||
JsonWebToken access = validateToken(response.getToken(), true);
|
||||
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
|
||||
}
|
||||
}
|
||||
|
||||
protected SimpleHttp getRefreshTokenRequest(KeycloakSession session, String refreshToken, String clientId, String clientSecret) {
|
||||
|
|
|
@ -18,13 +18,10 @@ package org.keycloak.broker.oidc;
|
|||
|
||||
import static org.keycloak.common.util.UriUtils.checkUrl;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
|
@ -34,6 +31,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
|||
|
||||
public static final String USE_JWKS_URL = "useJwksUrl";
|
||||
public static final String VALIDATE_SIGNATURE = "validateSignature";
|
||||
public static final String IS_ACCESS_TOKEN_JWT = "isAccessTokenJWT";
|
||||
|
||||
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
|
||||
super(identityProviderModel);
|
||||
|
@ -87,6 +85,14 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
|||
getConfig().put(VALIDATE_SIGNATURE, String.valueOf(validateSignature));
|
||||
}
|
||||
|
||||
public void setAccessTokenJwt(boolean accessTokenJwt) {
|
||||
getConfig().put(IS_ACCESS_TOKEN_JWT, String.valueOf(accessTokenJwt));
|
||||
}
|
||||
|
||||
public boolean isAccessTokenJwt() {
|
||||
return Boolean.parseBoolean(getConfig().get(IS_ACCESS_TOKEN_JWT));
|
||||
}
|
||||
|
||||
public boolean isUseJwksUrl() {
|
||||
return Boolean.valueOf(getConfig().get(USE_JWKS_URL));
|
||||
}
|
||||
|
|
|
@ -78,7 +78,6 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
|
||||
String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import java.util.List;
|
||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
|
||||
public class KcOidcAccessTokenOnlyClaimsUserAttributeMapperTest extends OidcUserAttributeMapperTest {
|
||||
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return new KcOidcBrokerConfiguration() {
|
||||
@Override
|
||||
public List<ClientRepresentation> createProviderClients() {
|
||||
List<ClientRepresentation> clientsRepList = super.createProviderClients();
|
||||
clientsRepList.stream()
|
||||
.flatMap(clientRepresentation -> clientRepresentation.getProtocolMappers().stream())
|
||||
.map(ProtocolMapperRepresentation::getConfig)
|
||||
.forEach(protocolMapperConfig -> {
|
||||
protocolMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
|
||||
protocolMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "false");
|
||||
protocolMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "false");
|
||||
});
|
||||
|
||||
return clientsRepList;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -73,10 +73,10 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
|
|||
client.setSecret(CLIENT_SECRET);
|
||||
|
||||
client.setRedirectUris(Collections.singletonList(getConsumerRoot() +
|
||||
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint/*"));
|
||||
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + getIDPAlias() + "/endpoint/*"));
|
||||
|
||||
client.setAdminUrl(getConsumerRoot() +
|
||||
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint");
|
||||
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + getIDPAlias() + "/endpoint");
|
||||
|
||||
OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+"));
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.models.IdentityProviderSyncMode;
|
||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
|
||||
public class OidcAccessTokenOnlyClaimsUserAttributeMapperTest extends OidcUserAttributeMapperTest {
|
||||
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return new KcOidcBrokerConfiguration() {
|
||||
|
||||
private static final String OIDC_IDP_ALIAS = "oidc-idp";
|
||||
|
||||
@Override
|
||||
public IdentityProviderRepresentation setUpIdentityProvider(
|
||||
IdentityProviderSyncMode syncMode) {
|
||||
final IdentityProviderRepresentation idp = createIdentityProvider(OIDC_IDP_ALIAS,
|
||||
OIDCIdentityProviderFactory.PROVIDER_ID);
|
||||
|
||||
final Map<String, String> config = idp.getConfig();
|
||||
applyDefaultConfiguration(config, syncMode);
|
||||
config.put(OIDCIdentityProviderConfig.IS_ACCESS_TOKEN_JWT, "true");
|
||||
|
||||
return idp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIDPAlias() {
|
||||
return OIDC_IDP_ALIAS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientRepresentation> createProviderClients() {
|
||||
List<ClientRepresentation> clientsRepList = super.createProviderClients();
|
||||
clientsRepList.stream()
|
||||
.flatMap(clientRepresentation -> clientRepresentation.getProtocolMappers().stream())
|
||||
.map(ProtocolMapperRepresentation::getConfig)
|
||||
.forEach(protocolMapperConfig -> {
|
||||
protocolMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
|
||||
protocolMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "false");
|
||||
protocolMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "false");
|
||||
});
|
||||
|
||||
return clientsRepList;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue