Details
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 098fdcd..4098439 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -105,7 +105,6 @@ public interface OAuth2Constants {
String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
- String JWT_ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt:access_token";
String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java
index a448d3d..2b3ef3b 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java
@@ -29,6 +29,7 @@ import javax.ws.rs.core.MultivaluedMap;
* @version $Revision: 1 $
*/
public interface ExchangeExternalToken {
+ boolean isIssuer(String issuer, MultivaluedMap<String, String> params);
BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params);
void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params);
diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index 918a385..c6f4b05 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -20,9 +20,11 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.ExchangeExternalToken;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.provider.util.SimpleHttp;
@@ -41,6 +43,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
@@ -62,11 +65,12 @@ import java.util.regex.Pattern;
/**
* @author Pedro Igor
*/
-public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements ExchangeTokenToIdentityProviderToken {
+public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements ExchangeTokenToIdentityProviderToken, ExchangeExternalToken {
protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
public static final String OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
+
public static final String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
public static final String FEDERATED_REFRESH_TOKEN = "FEDERATED_REFRESH_TOKEN";
public static final String FEDERATED_TOKEN_EXPIRATION = "FEDERATED_TOKEN_EXPIRATION";
@@ -412,4 +416,113 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE);
}
}
+
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ event.detail(Details.REASON, "exchange unsupported");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+ }
+
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode node) {
+ return null;
+ }
+
+ final protected BrokeredIdentityContext validateExternalTokenThroughUserInfo(EventBuilder event, String subjectToken, String subjectTokenType) {
+ event.detail("validation_method", "user info");
+ SimpleHttp.Response response = null;
+ int status = 0;
+ try {
+ String userInfoUrl = getProfileEndpointForValidation(event);
+ response = buildUserInfoRequest(subjectToken, userInfoUrl).asResponse();
+ status = response.getStatus();
+ } catch (IOException e) {
+ logger.debug("Failed to invoke user info for external exchange", e);
+ }
+ if (status != 200) {
+ logger.debug("Failed to invoke user info status: " + status);
+ event.detail(Details.REASON, "user info call failure");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+ }
+ JsonNode profile = null;
+ try {
+ profile = response.asJson();
+ } catch (IOException e) {
+ event.detail(Details.REASON, "user info call failure");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+ }
+ BrokeredIdentityContext context = extractIdentityFromProfile(event, profile);
+ if (context.getId() == null) {
+ event.detail(Details.REASON, "user info call failure");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+ }
+ return context;
+ }
+
+ protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
+ return SimpleHttp.doGet(userInfoUrl, session)
+ .header("Authorization", "Bearer " + subjectToken);
+ }
+
+
+ protected boolean supportsExternalExchange() {
+ return false;
+ }
+
+ @Override
+ public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+ if (!supportsExternalExchange()) return false;
+ String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+ if (requestedIssuer == null) requestedIssuer = issuer;
+ return requestedIssuer.equals(getConfig().getAlias());
+ }
+
+
+ final public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
+ if (!supportsExternalExchange()) return null;
+ BrokeredIdentityContext context = exchangeExternalImpl(event, params);
+ if (context != null) {
+ context.setIdp(this);
+ context.setIdpConfig(getConfig());
+ }
+ return context;
+ }
+
+ protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+ return exchangeExternalUserInfoValidationOnly(event, params);
+
+ }
+
+ protected BrokeredIdentityContext exchangeExternalUserInfoValidationOnly(EventBuilder event, MultivaluedMap<String, String> params) {
+ String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
+ if (subjectToken == null) {
+ event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+ }
+ String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+ if (subjectTokenType == null) {
+ subjectTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE;
+ }
+ if (!OAuth2Constants.ACCESS_TOKEN_TYPE.equals(subjectTokenType)) {
+ event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " invalid");
+ event.error(Errors.INVALID_TOKEN_TYPE);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST);
+ }
+ return validateExternalTokenThroughUserInfo(event, subjectToken, subjectTokenType);
+ }
+
+ @Override
+ public void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params) {
+ if (context.getContextData().containsKey(OIDCIdentityProvider.VALIDATED_ID_TOKEN))
+ userSession.setNote(FEDERATED_ACCESS_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
+ if (context.getContextData().containsKey(OIDCIdentityProvider.VALIDATED_ID_TOKEN))
+ userSession.setNote(OIDCIdentityProvider.FEDERATED_ID_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
+ userSession.setNote(OIDCIdentityProvider.EXCHANGE_PROVIDER, getConfig().getAlias());
+
+ }
+
+
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
index 1f2871b..4e3d160 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
@@ -17,9 +17,13 @@
package org.keycloak.broker.oidc;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.constants.AdapterConstants;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
@@ -30,11 +34,13 @@ 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;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
+import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.PublicKey;
@@ -134,5 +140,21 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
}
+ @Override
+ protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+ String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
+ if (subjectToken == null) {
+ event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+ }
+ String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+ if (subjectTokenType == null) {
+ subjectTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE;
+ }
+ return validateJwt(event, subjectToken, subjectTokenType);
+ }
+
+
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 6482544..7fceae8 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -379,11 +379,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
}
+
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
String id = idToken.getSubject();
BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
String name = (String) idToken.getOtherClaims().get(IDToken.NAME);
- String preferredUsername = (String) idToken.getOtherClaims().get(getUsernameClaimName());
+ String preferredUsername = (String) idToken.getOtherClaims().get(getusernameClaimNameForIdToken());
String email = (String) idToken.getOtherClaims().get(IDToken.EMAIL);
if (!getConfig().isDisableUserInfoService()) {
@@ -396,7 +397,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
id = getJsonProperty(userInfo, "sub");
name = getJsonProperty(userInfo, "name");
- preferredUsername = getJsonProperty(userInfo, "preferred_username");
+ preferredUsername = getUsernameFromUserInfo(userInfo);
email = getJsonProperty(userInfo, "email");
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
}
@@ -427,7 +428,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return identity;
}
- protected String getUsernameClaimName() {
+ protected String getusernameClaimNameForIdToken() {
return IDToken.PREFERRED_USERNAME;
}
@@ -518,9 +519,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return "openid";
}
- protected boolean isIssuer(MultivaluedMap<String, String> params) {
+ @Override
+ public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+ if (!supportsExternalExchange()) return false;
String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
- if (requestedIssuer == null) return true;
+ if (requestedIssuer == null) requestedIssuer = issuer;
if (requestedIssuer.equals(getConfig().getAlias())) return true;
String[] issuers = getConfig().getIssuer().split(",");
@@ -534,38 +537,65 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
@Override
- public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
- if (!isIssuer(params)) {
- return null;
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ String userInfoUrl = getUserInfoUrl();
+ if (getConfig().isDisableUserInfoService() || userInfoUrl == null || userInfoUrl.isEmpty()) {
+ event.detail(Details.REASON, "user info service disabled");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
}
- String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
- if (subjectToken == null) {
- event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+ return userInfoUrl;
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode userInfo) {
+ String id = getJsonProperty(userInfo, "sub");
+ if (id == null) {
+ event.detail(Details.REASON, "sub claim is null from user info json");
event.error(Errors.INVALID_TOKEN);
- throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
}
- String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
- if (subjectTokenType == null) {
- event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " param unset");
- event.error(Errors.INVALID_TOKEN_TYPE);
- throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token type unset", Response.Status.BAD_REQUEST);
+ BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
+
+ String name = getJsonProperty(userInfo, "name");
+ String preferredUsername = getUsernameFromUserInfo(userInfo);
+ String email = getJsonProperty(userInfo, "email");
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
+
+ identity.setId(id);
+ identity.setName(name);
+ identity.setEmail(email);
+
+ identity.setBrokerUserId(getConfig().getAlias() + "." + id);
+
+ if (preferredUsername == null) {
+ preferredUsername = email;
}
- boolean jwtAccessTokenType = subjectTokenType.equals(OAuth2Constants.JWT_ACCESS_TOKEN_TYPE);
- boolean idTokenType = subjectTokenType.equals(OAuth2Constants.ID_TOKEN_TYPE);
- if (!jwtAccessTokenType && !idTokenType) {
- event.error(Errors.INVALID_TOKEN_TYPE);
- throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST);
+
+ if (preferredUsername == null) {
+ preferredUsername = id;
}
+ identity.setUsername(preferredUsername);
+ return identity;
+ }
- if (getConfig().isValidateSignature() == false) {
- event.detail(Details.REASON, "validate signature unset");
- event.error(Errors.INVALID_CONFIG);
- throw new ErrorResponseException(Errors.INVALID_CONFIG, "Invalid server config", Response.Status.BAD_REQUEST);
+ protected String getUsernameFromUserInfo(JsonNode userInfo) {
+ return getJsonProperty(userInfo, "preferred_username");
+ }
+
+ final protected BrokeredIdentityContext validateJwt(EventBuilder event, String subjectToken, String subjectTokenType) {
+ if (!getConfig().isValidateSignature()) {
+ return validateExternalTokenThroughUserInfo(event, subjectToken, subjectTokenType);
}
+ event.detail("validation_method", "signature");
if (getConfig().isUseJwksUrl()) {
- logger.debug("using jwks url to validate token exchange");
if (getConfig().getJwksUrl() == null) {
event.detail(Details.REASON, "jwks url unset");
event.error(Errors.INVALID_CONFIG);
@@ -589,6 +619,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
try {
+ boolean idTokenType = OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType);
BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken);
if (context == null) {
event.detail(Details.REASON, "Failed to extract identity from token");
@@ -596,10 +627,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
}
- if (!idTokenType) {
+ if (idTokenType) {
context.getContextData().put(VALIDATED_ID_TOKEN, subjectToken);
- }
- if (jwtAccessTokenType) {
+ } else {
context.getContextData().put(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN, parsedToken);
}
context.getContextData().put(EXCHANGE_PROVIDER, getConfig().getAlias());
@@ -610,15 +640,31 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
logger.debug("Unable to extract identity from identity token", e);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
}
+
+
}
@Override
- public void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params) {
- if (context.getContextData().containsKey(VALIDATED_ID_TOKEN))
- userSession.setNote(FEDERATED_ACCESS_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
- if (context.getContextData().containsKey(VALIDATED_ID_TOKEN))
- userSession.setNote(FEDERATED_ID_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
- userSession.setNote(EXCHANGE_PROVIDER, getConfig().getAlias());
-
+ protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+ if (!supportsExternalExchange()) return null;
+ String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
+ if (subjectToken == null) {
+ event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+ }
+ String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+ if (subjectTokenType == null) {
+ subjectTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE;
+ }
+ if (OAuth2Constants.JWT_TOKEN_TYPE.equals(subjectTokenType) || OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType)) {
+ return validateJwt(event, subjectToken, subjectTokenType);
+ } else if (OAuth2Constants.ACCESS_TOKEN_TYPE.equals(subjectTokenType)) {
+ return validateExternalTokenThroughUserInfo(event, subjectToken, subjectTokenType);
+ } else {
+ event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " invalid");
+ event.error(Errors.INVALID_TOKEN_TYPE);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST);
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 42b42d9..9d1359c 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -40,6 +40,8 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@@ -58,6 +60,7 @@ import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
@@ -590,15 +593,29 @@ public class TokenEndpoint {
String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
if (subjectToken != null) {
- String subjectIssuer = formParams.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+ String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
String realmIssuerUrl = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName());
+ String subjectIssuer = formParams.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+
+ if (subjectIssuer == null && OAuth2Constants.JWT_TOKEN_TYPE.equals(subjectTokenType)) {
+ try {
+ JWSInput jws = new JWSInput(subjectToken);
+ JsonWebToken jwt = jws.readJsonContent(JsonWebToken.class);
+ subjectIssuer = jwt.getIssuer();
+ } catch (JWSInputException e) {
+ event.detail(Details.REASON, "unable to parse jwt subject_token");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
+
+ }
+ }
+
if (subjectIssuer != null && !realmIssuerUrl.equals(subjectIssuer)) {
event.detail(OAuth2Constants.SUBJECT_ISSUER, subjectIssuer);
- return exchangeExternalToken();
+ return exchangeExternalToken(subjectIssuer);
}
- String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
event.detail(Details.REASON, "subject_token supports access tokens only");
event.error(Errors.INVALID_TOKEN);
@@ -764,32 +781,44 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
- public Response exchangeExternalToken() {
- BrokeredIdentityContext context = null;
+ public Response exchangeExternalToken(String issuer) {
+ ExchangeExternalToken externalIdp = null;
+ IdentityProviderModel externalIdpModel = null;
for (IdentityProviderModel idpModel : realm.getIdentityProviders()) {
IdentityProviderFactory factory = IdentityBrokerService.getIdentityProviderFactory(session, idpModel);
IdentityProvider idp = factory.create(session, idpModel);
if (idp instanceof ExchangeExternalToken) {
- context = ((ExchangeExternalToken)idp).exchangeExternal(event, formParams);
- break;
+ ExchangeExternalToken external = (ExchangeExternalToken) idp;
+ if (idpModel.getAlias().equals(issuer) || externalIdp.isIssuer(issuer, formParams)) {
+ externalIdp = external;
+ externalIdpModel = idpModel;
+ break;
+ }
}
}
- if (context == null) {
+
+
+ if (externalIdp == null) {
event.error(Errors.INVALID_ISSUER);
throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
}
- if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, context.getIdpConfig())) {
+ if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, externalIdpModel)) {
event.detail(Details.REASON, "client not allowed to exchange subject_issuer");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
+ BrokeredIdentityContext context = externalIdp.exchangeExternal(event, formParams);
+ if (context == null) {
+ event.error(Errors.INVALID_ISSUER);
+ throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
+ }
UserModel user = importUserFromExternalIdentity(context);
String sessionId = KeycloakModelUtils.generateId();
UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "external-exchange", false, null, null);
- ((ExchangeExternalToken)context.getIdp()).exchangeExternalComplete(userSession, context, formParams);
+ externalIdp.exchangeExternalComplete(userSession, context, formParams);
return exchangeClientToClient(user, userSession);
diff --git a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
index bb7aa64..4b894fa 100755
--- a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
@@ -18,6 +18,7 @@
package org.keycloak.social.bitbucket;
import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
@@ -25,7 +26,13 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -51,6 +58,56 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
}
@Override
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return USER_URL;
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ String type = getJsonProperty(profile, "type");
+ if (type == null) {
+ event.detail(Details.REASON, "no type data in user info response");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
+ }
+ if (type.equals("error")) {
+ JsonNode errorNode = profile.get("error");
+ if (errorNode != null) {
+ String errorMsg = getJsonProperty(errorNode, "message");
+ event.detail(Details.REASON, "user info call failure: " + errorMsg);
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+ } else {
+ event.detail(Details.REASON, "user info call failure");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+ }
+ }
+ if (!type.equals("user")) {
+ event.detail(Details.REASON, "no user info in response");
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
+ }
+ BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id"));
+
+ String username = getJsonProperty(profile, "username");
+ user.setUsername(username);
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
+
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+ return user;
+ }
+
+ @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try {
JsonNode profile = SimpleHttp.doGet(USER_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
index 54be72c..57c0d03 100755
--- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
@@ -18,6 +18,7 @@
package org.keycloak.social.facebook;
import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
@@ -25,7 +26,14 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.core.Response;
+import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -48,45 +56,60 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
try {
JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
- String id = getJsonProperty(profile, "id");
+ return extractIdentityFromProfile(null, profile);
+ } catch (Exception e) {
+ throw new IdentityBrokerException("Could not obtain user profile from facebook.", e);
+ }
+ }
+
+ @Override
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return PROFILE_URL;
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ String id = getJsonProperty(profile, "id");
- BrokeredIdentityContext user = new BrokeredIdentityContext(id);
+ BrokeredIdentityContext user = new BrokeredIdentityContext(id);
- String email = getJsonProperty(profile, "email");
+ String email = getJsonProperty(profile, "email");
- user.setEmail(email);
+ user.setEmail(email);
- String username = getJsonProperty(profile, "username");
+ String username = getJsonProperty(profile, "username");
- if (username == null) {
- if (email != null) {
- username = email;
- } else {
- username = id;
- }
- }
+ if (username == null) {
+ if (email != null) {
+ username = email;
+ } else {
+ username = id;
+ }
+ }
- user.setUsername(username);
+ user.setUsername(username);
- String firstName = getJsonProperty(profile, "first_name");
- String lastName = getJsonProperty(profile, "last_name");
+ String firstName = getJsonProperty(profile, "first_name");
+ String lastName = getJsonProperty(profile, "last_name");
- if (lastName == null) {
- lastName = "";
- } else {
- lastName = " " + lastName;
- }
+ if (lastName == null) {
+ lastName = "";
+ } else {
+ lastName = " " + lastName;
+ }
- user.setName(firstName + lastName);
- user.setIdpConfig(getConfig());
- user.setIdp(this);
+ user.setName(firstName + lastName);
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
- AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
- return user;
- } catch (Exception e) {
- throw new IdentityBrokerException("Could not obtain user profile from facebook.", e);
- }
+ return user;
}
@Override
diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
index 4120e43..9b04b76 100755
--- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
/**
@@ -45,22 +46,39 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
}
@Override
- protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
- try {
- JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return PROFILE_URL;
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
- BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
+ String username = getJsonProperty(profile, "login");
+ user.setUsername(username);
+ user.setName(getJsonProperty(profile, "name"));
+ user.setEmail(getJsonProperty(profile, "email"));
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
- String username = getJsonProperty(profile, "login");
- user.setUsername(username);
- user.setName(getJsonProperty(profile, "name"));
- user.setEmail(getJsonProperty(profile, "email"));
- user.setIdpConfig(getConfig());
- user.setIdp(this);
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
- AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+ return user;
+
+ }
+
+
+ @Override
+ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+ try {
+ JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
- return user;
+ return extractIdentityFromProfile(null, profile);
} catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from github.", e);
}
diff --git a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
index f700d45..adf2e05 100755
--- a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
@@ -18,19 +18,25 @@
package org.keycloak.social.gitlab;
import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
import java.io.IOException;
/**
@@ -56,6 +62,37 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider implements Soc
}
}
+ protected String getUsernameFromUserInfo(JsonNode userInfo) {
+ return getJsonProperty(userInfo, "username");
+ }
+
+ protected String getusernameClaimNameForIdToken() {
+ return IDToken.NICKNAME;
+ }
+
+ @Override
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return getUserInfoUrl();
+ }
+
+ @Override
+ public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+ String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+ if (requestedIssuer == null) requestedIssuer = issuer;
+ return requestedIssuer.equals(getConfig().getAlias());
+ }
+
+
+ @Override
+ protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+ return exchangeExternalUserInfoValidationOnly(event, params);
+ }
+
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
String id = idToken.getSubject();
BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
@@ -100,10 +137,6 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider implements Soc
return identity;
}
- @Override
- public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
- return null;
- }
diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
index afd0430..f3e4990 100755
--- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
@@ -79,42 +80,23 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
return uri;
}
- protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
- String id = idToken.getSubject();
- BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
- String name = (String) idToken.getOtherClaims().get(IDToken.NAME);
- String preferredUsername = (String) idToken.getOtherClaims().get(getUsernameClaimName());
- String email = (String) idToken.getOtherClaims().get(IDToken.EMAIL);
-
- identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
-
- identity.setId(id);
- identity.setName(name);
- identity.setEmail(email);
-
- identity.setBrokerUserId(getConfig().getAlias() + "." + id);
-
- if (preferredUsername == null) {
- preferredUsername = email;
- }
+ @Override
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
- if (preferredUsername == null) {
- preferredUsername = id;
- }
- identity.setUsername(preferredUsername);
- if (tokenResponse != null && tokenResponse.getSessionState() != null) {
- identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
- }
- if (tokenResponse != null) identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
- if (tokenResponse != null) processAccessTokenResponse(identity, tokenResponse);
- return identity;
+ @Override
+ public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+ String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+ if (requestedIssuer == null) requestedIssuer = issuer;
+ return requestedIssuer.equals(getConfig().getAlias());
}
@Override
- public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
- return null;
+ protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+ return exchangeExternalUserInfoValidationOnly(event, params);
}
diff --git a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
index e25bcfa..c30db0c 100755
--- a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import java.net.MalformedURLException;
@@ -53,23 +54,39 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp
}
@Override
- protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
- log.debug("doGetFederatedIdentity()");
- try {
- JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return PROFILE_URL;
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
+
+ String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl"));
+ user.setUsername(username);
+ user.setName(getJsonProperty(profile, "formattedName"));
+ user.setEmail(getJsonProperty(profile, "emailAddress"));
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
- BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
- String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl"));
- user.setUsername(username);
- user.setName(getJsonProperty(profile, "formattedName"));
- user.setEmail(getJsonProperty(profile, "emailAddress"));
- user.setIdpConfig(getConfig());
- user.setIdp(this);
+ return user;
- AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+ }
- return user;
+
+ @Override
+ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+ log.debug("doGetFederatedIdentity()");
+ try {
+ JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
+ return extractIdentityFromProfile(null, profile);
} catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from linkedIn.", e);
}
diff --git a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
index 17dde5e..5df1ce9 100755
--- a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.social.microsoft;
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger;
+import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
@@ -27,8 +28,15 @@ import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
@@ -54,6 +62,27 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
}
@Override
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return PROFILE_URL;
+ }
+
+ @Override
+ protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
+ String URL = null;
+ try {
+ URL = PROFILE_URL + "?access_token=" + URLEncoder.encode(subjectToken, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ return SimpleHttp.doGet(URL, session);
+ }
+
+ @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try {
String URL = PROFILE_URL + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8");
@@ -62,29 +91,34 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
}
JsonNode profile = SimpleHttp.doGet(URL, session).asJson();
- String id = getJsonProperty(profile, "id");
+ return extractIdentityFromProfile(null, profile);
+ } catch (Exception e) {
+ throw new IdentityBrokerException("Could not obtain user profile from Microsoft Live ID.", e);
+ }
+ }
- String email = null;
- if (profile.has("emails")) {
- email = getJsonProperty(profile.get("emails"), "preferred");
- }
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ String id = getJsonProperty(profile, "id");
- BrokeredIdentityContext user = new BrokeredIdentityContext(id);
+ String email = null;
+ if (profile.has("emails")) {
+ email = getJsonProperty(profile.get("emails"), "preferred");
+ }
- user.setUsername(email != null ? email : id);
- user.setFirstName(getJsonProperty(profile, "first_name"));
- user.setLastName(getJsonProperty(profile, "last_name"));
- if (email != null)
- user.setEmail(email);
- user.setIdpConfig(getConfig());
- user.setIdp(this);
+ BrokeredIdentityContext user = new BrokeredIdentityContext(id);
- AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+ user.setUsername(email != null ? email : id);
+ user.setFirstName(getJsonProperty(profile, "first_name"));
+ user.setLastName(getJsonProperty(profile, "last_name"));
+ if (email != null)
+ user.setEmail(email);
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
- return user;
- } catch (Exception e) {
- throw new IdentityBrokerException("Could not obtain user profile from Microsoft Live ID.", e);
- }
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+ return user;
}
@Override
diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
index fafa425..fa58386 100644
--- a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
@@ -1,15 +1,22 @@
package org.keycloak.social.openshift;
import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
+import javax.ws.rs.core.Response;
import java.io.IOException;
+import java.net.URLEncoder;
import java.util.Optional;
/**
@@ -63,4 +70,21 @@ public class OpenshiftV3IdentityProvider extends AbstractOAuth2IdentityProvider<
.asJson();
}
+ @Override
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return getConfig().getUserInfoUrl();
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ final BrokeredIdentityContext user = extractUserContext(profile.get("metadata"));
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+ return user;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
index a3f4602..17a8353 100644
--- a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
/**
@@ -46,21 +47,36 @@ public class PayPalIdentityProvider extends AbstractOAuth2IdentityProvider<PayPa
}
@Override
- protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
- try {
- JsonNode profile = SimpleHttp.doGet(getConfig().getUserInfoUrl(), session).header("Authorization", "Bearer " + accessToken).asJson();
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
- BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return getConfig().getUserInfoUrl();
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
- user.setUsername(getJsonProperty(profile, "email"));
- user.setName(getJsonProperty(profile, "name"));
- user.setEmail(getJsonProperty(profile, "email"));
- user.setIdpConfig(getConfig());
- user.setIdp(this);
+ user.setUsername(getJsonProperty(profile, "email"));
+ user.setName(getJsonProperty(profile, "name"));
+ user.setEmail(getJsonProperty(profile, "email"));
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
- AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+ return user;
+ }
+
+
+ @Override
+ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+ try {
+ JsonNode profile = SimpleHttp.doGet(getConfig().getUserInfoUrl(), session).header("Authorization", "Bearer " + accessToken).asJson();
- return user;
+ return extractIdentityFromProfile(null, profile);
} catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from paypal.", e);
}
diff --git a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
index 9a0992a..b44de94 100755
--- a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
@@ -24,12 +24,15 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
+import java.net.URLEncoder;
import java.util.HashMap;
/**
@@ -54,6 +57,41 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
}
@Override
+ protected boolean supportsExternalExchange() {
+ return true;
+ }
+
+ @Override
+ protected String getProfileEndpointForValidation(EventBuilder event) {
+ return PROFILE_URL;
+ }
+
+ @Override
+ protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
+ String URL = PROFILE_URL + "&access_token=" + subjectToken + "&key=" + getConfig().getKey();
+ return SimpleHttp.doGet(URL, session);
+ }
+
+ @Override
+ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode node) {
+ JsonNode profile = node.get("items").get(0);
+
+ BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
+
+ String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link"));
+ user.setUsername(username);
+ user.setName(unescapeHtml3(getJsonProperty(profile, "display_name")));
+ // email is not provided
+ // user.setEmail(getJsonProperty(profile, "email"));
+ user.setIdpConfig(getConfig());
+ user.setIdp(this);
+
+ AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+ return user;
+ }
+
+ @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
log.debug("doGetFederatedIdentity()");
try {
@@ -62,21 +100,7 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
if (log.isDebugEnabled()) {
log.debug("StackOverflow profile request to: " + URL);
}
- JsonNode profile = SimpleHttp.doGet(URL, session).asJson().get("items").get(0);
-
- BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
-
- String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link"));
- user.setUsername(username);
- user.setName(unescapeHtml3(getJsonProperty(profile, "display_name")));
- // email is not provided
- // user.setEmail(getJsonProperty(profile, "email"));
- user.setIdpConfig(getConfig());
- user.setIdp(this);
-
- AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
-
- return user;
+ return extractIdentityFromProfile(null, SimpleHttp.doGet(URL, session).asJson());
} catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from Stackoverflow: " + e.getMessage(), e);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
index be579e2..d3b3258 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
@@ -464,21 +464,34 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation();
rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(false));
adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep);
- // test failure that validate signatures not set up yet.
+ // test user info validation.
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
- .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+ .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
));
- Assert.assertEquals(400, response.getStatus());
- String json = response.readEntity(String.class);
- System.out.println(json);
- Assert.assertTrue(json.contains("Invalid server config"));
+ Assert.assertEquals(200, response.getStatus());
+ AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
+ String exchangedAccessToken = tokenResponse.getToken();
+ Assert.assertNotNull(exchangedAccessToken);
+ response.close();
+
+ Assert.assertEquals(1, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
+
+ // test logout
+ response = childLogoutWebTarget(httpClient)
+ .queryParam("id_token_hint", exchangedAccessToken)
+ .request()
+ .get();
+ response.close();
+
+ Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
+
}
IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation();
rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(true));
@@ -490,14 +503,14 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
String exchangedUsername = null;
{
- // valid exchange
+ // test signature validation
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
- .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+ .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
));
@@ -554,7 +567,7 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
- .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+ .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
));
@@ -597,7 +610,7 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
- .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+ .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
));