Add a new identity provider for LinkedIn based on OIDC

Closes https://github.com/keycloak/keycloak/issues/22383
This commit is contained in:
rmartinc 2023-09-04 18:05:35 +02:00 committed by Marek Posolda
parent 29a9f48d4e
commit 8887be7887
21 changed files with 330 additions and 42 deletions

View file

@ -65,13 +65,19 @@ public class JWKSUtils {
}
public static PublicKeysWrapper getKeyWrappersForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
return getKeyWrappersForUse(keySet, requestedUse, false);
}
public static PublicKeysWrapper getKeyWrappersForUse(JSONWebKeySet keySet, JWK.Use requestedUse, boolean useRequestedUseWhenNull) {
List<KeyWrapper> result = new ArrayList<>();
for (JWK jwk : keySet.getKeys()) {
JWKParser parser = JWKParser.create(jwk);
if (jwk.getPublicKeyUse() == null) {
if (jwk.getPublicKeyUse() == null && !useRequestedUseWhenNull) {
logger.debugf("Ignoring JWK key '%s'. Missing required field 'use'.", jwk.getKeyId());
} else if (requestedUse.asString().equals(jwk.getPublicKeyUse()) && parser.isKeyTypeSupported(jwk.getKeyType())) {
} else if ((requestedUse.asString().equals(jwk.getPublicKeyUse()) || (jwk.getPublicKeyUse() == null && useRequestedUseWhenNull))
&& parser.isKeyTypeSupported(jwk.getKeyType())) {
KeyWrapper keyWrapper = wrap(jwk, parser);
keyWrapper.setUse(getKeyUse(requestedUse.asString()));
result.add(keyWrapper);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -5,24 +5,17 @@
.Procedure
. Click *Identity Providers* in the menu.
. From the `Add provider` list, select `LinkedIn`.
. From the `Add provider` list, select `LinkedIn OpenID Connect`.
+
.Add identity provider
image:images/linked-in-add-identity-provider.png[Add Identity Provider]
+
. Copy the value of *Redirect URI* to your clipboard.
. In a separate browser tab, https://www.linkedin.com/developer/apps[create an app].
. In a separate browser tab, https://developer.linkedin.com[create an app] in the LinkedIn developer portal.
.. After you create the app, click the *Auth* tab.
.. Enter the value of *Redirect URI* into the *Authorized redirect URLs for your app* field.
.. Note *Your Client ID* and *Your Client Secret*.
.. Click the *Products* tab and *Request access* for the *Sign In with LinkedIn using OpenID Connect* product.
. In {project_name}, paste the value of the *Client ID* into the *Client ID* field.
. In {project_name}, paste the value of the *Client Secret* into the *Client Secret* field.
. Click *Add*.
.Configuration
* With `Profile Projection` you can configure the `projection` parameter for profile requests.
* For example, `(id,firstName,lastName,profilePicture(displayImage~:playableStreams))` produces the following profile request URL:
[source,txt]
----
https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))
----

View file

@ -1,3 +1,9 @@
= Never expires option removed from client advanced settings combos
The option `Never expires` is now removed from all the combos of the Advanced Settings client tab. This option was misleading because the different lifespans or idle timeouts were never infinite, but limited by the general user session or realm values. Therefore, this option is removed in favor of the other two remaining options: `Inherits from the realm settings` (the client uses general realm timeouts) and `Expires in` (the value is overriden for the client). Internally the `Never expires` was represented by `-1`. Now that value is shown with a warning in the Admin Console and cannot be set directly by the administrator.
= New LinkedIn OpenID Connect social provider
A new social identity provider called *LinkedIn OpenID Connect* has been introduced for the business and employment-focused platform. LinkedIn released recently a new product for developers called link:https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2[Sign In with LinkedIn using OpenID Connect]. The product provides a new way to authenticate members using OpenID Connect, but the default *OpenID Connect v1.0* identity provider does not work with it at present time. For that reason, {project_name} adds this new identity provider as the specific social provider for the new product.
The old LinkedIn way based on OAuth seems to be completely removed from the link:https://developer.linkedin.com[developer portal]. How the existing LinkedIn social provider is working with current applications is not clear. {project_name} maintains the old provider but deprecated, and it will be removed in future versions. Its name was changed to *LinkedIn (deprecated)* to avoid misunderstandings.

View file

@ -14,6 +14,7 @@
"passCurrentLocale": "Pass the current locale to the identity provider as a ui_locales parameter.",
"logoutUrl": "End session endpoint to use to logout user from external IDP.",
"backchannelLogout": "Does the external IDP support backchannel logout?",
"disableNonce": "Do not send the nonce parameter in the authentication request. The nonce parameter is sent and verified by default.",
"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.",

View file

@ -98,6 +98,7 @@
"tokenUrl": "Token URL",
"logoutUrl": "Logout URL",
"backchannelLogout": "Backchannel logout",
"disableNonce": "Disable nonce",
"disableUserInfo": "Disable user info",
"isAccessTokenJWT": "Access Token is JWT",
"userInfoUrl": "User Info URL",

View file

@ -46,6 +46,7 @@ export const ExtendedNonDiscoverySettings = () => {
label="backchannelLogout"
/>
<SwitchField field="config.disableUserInfo" label="disableUserInfo" />
<SwitchField field="config.disableNonce" label="disableNonce" />
<TextField field="config.defaultScope" label="scopes" />
<FormGroupField label="prompt">
<Controller

View file

@ -34,6 +34,7 @@ function getIcon(icon: string) {
case "google":
return GoogleIcon;
case "linkedin":
case "linkedin-openid-connect":
return LinkedinIcon;
case "openshift-v3":

View file

@ -426,7 +426,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
}
identity.getContextData().put(BROKER_NONCE_PARAM, idToken.getOtherClaims().get(OIDCLoginProtocol.NONCE_PARAM));
if (!getConfig().isDisableNonce()) {
identity.getContextData().put(BROKER_NONCE_PARAM, idToken.getOtherClaims().get(OIDCLoginProtocol.NONCE_PARAM));
}
if (getConfig().isStoreToken()) {
if (tokenResponse.getExpiresIn() > 0) {
@ -584,11 +586,15 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return accessToken;
}
protected KeyWrapper getIdentityProviderKeyWrapper(JWSInput jws) {
return PublicKeyStorageManager.getIdentityProviderKeyWrapper(session, session.getContext().getRealm(), getConfig(), jws);
}
protected boolean verify(JWSInput jws) {
if (!getConfig().isValidateSignature()) return true;
try {
KeyWrapper key = PublicKeyStorageManager.getIdentityProviderKeyWrapper(session, session.getContext().getRealm(), getConfig(), jws);
KeyWrapper key = getIdentityProviderKeyWrapper(jws);
if (key == null) {
logger.debugf("Failed to verify token, key not found for algorithm %s", jws.getHeader().getRawAlgorithm());
return false;
@ -910,11 +916,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
@Override
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
UriBuilder uriBuilder = super.createAuthorizationUrl(request);
String nonce = Base64Url.encode(SecretGenerator.getInstance().randomBytes(16));
AuthenticationSessionModel authenticationSession = request.getAuthenticationSession();
authenticationSession.setClientNote(BROKER_NONCE_PARAM, nonce);
uriBuilder.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
if (!getConfig().isDisableNonce()) {
String nonce = Base64Url.encode(SecretGenerator.getInstance().randomBytes(16));
authenticationSession.setClientNote(BROKER_NONCE_PARAM, nonce);
uriBuilder.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
}
String maxAge = request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
@ -929,8 +937,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context) {
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
if (authenticationSession == null) {
// no interacting with the brokered OP, likely doing token exchanges
if (authenticationSession == null || getConfig().isDisableNonce()) {
// no interacting with the brokered OP, likely doing token exchanges or no nonce
return;
}

View file

@ -126,6 +126,18 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
getConfig().put("disableUserInfo", String.valueOf(disable));
}
public boolean isDisableNonce() {
return Boolean.parseBoolean(getConfig().get("disableNonce"));
}
public void setDisableNonce(boolean disableNonce) {
if (disableNonce) {
getConfig().put("disableNonce", Boolean.TRUE.toString());
} else {
getConfig().remove("disableNonce");
}
}
public int getAllowedClockSkew() {
String allowedClockSkew = getConfig().get(ALLOWED_CLOCK_SKEW);
if (allowedClockSkew == null || allowedClockSkew.isEmpty()) {

View file

@ -35,6 +35,7 @@ import org.keycloak.social.gitlab.GitLabIdentityProviderFactory;
import org.keycloak.social.google.GoogleIdentityProviderFactory;
import org.keycloak.social.instagram.InstagramIdentityProviderFactory;
import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
import org.keycloak.social.linkedin.LinkedInOIDCIdentityProviderFactory;
import org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory;
import org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory;
import org.keycloak.social.openshift.OpenshiftV4IdentityProviderFactory;
@ -72,6 +73,7 @@ public class UsernameTemplateMapper extends AbstractClaimMapper {
GoogleIdentityProviderFactory.PROVIDER_ID,
InstagramIdentityProviderFactory.PROVIDER_ID,
LinkedInIdentityProviderFactory.PROVIDER_ID,
LinkedInOIDCIdentityProviderFactory.PROVIDER_ID,
MicrosoftIdentityProviderFactory.PROVIDER_ID,
OpenshiftV3IdentityProviderFactory.PROVIDER_ID,
OpenshiftV4IdentityProviderFactory.PROVIDER_ID,

View file

@ -36,6 +36,7 @@ import java.util.Iterator;
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
@Deprecated
public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> implements SocialIdentityProvider<OAuth2IdentityProviderConfig> {
private static final Logger log = Logger.getLogger(LinkedInIdentityProvider.class);

View file

@ -29,6 +29,7 @@ import java.util.List;
/**
* @author Vlastimil Elias (velias at redhat dot com)
*/
@Deprecated
public class LinkedInIdentityProviderFactory extends AbstractIdentityProviderFactory<LinkedInIdentityProvider>
implements SocialIdentityProviderFactory<LinkedInIdentityProvider> {
@ -36,7 +37,7 @@ public class LinkedInIdentityProviderFactory extends AbstractIdentityProviderFac
@Override
public String getName() {
return "LinkedIn";
return "LinkedIn (deprecated)";
}
@Override

View file

@ -0,0 +1,56 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.social.linkedin;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.keys.PublicKeyStorageProvider;
import org.keycloak.keys.PublicKeyStorageUtils;
import org.keycloak.models.KeycloakSession;
/**
* <p>Specific OIDC LinkedIn provider for <b>Sign In with LinkedIn using OpenID Connect</b>
* product app.</p>
*
* @author rmartinc
*/
public class LinkedInOIDCIdentityProvider extends OIDCIdentityProvider implements SocialIdentityProvider<OIDCIdentityProviderConfig> {
public static final String DEFAULT_SCOPE = "openid profile email";
public LinkedInOIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
super(session, config);
}
@Override
protected String getDefaultScopes() {
return DEFAULT_SCOPE;
}
@Override
protected KeyWrapper getIdentityProviderKeyWrapper(JWSInput jws) {
// workaround to load keys published with no "use" as signature
PublicKeyLoader loader = new LinkedInPublicKeyLoader(session, getConfig());
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
String modelKey = PublicKeyStorageUtils.getIdpModelCacheKey(session.getContext().getRealm().getId(), getConfig().getInternalId());
return keyStorage.getPublicKey(modelKey, jws.getHeader().getKeyId(), jws.getHeader().getRawAlgorithm(), loader);
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.social.linkedin;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
/**
* <p>Specific OIDC LinkedIn provider for <b>Sign In with LinkedIn using OpenID Connect</b>
* product app. LinkedIn currently has two issues with default OIDC provider
* implementation:</p>
*
* <ol>
* <li>The jwks endpoint does not contain <em>use</em> claim for the signature key.</li>
* <li>The nonce in the authentication request is not returned back in the ID Token.</li>
* </ol>
*
* <p>This factory workarounds the default provider to overcome the issues.</p>
*
* @author rmartinc
*/
public class LinkedInOIDCIdentityProviderFactory extends AbstractIdentityProviderFactory<LinkedInOIDCIdentityProvider> implements SocialIdentityProviderFactory<LinkedInOIDCIdentityProvider> {
public static final String PROVIDER_ID = "linkedin-openid-connect";
public static final String WELL_KNOWN_URL = "https://www.linkedin.com/oauth/.well-known/openid-configuration";
// well known oidc metadata is cached as static property
private static OIDCConfigurationRepresentation metadata;
@Override
public String getName() {
return "LinkedIn OpenID Connect";
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public LinkedInOIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
OIDCConfigurationRepresentation local = metadata;
if (local == null) {
local = getWellKnownMetadata(session);
if (local.getIssuer() == null || local.getTokenEndpoint() == null || local.getAuthorizationEndpoint()== null || local.getJwksUri() == null) {
throw new RuntimeException("Invalid data in the OIDC LinkedIn well-known address.");
}
metadata = local;
}
OIDCIdentityProviderConfig config = new OIDCIdentityProviderConfig(model);
config.setIssuer(local.getIssuer());
config.setAuthorizationUrl(local.getAuthorizationEndpoint());
config.setTokenUrl(local.getTokenEndpoint());
if (local.getUserinfoEndpoint() != null) {
config.setUserInfoUrl(local.getUserinfoEndpoint());
}
config.setUseJwksUrl(true);
config.setJwksUrl(local.getJwksUri());
config.setValidateSignature(true);
config.setDisableNonce(true); // linkedin does not manage nonce correctly
return new LinkedInOIDCIdentityProvider(session, config);
}
@Override
public OIDCIdentityProviderConfig createConfig() {
return new OIDCIdentityProviderConfig();
}
private static OIDCConfigurationRepresentation getWellKnownMetadata(KeycloakSession session) {
try (SimpleHttp.Response response = SimpleHttp.doGet(WELL_KNOWN_URL, session)
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
.asResponse()) {
if (Response.Status.fromStatusCode(response.getStatus()).getFamily() != Response.Status.Family.SUCCESSFUL) {
throw new RuntimeException("Error calling the OIDC LinkedIn well-known address. Http status " + response.getStatus());
}
return response.asJson(OIDCConfigurationRepresentation.class);
} catch (IOException e) {
throw new RuntimeException("Error calling the OIDC LinkedIn well-known address.", e);
}
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
// we can add some common OIDC config parameters here if needed
return ProviderConfigurationBuilder.create()
.build();
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.social.linkedin;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.crypto.PublicKeysWrapper;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.util.JWKSUtils;
/**
* <p>Specific public key loader that assumes that use for the keys is the requested one.
* The LinkedIn OpenID Connect implementation does not add the compulsory
* <em>use</em> claim in the <a href="https://www.linkedin.com/oauth/openid/jwks">jwks endpoint</a>.</p>
*
* @author rmartinc
*/
public class LinkedInPublicKeyLoader implements PublicKeyLoader {
private final KeycloakSession session;
private final OIDCIdentityProviderConfig config;
public LinkedInPublicKeyLoader(KeycloakSession session, OIDCIdentityProviderConfig config) {
this.session = session;
this.config = config;
}
@Override
public PublicKeysWrapper loadKeys() throws Exception {
String jwksUrl = config.getJwksUrl();
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG, true);
}
}

View file

@ -25,7 +25,7 @@ import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
*/
public class LinkedInUserAttributeMapper extends AbstractJsonUserAttributeMapper {
private static final String[] cp = new String[] { LinkedInIdentityProviderFactory.PROVIDER_ID };
private static final String[] cp = new String[] { LinkedInIdentityProviderFactory.PROVIDER_ID, LinkedInOIDCIdentityProviderFactory.PROVIDER_ID };
@Override
public String[] getCompatibleProviders() {

View file

@ -20,6 +20,7 @@ org.keycloak.social.paypal.PayPalIdentityProviderFactory
org.keycloak.social.github.GitHubIdentityProviderFactory
org.keycloak.social.google.GoogleIdentityProviderFactory
org.keycloak.social.linkedin.LinkedInIdentityProviderFactory
org.keycloak.social.linkedin.LinkedInOIDCIdentityProviderFactory
org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory
org.keycloak.social.twitter.TwitterIdentityProviderFactory
org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory
@ -27,4 +28,4 @@ org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory
org.keycloak.social.openshift.OpenshiftV4IdentityProviderFactory
org.keycloak.social.gitlab.GitLabIdentityProviderFactory
org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory
org.keycloak.social.instagram.InstagramIdentityProviderFactory
org.keycloak.social.instagram.InstagramIdentityProviderFactory

View file

@ -897,7 +897,12 @@ public class IdentityProviderTest extends AbstractAdminTest {
response = realm.identityProviders().getIdentityProviders("linkedin");
Assert.assertEquals("Status", 200, response.getStatus());
body = response.readEntity(Map.class);
assertProviderInfo(body, "linkedin", "LinkedIn");
assertProviderInfo(body, "linkedin", "LinkedIn (deprecated)");
response = realm.identityProviders().getIdentityProviders("linkedin-openid-connect");
Assert.assertEquals("Status", 200, response.getStatus());
body = response.readEntity(Map.class);
assertProviderInfo(body, "linkedin-openid-connect", "LinkedIn OpenID Connect");
response = realm.identityProviders().getIdentityProviders("microsoft");
Assert.assertEquals("Status", 200, response.getStatus());

View file

@ -2,15 +2,25 @@ package org.keycloak.testsuite.broker;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
@ -23,11 +33,23 @@ public class KcOidcBrokerNonceParameterTest extends AbstractBrokerTest {
public List<ClientRepresentation> createConsumerClients() {
List<ClientRepresentation> clients = new ArrayList<>(super.createConsumerClients());
clients.add(ClientBuilder.create().clientId("consumer-client")
ClientRepresentation client = ClientBuilder.create().clientId("consumer-client")
.publicClient()
.redirectUris(getConsumerRoot() + "/auth/realms/master/app/auth/*")
.publicClient().build());
.publicClient().build();
// add the federated ID token to the protocol ID token
ProtocolMapperRepresentation consumerSessionNoteToClaimMapper = new ProtocolMapperRepresentation();
consumerSessionNoteToClaimMapper.setName(OIDCIdentityProvider.FEDERATED_ID_TOKEN);
consumerSessionNoteToClaimMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
consumerSessionNoteToClaimMapper.setProtocolMapper(UserSessionNoteMapper.PROVIDER_ID);
consumerSessionNoteToClaimMapper.setConfig(Map.of(ProtocolMapperUtils.USER_SESSION_NOTE, OIDCIdentityProvider.FEDERATED_ID_TOKEN,
OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, OIDCIdentityProvider.FEDERATED_ID_TOKEN,
OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, Boolean.TRUE.toString()));
client.setProtocolMappers(Arrays.asList(consumerSessionNoteToClaimMapper));
clients.add(client);
return clients;
}
};
@ -48,12 +70,23 @@ public class KcOidcBrokerNonceParameterTest extends AbstractBrokerTest {
IDToken idToken = toIdToken(response.getIdToken());
Assert.assertEquals("123456", idToken.getNonce());
String federatedIdTokenString = (String) idToken.getOtherClaims().get(OIDCIdentityProvider.FEDERATED_ID_TOKEN);
Assert.assertNotNull(federatedIdTokenString);
IDToken federatedIdToken = toIdToken(federatedIdTokenString);
Assert.assertNotNull(federatedIdToken.getNonce());
}
@Test
public void testNonceNotSet() {
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
// do not send nonce at IDP provider level either
IdentityProviderResource idpRes = adminClient.realm(bc.consumerRealmName()).identityProviders().get(BrokerTestConstants.IDP_OIDC_ALIAS);
IdentityProviderRepresentation idpRep = idpRes.toRepresentation();
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
cfg.setDisableNonce(true);
idpRes.update(idpRep);
oauth.realm(bc.consumerRealmName());
oauth.clientId("consumer-client");
oauth.nonce(null);
@ -65,6 +98,10 @@ public class KcOidcBrokerNonceParameterTest extends AbstractBrokerTest {
IDToken idToken = toIdToken(response.getIdToken());
Assert.assertNull(idToken.getNonce());
String federatedIdTokenString = (String) idToken.getOtherClaims().get(OIDCIdentityProvider.FEDERATED_ID_TOKEN);
Assert.assertNotNull(federatedIdTokenString);
IDToken federatedIdToken = toIdToken(federatedIdTokenString);
Assert.assertNull(federatedIdToken.getNonce());
}
protected IDToken toIdToken(String encoded) {

View file

@ -83,7 +83,6 @@ import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE_HOST
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE_NON_MATCHING_HOSTED_DOMAIN;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.INSTAGRAM;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN_WITH_PROJECTION;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT4;
@ -125,8 +124,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
GITHUB("github", GitHubLoginPage.class),
GITHUB_PRIVATE_EMAIL("github", "github-private-email", GitHubLoginPage.class),
TWITTER("twitter", TwitterConsentLoginPage.class),
LINKEDIN("linkedin", LinkedInLoginPage.class),
LINKEDIN_WITH_PROJECTION("linkedin", LinkedInLoginPage.class),
LINKEDIN("linkedin-openid-connect", LinkedInLoginPage.class),
MICROSOFT("microsoft", MicrosoftLoginPage.class),
PAYPAL("paypal", PayPalLoginPage.class),
STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class),
@ -395,15 +393,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
@Test
public void linkedinLogin() {
setTestProvider(LINKEDIN);
performLogin();
appPage.assertCurrent();
}
@Test
public void linkedinLoginWithProjection() {
setTestProvider(LINKEDIN_WITH_PROJECTION);
addAttributeMapper("picture",
"profilePicture.displayImage~.elements[0].identifiers[0].identifier");
addAttributeMapper("picture", "picture", "linkedin-user-attribute-mapper");
performLogin();
appPage.assertCurrent();
assertAttribute("picture", getConfig("profile.picture"));
@ -451,9 +441,6 @@ public class SocialLoginTest extends AbstractKeycloakTest {
if (provider == GOOGLE_NON_MATCHING_HOSTED_DOMAIN) {
idp.getConfig().put("hostedDomain", "non-matching-hosted-domain");
}
if (provider == LINKEDIN_WITH_PROJECTION) {
idp.getConfig().put("profileProjection", "(id,firstName,lastName,profilePicture(displayImage~:playableStreams))");
}
if (provider == STACKOVERFLOW) {
idp.getConfig().put("key", getConfig(provider, "clientKey"));
}
@ -471,13 +458,17 @@ public class SocialLoginTest extends AbstractKeycloakTest {
}
private void addAttributeMapper(String name, String jsonField) {
addAttributeMapper(name, jsonField, currentTestProvider.id + "-user-attribute-mapper");
}
private void addAttributeMapper(String name, String jsonField, String mapperName) {
IdentityProviderResource identityProvider = adminClient.realm(REALM).identityProviders().get(currentTestProvider.id);
IdentityProviderRepresentation identityProviderRepresentation = identityProvider.toRepresentation();
//Add birthday mapper
IdentityProviderMapperRepresentation mapperRepresentation = new IdentityProviderMapperRepresentation();
mapperRepresentation.setName(name);
mapperRepresentation.setIdentityProviderAlias(identityProviderRepresentation.getAlias());
mapperRepresentation.setIdentityProviderMapper(currentTestProvider.id + "-user-attribute-mapper");
mapperRepresentation.setIdentityProviderMapper(mapperName);
mapperRepresentation.setConfig(ImmutableMap.<String, String>builder()
.put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.IMPORT.toString())
.put(AbstractJsonUserAttributeMapper.CONF_JSON_FIELD, jsonField)