Add a new identity provider for LinkedIn based on OIDC
Closes https://github.com/keycloak/keycloak/issues/22383
This commit is contained in:
parent
29a9f48d4e
commit
8887be7887
21 changed files with 330 additions and 42 deletions
|
@ -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 |
|
@ -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))
|
||||
----
|
|
@ -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.
|
|
@ -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.",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -34,6 +34,7 @@ function getIcon(icon: string) {
|
|||
case "google":
|
||||
return GoogleIcon;
|
||||
case "linkedin":
|
||||
case "linkedin-openid-connect":
|
||||
return LinkedinIcon;
|
||||
|
||||
case "openshift-v3":
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue