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 scopesInput = "#scopes";
|
||||||
private storeTokensSwitch = "#storeTokens";
|
private storeTokensSwitch = "#storeTokens";
|
||||||
private storedTokensReadable = "#storedTokensReadable";
|
private storedTokensReadable = "#storedTokensReadable";
|
||||||
|
private isAccessTokenJWT = "#isAccessTokenJWT";
|
||||||
private acceptsPromptNoneForwardFromClientSwitch = "#acceptsPromptNone";
|
private acceptsPromptNoneForwardFromClientSwitch = "#acceptsPromptNone";
|
||||||
private advancedSettingsToggle = ".pf-c-expandable-section__toggle";
|
private advancedSettingsToggle = ".pf-c-expandable-section__toggle";
|
||||||
private passLoginHintSwitch = "#passLoginHint";
|
private passLoginHintSwitch = "#passLoginHint";
|
||||||
|
@ -112,6 +113,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clickIsAccessTokenJWTSwitch() {
|
||||||
|
cy.get(this.isAccessTokenJWT).parent().click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public clickAcceptsPromptNoneForwardFromClientSwitch() {
|
public clickAcceptsPromptNoneForwardFromClientSwitch() {
|
||||||
cy.get(this.acceptsPromptNoneForwardFromClientSwitch).parent().click();
|
cy.get(this.acceptsPromptNoneForwardFromClientSwitch).parent().click();
|
||||||
return this;
|
return this;
|
||||||
|
@ -219,6 +225,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public assertIsAccessTokenJWTTurnedOn(isOn: boolean) {
|
||||||
|
super.assertSwitchStateOn(cy.get(this.isAccessTokenJWT).parent(), isOn);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(isOn: boolean) {
|
public assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(isOn: boolean) {
|
||||||
super.assertSwitchStateOn(
|
super.assertSwitchStateOn(
|
||||||
cy.get(this.acceptsPromptNoneForwardFromClientSwitch).parent(),
|
cy.get(this.acceptsPromptNoneForwardFromClientSwitch).parent(),
|
||||||
|
@ -394,6 +405,8 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
this.assertStoreTokensSwitchTurnedOn(true);
|
this.assertStoreTokensSwitchTurnedOn(true);
|
||||||
this.clickStoredTokensReadableSwitch();
|
this.clickStoredTokensReadableSwitch();
|
||||||
this.assertStoredTokensReadableTurnedOn(true);
|
this.assertStoredTokensReadableTurnedOn(true);
|
||||||
|
this.clickIsAccessTokenJWTSwitch();
|
||||||
|
this.assertIsAccessTokenJWTTurnedOn(true);
|
||||||
this.clickTrustEmailSwitch();
|
this.clickTrustEmailSwitch();
|
||||||
this.assertTrustEmailSwitchTurnedOn(true);
|
this.assertTrustEmailSwitchTurnedOn(true);
|
||||||
this.clickAccountLinkingOnlySwitch();
|
this.clickAccountLinkingOnlySwitch();
|
||||||
|
@ -406,8 +419,10 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
|
||||||
this.selectSyncModeOption(SyncModeOption.legacy);
|
this.selectSyncModeOption(SyncModeOption.legacy);
|
||||||
|
|
||||||
this.clickRevertBtn();
|
this.clickRevertBtn();
|
||||||
|
cy.get(this.advancedSettingsToggle).scrollIntoView().click();
|
||||||
this.assertStoreTokensSwitchTurnedOn(false);
|
this.assertStoreTokensSwitchTurnedOn(false);
|
||||||
this.assertStoredTokensReadableTurnedOn(false);
|
this.assertStoredTokensReadableTurnedOn(false);
|
||||||
|
this.assertIsAccessTokenJWTTurnedOn(false);
|
||||||
this.assertTrustEmailSwitchTurnedOn(false);
|
this.assertTrustEmailSwitchTurnedOn(false);
|
||||||
this.assertAccountLinkingOnlySwitchTurnedOn(false);
|
this.assertAccountLinkingOnlySwitchTurnedOn(false);
|
||||||
this.assertHideOnLoginPageSwitchTurnedOn(false);
|
this.assertHideOnLoginPageSwitchTurnedOn(false);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"logoutUrl": "End session endpoint to use to logout user from external IDP.",
|
"logoutUrl": "End session endpoint to use to logout user from external IDP.",
|
||||||
"backchannelLogout": "Does the external IDP support backchannel logout?",
|
"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.",
|
"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.",
|
"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.",
|
"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'.",
|
"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",
|
"logoutUrl": "Logout URL",
|
||||||
"backchannelLogout": "Backchannel logout",
|
"backchannelLogout": "Backchannel logout",
|
||||||
"disableUserInfo": "Disable user info",
|
"disableUserInfo": "Disable user info",
|
||||||
|
"isAccessTokenJWT": "Access Token is JWT",
|
||||||
"userInfoUrl": "User Info URL",
|
"userInfoUrl": "User Info URL",
|
||||||
"issuer": "Issuer",
|
"issuer": "Issuer",
|
||||||
"scopes": "Scopes",
|
"scopes": "Scopes",
|
||||||
|
|
|
@ -132,6 +132,9 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
|
||||||
<SwitchField field="config.disableUserInfo" label="disableUserInfo" />
|
<SwitchField field="config.disableUserInfo" label="disableUserInfo" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{isOIDC && (
|
||||||
|
<SwitchField field="config.isAccessTokenJWT" label="isAccessTokenJWT" />
|
||||||
|
)}
|
||||||
<SwitchField field="trustEmail" label="trustEmail" fieldType="boolean" />
|
<SwitchField field="trustEmail" label="trustEmail" fieldType="boolean" />
|
||||||
<SwitchField
|
<SwitchField
|
||||||
field="linkOnly"
|
field="linkOnly"
|
||||||
|
|
|
@ -31,8 +31,6 @@ import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
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.AdminAction;
|
||||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
|
@ -53,10 +51,9 @@ import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForM
|
||||||
*/
|
*/
|
||||||
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||||
|
|
||||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
|
||||||
|
|
||||||
public KeycloakOIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
public KeycloakOIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||||
super(session, config);
|
super(session, config);
|
||||||
|
config.setAccessTokenJwt(true); // force access token JWT
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -64,13 +61,6 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||||
return new KeycloakEndpoint(callback, realm, event, this);
|
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 {
|
protected static class KeycloakEndpoint extends OIDCEndpoint {
|
||||||
|
|
||||||
private KeycloakOIDCIdentityProvider provider;
|
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 VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
|
||||||
public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration";
|
public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration";
|
||||||
public static final String EXCHANGE_PROVIDER = "EXCHANGE_PROVIDER";
|
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";
|
private static final String BROKER_NONCE_PARAM = "BROKER_NONCE";
|
||||||
|
|
||||||
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||||
|
@ -257,8 +258,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
|
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) {
|
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 static org.keycloak.common.util.UriUtils.checkUrl;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.common.enums.SslRequired;
|
import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Pedro Igor
|
* @author Pedro Igor
|
||||||
*/
|
*/
|
||||||
|
@ -34,6 +31,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||||
|
|
||||||
public static final String USE_JWKS_URL = "useJwksUrl";
|
public static final String USE_JWKS_URL = "useJwksUrl";
|
||||||
public static final String VALIDATE_SIGNATURE = "validateSignature";
|
public static final String VALIDATE_SIGNATURE = "validateSignature";
|
||||||
|
public static final String IS_ACCESS_TOKEN_JWT = "isAccessTokenJWT";
|
||||||
|
|
||||||
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
|
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
|
||||||
super(identityProviderModel);
|
super(identityProviderModel);
|
||||||
|
@ -87,6 +85,14 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||||
getConfig().put(VALIDATE_SIGNATURE, String.valueOf(validateSignature));
|
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() {
|
public boolean isUseJwksUrl() {
|
||||||
return Boolean.valueOf(getConfig().get(USE_JWKS_URL));
|
return Boolean.valueOf(getConfig().get(USE_JWKS_URL));
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,6 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
|
public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
|
||||||
String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
|
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.setSecret(CLIENT_SECRET);
|
||||||
|
|
||||||
client.setRedirectUris(Collections.singletonList(getConsumerRoot() +
|
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() +
|
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("+"));
|
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