Merge pull request #4562 from patriot1burke/master

KEYCLOAK-5683, KEYCLOAK-5684, KEYCLOAK-5682, KEYCLOAK-5612, KEYCLOAK-5611, KEYCLOAK-5331
This commit is contained in:
Bill Burke 2017-10-13 18:43:48 -04:00 committed by GitHub
commit 7c99a8b641
24 changed files with 1185 additions and 260 deletions

View file

@ -105,7 +105,6 @@ public interface OAuth2Constants {
String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token"; 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 REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt"; 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"; String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";

View file

@ -29,6 +29,7 @@ import javax.ws.rs.core.MultivaluedMap;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface ExchangeExternalToken { public interface ExchangeExternalToken {
boolean isIssuer(String issuer, MultivaluedMap<String, String> params);
BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params); BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params);
void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params); void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params);

View file

@ -34,6 +34,9 @@ import javax.ws.rs.core.UriInfo;
*/ */
public interface IdentityProvider<C extends IdentityProviderModel> extends Provider { public interface IdentityProvider<C extends IdentityProviderModel> extends Provider {
String EXTERNAL_IDENTITY_PROVIDER = "EXTERNAL_IDENTITY_PROVIDER";
String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
interface AuthenticationCallback { interface AuthenticationCallback {
/** /**
* This method should be called by provider after the JAXRS callback endpoint has finished authentication * This method should be called by provider after the JAXRS callback endpoint has finished authentication

View file

@ -20,11 +20,14 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ExchangeExternalToken;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken; import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details; import org.keycloak.events.Details;
@ -40,6 +43,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
@ -61,12 +65,12 @@ import java.util.regex.Pattern;
/** /**
* @author Pedro Igor * @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); 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_REFRESH_TOKEN = "refresh_token";
public static final String OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"; 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_REFRESH_TOKEN = "FEDERATED_REFRESH_TOKEN";
public static final String FEDERATED_TOKEN_EXPIRATION = "FEDERATED_TOKEN_EXPIRATION"; public static final String FEDERATED_TOKEN_EXPIRATION = "FEDERATED_TOKEN_EXPIRATION";
public static final String ACCESS_DENIED = "access_denied"; public static final String ACCESS_DENIED = "access_denied";
@ -163,6 +167,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
if (!getConfig().isStoreToken()) { if (!getConfig().isStoreToken()) {
// if token isn't stored, we need to see if this session has been linked // if token isn't stored, we need to see if this session has been linked
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER); String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
brokerId = brokerId == null ? tokenUserSession.getNote(IdentityProvider.EXTERNAL_IDENTITY_PROVIDER) : brokerId;
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) { if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
event.detail(Details.REASON, "requested_issuer has not linked"); event.detail(Details.REASON, "requested_issuer has not linked");
event.error(Errors.INVALID_REQUEST); event.error(Errors.INVALID_REQUEST);
@ -411,4 +416,113 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE); .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;
}
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());
}
} }

View file

@ -17,9 +17,13 @@
package org.keycloak.broker.oidc; package org.keycloak.broker.oidc;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
@ -30,11 +34,13 @@ import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.adapters.action.AdminAction; import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.security.PublicKey; 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);
}
} }

View file

@ -195,7 +195,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
private String getIDTokenForLogout(KeycloakSession session, UserSessionModel userSession) { private String getIDTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
long exp = Long.parseLong(userSession.getNote(FEDERATED_TOKEN_EXPIRATION)); String tokenExpirationString = userSession.getNote(FEDERATED_TOKEN_EXPIRATION);
long exp = tokenExpirationString == null ? 0 : Long.parseLong(tokenExpirationString);
int currentTime = Time.currentTime(); int currentTime = Time.currentTime();
if (exp > 0 && currentTime > exp) { if (exp > 0 && currentTime > exp) {
String response = refreshTokenForLogout(session, userSession); String response = refreshTokenForLogout(session, userSession);
@ -379,11 +380,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
} }
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException { protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
String id = idToken.getSubject(); String id = idToken.getSubject();
BrokeredIdentityContext identity = new BrokeredIdentityContext(id); BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
String name = (String) idToken.getOtherClaims().get(IDToken.NAME); 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); String email = (String) idToken.getOtherClaims().get(IDToken.EMAIL);
if (!getConfig().isDisableUserInfoService()) { if (!getConfig().isDisableUserInfoService()) {
@ -391,12 +393,24 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) { if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) {
if (accessToken != null) { if (accessToken != null) {
JsonNode userInfo = SimpleHttp.doGet(userInfoUrl, session) SimpleHttp.Response response = SimpleHttp.doGet(userInfoUrl, session)
.header("Authorization", "Bearer " + accessToken).asJson(); .header("Authorization", "Bearer " + accessToken).asResponse();
if (response.getStatus() != 200) {
String msg = "failed to invoke user info url";
try {
String tmp = response.asString();
if (tmp != null) msg = tmp;
} catch (IOException e) {
}
throw new IdentityBrokerException("Failed to invoke on user info url: " + msg);
}
JsonNode userInfo = response.asJson();
id = getJsonProperty(userInfo, "sub"); id = getJsonProperty(userInfo, "sub");
name = getJsonProperty(userInfo, "name"); name = getJsonProperty(userInfo, "name");
preferredUsername = getJsonProperty(userInfo, "preferred_username"); preferredUsername = getUsernameFromUserInfo(userInfo);
email = getJsonProperty(userInfo, "email"); email = getJsonProperty(userInfo, "email");
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias()); AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
} }
@ -427,7 +441,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return identity; return identity;
} }
protected String getUsernameClaimName() { protected String getusernameClaimNameForIdToken() {
return IDToken.PREFERRED_USERNAME; return IDToken.PREFERRED_USERNAME;
} }
@ -518,9 +532,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return "openid"; 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); String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
if (requestedIssuer == null) return true; if (requestedIssuer == null) requestedIssuer = issuer;
if (requestedIssuer.equals(getConfig().getAlias())) return true; if (requestedIssuer.equals(getConfig().getAlias())) return true;
String[] issuers = getConfig().getIssuer().split(","); String[] issuers = getConfig().getIssuer().split(",");
@ -534,38 +550,65 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
protected boolean supportsExternalExchange() {
return true;
}
@Override @Override
public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) { protected String getProfileEndpointForValidation(EventBuilder event) {
if (!isIssuer(params)) { String userInfoUrl = getUserInfoUrl();
return null; if (getConfig().isDisableUserInfoService() || userInfoUrl == null || userInfoUrl.isEmpty()) {
} event.detail(Details.REASON, "user info service disabled");
String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
if (subjectToken == null) {
event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
event.error(Errors.INVALID_TOKEN); 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); return userInfoUrl;
if (subjectTokenType == null) { }
event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " param unset");
event.error(Errors.INVALID_TOKEN_TYPE); @Override
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token type unset", Response.Status.BAD_REQUEST); 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, "invalid token", Response.Status.BAD_REQUEST);
} }
boolean jwtAccessTokenType = subjectTokenType.equals(OAuth2Constants.JWT_ACCESS_TOKEN_TYPE); BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
boolean idTokenType = subjectTokenType.equals(OAuth2Constants.ID_TOKEN_TYPE);
if (!jwtAccessTokenType && !idTokenType) { String name = getJsonProperty(userInfo, "name");
event.error(Errors.INVALID_TOKEN_TYPE); String preferredUsername = getUsernameFromUserInfo(userInfo);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST); 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;
} }
if (preferredUsername == null) {
if (getConfig().isValidateSignature() == false) { preferredUsername = id;
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);
} }
identity.setUsername(preferredUsername);
return identity;
}
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()) { if (getConfig().isUseJwksUrl()) {
logger.debug("using jwks url to validate token exchange");
if (getConfig().getJwksUrl() == null) { if (getConfig().getJwksUrl() == null) {
event.detail(Details.REASON, "jwks url unset"); event.detail(Details.REASON, "jwks url unset");
event.error(Errors.INVALID_CONFIG); event.error(Errors.INVALID_CONFIG);
@ -589,6 +632,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
try { try {
boolean idTokenType = OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType);
BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken); BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken);
if (context == null) { if (context == null) {
event.detail(Details.REASON, "Failed to extract identity from token"); event.detail(Details.REASON, "Failed to extract identity from token");
@ -596,10 +640,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST); throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
} }
if (!idTokenType) { if (idTokenType) {
context.getContextData().put(VALIDATED_ID_TOKEN, subjectToken); context.getContextData().put(VALIDATED_ID_TOKEN, subjectToken);
} } else {
if (jwtAccessTokenType) {
context.getContextData().put(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN, parsedToken); context.getContextData().put(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN, parsedToken);
} }
context.getContextData().put(EXCHANGE_PROVIDER, getConfig().getAlias()); context.getContextData().put(EXCHANGE_PROVIDER, getConfig().getAlias());
@ -610,15 +653,31 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
logger.debug("Unable to extract identity from identity token", e); logger.debug("Unable to extract identity from identity token", e);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST); throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
} }
} }
@Override @Override
public void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params) { protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
if (context.getContextData().containsKey(VALIDATED_ID_TOKEN)) if (!supportsExternalExchange()) return null;
userSession.setNote(FEDERATED_ACCESS_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN)); String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
if (context.getContextData().containsKey(VALIDATED_ID_TOKEN)) if (subjectToken == null) {
userSession.setNote(FEDERATED_ID_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN)); event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
userSession.setNote(EXCHANGE_PROVIDER, getConfig().getAlias()); 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);
}
} }
} }

View file

@ -40,6 +40,8 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; 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.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; 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.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
@ -590,15 +593,29 @@ public class TokenEndpoint {
String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN); String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
if (subjectToken != null) { 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 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)) { if (subjectIssuer != null && !realmIssuerUrl.equals(subjectIssuer)) {
event.detail(OAuth2Constants.SUBJECT_ISSUER, subjectIssuer); event.detail(OAuth2Constants.SUBJECT_ISSUER, subjectIssuer);
return exchangeExternalToken(); return exchangeExternalToken(subjectIssuer, subjectToken);
} }
String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) { if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
event.detail(Details.REASON, "subject_token supports access tokens only"); event.detail(Details.REASON, "subject_token supports access tokens only");
event.error(Errors.INVALID_TOKEN); event.error(Errors.INVALID_TOKEN);
@ -670,8 +687,18 @@ public class TokenEndpoint {
if (requestedIssuer == null) { if (requestedIssuer == null) {
return exchangeClientToClient(tokenUser, tokenSession); return exchangeClientToClient(tokenUser, tokenSession);
} else { } else {
return exchangeToIdentityProvider(tokenUser, tokenSession, requestedIssuer); try {
} return exchangeToIdentityProvider(tokenUser, tokenSession, requestedIssuer);
} finally {
if (subjectToken == null) { // we are naked! So need to clean up user session
try {
session.sessions().removeUserSession(realm, tokenSession);
} catch (Exception ignore) {
}
}
}
}
} }
public Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) { public Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) {
@ -764,32 +791,49 @@ 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(); 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() { public Response exchangeExternalToken(String issuer, String subjectToken) {
BrokeredIdentityContext context = null; ExchangeExternalToken externalIdp = null;
IdentityProviderModel externalIdpModel = null;
for (IdentityProviderModel idpModel : realm.getIdentityProviders()) { for (IdentityProviderModel idpModel : realm.getIdentityProviders()) {
IdentityProviderFactory factory = IdentityBrokerService.getIdentityProviderFactory(session, idpModel); IdentityProviderFactory factory = IdentityBrokerService.getIdentityProviderFactory(session, idpModel);
IdentityProvider idp = factory.create(session, idpModel); IdentityProvider idp = factory.create(session, idpModel);
if (idp instanceof ExchangeExternalToken) { if (idp instanceof ExchangeExternalToken) {
context = ((ExchangeExternalToken)idp).exchangeExternal(event, formParams); ExchangeExternalToken external = (ExchangeExternalToken) idp;
break; if (idpModel.getAlias().equals(issuer) || external.isIssuer(issuer, formParams)) {
externalIdp = external;
externalIdpModel = idpModel;
break;
}
} }
} }
if (context == null) {
if (externalIdp == null) {
event.error(Errors.INVALID_ISSUER); event.error(Errors.INVALID_ISSUER);
throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST); 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.detail(Details.REASON, "client not allowed to exchange subject_issuer");
event.error(Errors.NOT_ALLOWED); event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN); 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); UserModel user = importUserFromExternalIdentity(context);
String sessionId = KeycloakModelUtils.generateId(); String sessionId = KeycloakModelUtils.generateId();
UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "external-exchange", false, null, null); 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);
// this must exist so that we can obtain access token from user session if idp's store tokens is off
userSession.setNote(IdentityProvider.EXTERNAL_IDENTITY_PROVIDER, externalIdpModel.getAlias());
userSession.setNote(IdentityProvider.FEDERATED_ACCESS_TOKEN, subjectToken);
return exchangeClientToClient(user, userSession); return exchangeClientToClient(user, userSession);

View file

@ -371,6 +371,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return response; return response;
} }
} catch (IdentityBrokerException e) { } catch (IdentityBrokerException e) {
e.printStackTrace();
return redirectToErrorPage(Messages.COULD_NOT_SEND_AUTHENTICATION_REQUEST, e, providerId); return redirectToErrorPage(Messages.COULD_NOT_SEND_AUTHENTICATION_REQUEST, e, providerId);
} catch (Exception e) { } catch (Exception e) {
return redirectToErrorPage(Messages.UNEXPECTED_ERROR_HANDLING_REQUEST, e, providerId); return redirectToErrorPage(Messages.UNEXPECTED_ERROR_HANDLING_REQUEST, e, providerId);

View file

@ -18,6 +18,7 @@
package org.keycloak.social.bitbucket; package org.keycloak.social.bitbucket;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; 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.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; 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.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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -35,6 +43,7 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
public static final String AUTH_URL = "https://bitbucket.org/site/oauth2/authorize"; public static final String AUTH_URL = "https://bitbucket.org/site/oauth2/authorize";
public static final String TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"; public static final String TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token";
public static final String USER_URL = "https://api.bitbucket.org/2.0/user"; public static final String USER_URL = "https://api.bitbucket.org/2.0/user";
public static final String USER_EMAIL_URL = "https://api.bitbucket.org/2.0/user/emails";
public static final String EMAIL_SCOPE = "email"; public static final String EMAIL_SCOPE = "email";
public static final String ACCOUNT_SCOPE = "account"; public static final String ACCOUNT_SCOPE = "account";
public static final String DEFAULT_SCOPE = ACCOUNT_SCOPE; public static final String DEFAULT_SCOPE = ACCOUNT_SCOPE;
@ -46,10 +55,115 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
String defaultScope = config.getDefaultScope(); String defaultScope = config.getDefaultScope();
if (defaultScope == null || defaultScope.trim().equals("")) { if (defaultScope == null || defaultScope.trim().equals("")) {
config.setDefaultScope(ACCOUNT_SCOPE); config.setDefaultScope(ACCOUNT_SCOPE + " " + EMAIL_SCOPE);
} }
} }
@Override
protected boolean supportsExternalExchange() {
return true;
}
@Override
protected String getProfileEndpointForValidation(EventBuilder event) {
return USER_URL;
}
@Override
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);
}
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);
}
String id = getJsonProperty(profile, "account_id");
if (id == 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 extractUserInfo(subjectToken, profile);
}
private BrokeredIdentityContext extractUserInfo(String subjectToken, JsonNode profile) {
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id"));
String username = getJsonProperty(profile, "username");
user.setUsername(username);
user.setName(getJsonProperty(profile, "display_name"));
user.setIdpConfig(getConfig());
user.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
try {
JsonNode emails = SimpleHttp.doGet(USER_EMAIL_URL, session).header("Authorization", "Bearer " + subjectToken).asJson();
// {"pagelen":10,"values":[{"is_primary":true,"is_confirmed":true,"type":"email","email":"bburke@redhat.com","links":{"self":{"href":"https://api.bitbucket.org/2.0/user/emails/bburke@redhat.com"}}}],"page":1,"size":1}
JsonNode emailJson = emails.get("values");
if (emailJson != null) {
if (emailJson.isArray()) {
emailJson = emailJson.get(0);
}
if (emailJson != null && "email".equals(getJsonProperty(emailJson, "type"))) {
user.setEmail(getJsonProperty(emailJson, "email"));
}
}
} catch (Exception ignore) {
logger.debug("failed to get email from BitBucket", ignore);
}
return user;
}
@Override @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try { try {
@ -74,16 +188,7 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
throw new IdentityBrokerException("Could not obtain account information from bitbucket."); throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
} }
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id")); return extractUserInfo(accessToken, profile);
String username = getJsonProperty(profile, "username");
user.setUsername(username);
user.setIdpConfig(getConfig());
user.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
} catch (Exception e) { } catch (Exception e) {
if (e instanceof IdentityBrokerException) throw (IdentityBrokerException)e; if (e instanceof IdentityBrokerException) throw (IdentityBrokerException)e;
throw new IdentityBrokerException("Could not obtain user profile from github.", e); throw new IdentityBrokerException("Could not obtain user profile from github.", e);

View file

@ -18,6 +18,7 @@
package org.keycloak.social.facebook; package org.keycloak.social.facebook;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; 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.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; 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.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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -48,47 +56,62 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
try { try {
JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson(); JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
String id = getJsonProperty(profile, "id"); return extractIdentityFromProfile(null, profile);
BrokeredIdentityContext user = new BrokeredIdentityContext(id);
String email = getJsonProperty(profile, "email");
user.setEmail(email);
String username = getJsonProperty(profile, "username");
if (username == null) {
if (email != null) {
username = email;
} else {
username = id;
}
}
user.setUsername(username);
String firstName = getJsonProperty(profile, "first_name");
String lastName = getJsonProperty(profile, "last_name");
if (lastName == null) {
lastName = "";
} else {
lastName = " " + lastName;
}
user.setName(firstName + lastName);
user.setIdpConfig(getConfig());
user.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
} catch (Exception e) { } catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from facebook.", 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);
String email = getJsonProperty(profile, "email");
user.setEmail(email);
String username = getJsonProperty(profile, "username");
if (username == null) {
if (email != null) {
username = email;
} else {
username = id;
}
}
user.setUsername(username);
String firstName = getJsonProperty(profile, "first_name");
String lastName = getJsonProperty(profile, "last_name");
if (lastName == null) {
lastName = "";
} else {
lastName = " " + lastName;
}
user.setName(firstName + lastName);
user.setIdpConfig(getConfig());
user.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
}
@Override @Override
protected String getDefaultScopes() { protected String getDefaultScopes() {
return DEFAULT_SCOPE; return DEFAULT_SCOPE;

View file

@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
/** /**
@ -44,23 +45,40 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);
} }
@Override
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 = 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());
return user;
}
@Override @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try { try {
JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson(); JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id")); return extractIdentityFromProfile(null, profile);
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());
return user;
} catch (Exception e) { } catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from github.", e); throw new IdentityBrokerException("Could not obtain user profile from github.", e);
} }

View file

@ -18,19 +18,26 @@
package org.keycloak.social.gitlab; package org.keycloak.social.gitlab;
import com.fasterxml.jackson.databind.JsonNode; 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.OIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
/** /**
@ -56,37 +63,62 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider implements Soc
} }
} }
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException { protected String getUsernameFromUserInfo(JsonNode userInfo) {
String id = idToken.getSubject(); return getJsonProperty(userInfo, "username");
BrokeredIdentityContext identity = new BrokeredIdentityContext(id); }
String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.NICKNAME);
String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
if (getConfig().getDefaultScope().contains(API_SCOPE)) { protected String getusernameClaimNameForIdToken() {
String userInfoUrl = getUserInfoUrl(); return IDToken.NICKNAME;
if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) { }
JsonNode userInfo = SimpleHttp.doGet(userInfoUrl, session)
.header("Authorization", "Bearer " + accessToken).asJson();
name = getJsonProperty(userInfo, "name"); @Override
preferredUsername = getJsonProperty(userInfo, "username"); protected boolean supportsExternalExchange() {
email = getJsonProperty(userInfo, "email"); return true;
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias()); }
}
@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);
}
@Override
protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
String id = getJsonProperty(profile, "id");
if (id == null) {
event.detail(Details.REASON, "id claim is null from user info json");
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
} }
identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse); return gitlabExtractFromProfile(profile);
identity.getContextData().put(VALIDATED_ID_TOKEN, idToken); }
processAccessTokenResponse(identity, tokenResponse);
private BrokeredIdentityContext gitlabExtractFromProfile(JsonNode profile) {
String id = getJsonProperty(profile, "id");
BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
String name = getJsonProperty(profile, "name");
String preferredUsername = getJsonProperty(profile, "username");
String email = getJsonProperty(profile, "email");
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, profile, getConfig().getAlias());
identity.setId(id); identity.setId(id);
identity.setName(name); identity.setName(name);
identity.setEmail(email); identity.setEmail(email);
identity.setBrokerUserId(getConfig().getAlias() + "." + id); identity.setBrokerUserId(getConfig().getAlias() + "." + id);
if (tokenResponse.getSessionState() != null) {
identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
}
if (preferredUsername == null) { if (preferredUsername == null) {
preferredUsername = email; preferredUsername = email;
@ -100,13 +132,54 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider implements Soc
return identity; return identity;
} }
@Override
public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) { protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
return null;
SimpleHttp.Response response = null;
int status = 0;
for (int i = 0; i < 10; i++) {
try {
String userInfoUrl = getUserInfoUrl();
response = SimpleHttp.doGet(userInfoUrl, session)
.header("Authorization", "Bearer " + accessToken).asResponse();
status = response.getStatus();
} catch (IOException e) {
logger.debug("Failed to invoke user info for external exchange", e);
}
if (status == 200) break;
response.close();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (status != 200) {
logger.debug("Failed to invoke user info status: " + status);
throw new IdentityBrokerException("Gitlab user info call failure");
}
JsonNode profile = null;
try {
profile = response.asJson();
} catch (IOException e) {
throw new IdentityBrokerException("Gitlab user info call failure");
}
String id = getJsonProperty(profile, "id");
if (id == null) {
throw new IdentityBrokerException("Gitlab id claim is null from user info json");
}
BrokeredIdentityContext identity = gitlabExtractFromProfile(profile);
identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
processAccessTokenResponse(identity, tokenResponse);
return identity;
} }
} }

View file

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider; import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
@ -79,48 +80,23 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
return uri; return uri;
} }
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException { @Override
String id = idToken.getSubject(); protected boolean supportsExternalExchange() {
BrokeredIdentityContext identity = new BrokeredIdentityContext(id); return true;
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);
if (!getConfig().isDisableUserInfoService() && accessToken != null && name == null) {
JsonNode userInfo = SimpleHttp.doGet(getUserInfoUrl(), session)
.header("Authorization", "Bearer " + accessToken).asJson();
name = getJsonProperty(userInfo, "name");
}
identity.setId(id);
identity.setName(name);
identity.setEmail(email);
identity.setBrokerUserId(getConfig().getAlias() + "." + id);
if (preferredUsername == null) {
preferredUsername = email;
}
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 @Override
public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) { public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
return null; 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);
} }

View file

@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@ -52,24 +53,40 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);
} }
@Override
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);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
}
@Override @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
log.debug("doGetFederatedIdentity()"); log.debug("doGetFederatedIdentity()");
try { try {
JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson(); JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
return extractIdentityFromProfile(null, 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);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
} catch (Exception e) { } catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from linkedIn.", e); throw new IdentityBrokerException("Could not obtain user profile from linkedIn.", e);
} }

View file

@ -19,6 +19,7 @@ package org.keycloak.social.microsoft;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; 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.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; 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.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; import java.net.URLEncoder;
/** /**
@ -53,6 +61,27 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);
} }
@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 @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try { try {
@ -62,31 +91,36 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
} }
JsonNode profile = SimpleHttp.doGet(URL, session).asJson(); JsonNode profile = SimpleHttp.doGet(URL, session).asJson();
String id = getJsonProperty(profile, "id"); return extractIdentityFromProfile(null, profile);
String email = null;
if (profile.has("emails")) {
email = getJsonProperty(profile.get("emails"), "preferred");
}
BrokeredIdentityContext user = new BrokeredIdentityContext(id);
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);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
} catch (Exception e) { } catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from Microsoft Live ID.", e); throw new IdentityBrokerException("Could not obtain user profile from Microsoft Live ID.", e);
} }
} }
@Override
protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
String id = getJsonProperty(profile, "id");
String email = null;
if (profile.has("emails")) {
email = getJsonProperty(profile.get("emails"), "preferred");
}
BrokeredIdentityContext user = new BrokeredIdentityContext(id);
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);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
}
@Override @Override
protected String getDefaultScopes() { protected String getDefaultScopes() {
return DEFAULT_SCOPE; return DEFAULT_SCOPE;

View file

@ -1,15 +1,22 @@
package org.keycloak.social.openshift; package org.keycloak.social.openshift;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; 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.models.KeycloakSession;
import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.util.Optional; import java.util.Optional;
/** /**
@ -63,4 +70,21 @@ public class OpenshiftV3IdentityProvider extends AbstractOAuth2IdentityProvider<
.asJson(); .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;
}
} }

View file

@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
/** /**
@ -45,22 +46,37 @@ public class PayPalIdentityProvider extends AbstractOAuth2IdentityProvider<PayPa
config.setUserInfoUrl((config.targetSandbox() ? "https://api.sandbox.paypal.com/v1" : BASE_URL) + PROFILE_RESOURCE); config.setUserInfoUrl((config.targetSandbox() ? "https://api.sandbox.paypal.com/v1" : BASE_URL) + PROFILE_RESOURCE);
} }
@Override
protected boolean supportsExternalExchange() {
return true;
}
@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);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
}
@Override @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try { try {
JsonNode profile = SimpleHttp.doGet(getConfig().getUserInfoUrl(), session).header("Authorization", "Bearer " + accessToken).asJson(); JsonNode profile = SimpleHttp.doGet(getConfig().getUserInfoUrl(), session).header("Authorization", "Bearer " + accessToken).asJson();
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id")); return extractIdentityFromProfile(null, profile);
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());
return user;
} catch (Exception e) { } catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from paypal.", e); throw new IdentityBrokerException("Could not obtain user profile from paypal.", e);
} }

View file

@ -24,12 +24,15 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap; import java.util.HashMap;
/** /**
@ -53,6 +56,41 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);
} }
@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 @Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
log.debug("doGetFederatedIdentity()"); log.debug("doGetFederatedIdentity()");
@ -62,21 +100,7 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("StackOverflow profile request to: " + URL); log.debug("StackOverflow profile request to: " + URL);
} }
JsonNode profile = SimpleHttp.doGet(URL, session).asJson().get("items").get(0); return extractIdentityFromProfile(null, SimpleHttp.doGet(URL, session).asJson());
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;
} catch (Exception e) { } catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from Stackoverflow: " + e.getMessage(), e); throw new IdentityBrokerException("Could not obtain user profile from Stackoverflow: " + e.getMessage(), e);
} }

View file

@ -18,13 +18,13 @@ package org.keycloak.social.twitter;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken; import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details; import org.keycloak.events.Details;
@ -142,7 +142,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
} }
protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) { protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
String accessToken = tokenUserSession.getNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN); String accessToken = tokenUserSession.getNote(IdentityProvider.FEDERATED_ACCESS_TOKEN);
if (accessToken == null) { if (accessToken == null) {
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject); return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
} }
@ -226,7 +226,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
if (getConfig().isStoreToken()) { if (getConfig().isStoreToken()) {
identity.setToken(token); identity.setToken(token);
} }
identity.getContextData().put(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, token); identity.getContextData().put(IdentityProvider.FEDERATED_ACCESS_TOKEN, token);
identity.setIdpConfig(getConfig()); identity.setIdpConfig(getConfig());
identity.setCode(state); identity.setCode(state);
@ -256,7 +256,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
@Override @Override
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) { public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
authSession.setUserSessionNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, (String)context.getContextData().get(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN)); authSession.setUserSessionNote(IdentityProvider.FEDERATED_ACCESS_TOKEN, (String)context.getContextData().get(IdentityProvider.FEDERATED_ACCESS_TOKEN));
} }

View file

@ -0,0 +1,43 @@
/*
* Copyright 2017 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.testsuite.pages.social;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class BitbucketLoginPage extends AbstractSocialLoginPage {
@FindBy(name = "username")
private WebElement usernameInput;
@FindBy(name = "password")
private WebElement passwordInput;
@FindBy(name = "commit")
private WebElement loginButton;
@Override
public void login(String user, String password) {
usernameInput.sendKeys(user);
passwordInput.sendKeys(password);
passwordInput.sendKeys(Keys.RETURN);
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2017 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.testsuite.pages.social;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class GitLabLoginPage extends AbstractSocialLoginPage {
@FindBy(id = "user_login")
//@FindBy(name = "user[login]")
private WebElement usernameInput;
@FindBy(id = "user_password")
//@FindBy(name = "user[password]")
private WebElement passwordInput;
@FindBy(name = "commit")
private WebElement loginButton;
@Override
public void login(String user, String password) {
usernameInput.sendKeys(user);
passwordInput.sendKeys(password);
passwordInput.sendKeys(Keys.RETURN);
}
}

View file

@ -35,9 +35,20 @@ public class GreenMailRule extends ExternalResource {
private GreenMail greenMail; private GreenMail greenMail;
private int port = 3025;
private String host = "localhost";
public GreenMailRule() {
}
public GreenMailRule(int port, String host) {
this.port = port;
this.host = host;
}
@Override @Override
protected void before() throws Throwable { protected void before() throws Throwable {
ServerSetup setup = new ServerSetup(3025, "localhost", "smtp"); ServerSetup setup = new ServerSetup(port, host, "smtp");
greenMail = new GreenMail(setup); greenMail = new GreenMail(setup);
greenMail.start(); greenMail.start();

View file

@ -464,40 +464,58 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation(); IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation();
rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(false)); rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(false));
adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep); 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() Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
.post(Entity.form( .post(Entity.form(
new Form() new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken) .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) .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
)); ));
Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(200, response.getStatus());
String json = response.readEntity(String.class); AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
System.out.println(json); String exchangedAccessToken = tokenResponse.getToken();
Assert.assertTrue(json.contains("Invalid server config")); 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(); IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation();
rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(true)); rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(true));
rep.getConfig().put(OIDCIdentityProviderConfig.USE_JWKS_URL, String.valueOf(true)); rep.getConfig().put(OIDCIdentityProviderConfig.USE_JWKS_URL, String.valueOf(true));
rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl()); rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl());
String parentIssuer = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
.path(PARENT_IDP)
.build().toString();
rep.getConfig().put("issuer", parentIssuer);
adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep); adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep);
String exchangedUserId = null; String exchangedUserId = null;
String exchangedUsername = null; String exchangedUsername = null;
{ {
// valid exchange // test signature validation
Response response = exchangeUrl.request() Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
.post(Entity.form( .post(Entity.form(
new Form() new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken) .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) .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
)); ));
@ -554,7 +572,7 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
new Form() new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken) .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) .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
)); ));
@ -583,6 +601,45 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity();
Assert.assertEquals(1, links.size());
}
{
// check that we can exchange without specifying an SUBJECT_ISSUER
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_TOKEN_TYPE)
));
Assert.assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String exchangedAccessToken = tokenResponse.getToken();
JWSInput jws = new JWSInput(tokenResponse.getToken());
AccessToken token = jws.readJsonContent(AccessToken.class);
response.close();
String exchanged2UserId = token.getSubject();
String exchanged2Username = token.getPreferredUsername();
// assert that we get the same linked account as was previously imported
Assert.assertEquals(exchangedUserId, exchanged2UserId);
Assert.assertEquals(exchangedUsername, exchanged2Username);
// test logout
response = childLogoutWebTarget(httpClient)
.queryParam("id_token_hint", exchangedAccessToken)
.request()
.get();
response.close();
Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity(); List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity();
Assert.assertEquals(1, links.size()); Assert.assertEquals(1, links.size());
} }
@ -597,7 +654,7 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
new Form() new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken) .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) .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
)); ));

View file

@ -3,20 +3,36 @@ package org.keycloak.testsuite.broker;
import org.jboss.arquillian.graphene.Graphene; import org.jboss.arquillian.graphene.Graphene;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.After; import org.junit.After;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.social.openshift.OpenshiftV3IdentityProvider; import org.keycloak.social.openshift.OpenshiftV3IdentityProvider;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.auth.page.login.UpdateAccount; import org.keycloak.testsuite.auth.page.login.UpdateAccount;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.social.AbstractSocialLoginPage; import org.keycloak.testsuite.pages.social.AbstractSocialLoginPage;
import org.keycloak.testsuite.pages.social.BitbucketLoginPage;
import org.keycloak.testsuite.pages.social.FacebookLoginPage; import org.keycloak.testsuite.pages.social.FacebookLoginPage;
import org.keycloak.testsuite.pages.social.GitHubLoginPage; import org.keycloak.testsuite.pages.social.GitHubLoginPage;
import org.keycloak.testsuite.pages.social.GitLabLoginPage;
import org.keycloak.testsuite.pages.social.GoogleLoginPage; import org.keycloak.testsuite.pages.social.GoogleLoginPage;
import org.keycloak.testsuite.pages.social.LinkedInLoginPage; import org.keycloak.testsuite.pages.social.LinkedInLoginPage;
import org.keycloak.testsuite.pages.social.MicrosoftLoginPage; import org.keycloak.testsuite.pages.social.MicrosoftLoginPage;
@ -24,12 +40,21 @@ import org.keycloak.testsuite.pages.social.PayPalLoginPage;
import org.keycloak.testsuite.pages.social.StackOverflowLoginPage; import org.keycloak.testsuite.pages.social.StackOverflowLoginPage;
import org.keycloak.testsuite.pages.social.TwitterLoginPage; import org.keycloak.testsuite.pages.social.TwitterLoginPage;
import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.URLUtils; import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.BasicAuthHelper;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.ExpectedConditions;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -38,8 +63,10 @@ import java.util.Properties;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.BITBUCKET;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.FACEBOOK; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.FACEBOOK;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT;
@ -56,6 +83,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
public static final String SOCIAL_CONFIG = "social.config"; public static final String SOCIAL_CONFIG = "social.config";
public static final String REALM = "social"; public static final String REALM = "social";
public static final String EXCHANGE_CLIENT = "exchange-client";
private static Properties config = new Properties(); private static Properties config = new Properties();
@ -74,7 +102,9 @@ public class SocialLoginTest extends AbstractKeycloakTest {
MICROSOFT("microsoft", MicrosoftLoginPage.class), MICROSOFT("microsoft", MicrosoftLoginPage.class),
PAYPAL("paypal", PayPalLoginPage.class), PAYPAL("paypal", PayPalLoginPage.class),
STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class), STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class),
OPENSHIFT("openshift-v3", null); OPENSHIFT("openshift-v3", null),
GITLAB("gitlab", GitLabLoginPage.class),
BITBUCKET("bitbucket", BitbucketLoginPage.class);
private String id; private String id;
private Class<? extends AbstractSocialLoginPage> pageObjectClazz; private Class<? extends AbstractSocialLoginPage> pageObjectClazz;
@ -95,11 +125,15 @@ public class SocialLoginTest extends AbstractKeycloakTest {
private Provider currentTestProvider; private Provider currentTestProvider;
private static final boolean localConfig = false;
@BeforeClass @BeforeClass
public static void loadConfig() throws Exception { public static void loadConfig() throws Exception {
assumeTrue(System.getProperties().containsKey(SOCIAL_CONFIG)); if (localConfig) {
} else {
config.load(new FileInputStream(System.getProperty(SOCIAL_CONFIG))); assumeTrue(System.getProperties().containsKey(SOCIAL_CONFIG));
config.load(new FileInputStream(System.getProperty(SOCIAL_CONFIG)));
}
} }
@Before @Before
@ -133,6 +167,34 @@ public class SocialLoginTest extends AbstractKeycloakTest {
testRealms.add(rep); testRealms.add(rep);
} }
public static void setupClientExchangePermissions(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(REALM);
ClientModel client = session.realms().getClientByClientId(EXCHANGE_CLIENT, realm);
// lazy init
if (client != null) return;
client = realm.addClient(EXCHANGE_CLIENT);
client.setSecret("secret");
client.setPublicClient(false);
client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
client.setEnabled(true);
client.setDirectAccessGrantsEnabled(true);
ClientPolicyRepresentation clientPolicyRep = new ClientPolicyRepresentation();
clientPolicyRep.setName("client-policy");
clientPolicyRep.addClient(client.getId());
AdminPermissionManagement management = AdminPermissions.management(session, realm);
management.users().setPermissionsEnabled(true);
ResourceServer server = management.realmResourceServer();
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientPolicyRep, server);
management.users().adminImpersonatingPermission().addAssociatedPolicy(clientPolicy);
management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
for (IdentityProviderModel idp : realm.getIdentityProviders()) {
management.idps().setPermissionsEnabled(idp, true);
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
}
}
@Test @Test
@Ignore @Ignore
// TODO: Fix and revamp this test // TODO: Fix and revamp this test
@ -155,20 +217,57 @@ public class SocialLoginTest extends AbstractKeycloakTest {
currentTestProvider = GOOGLE; currentTestProvider = GOOGLE;
performLogin(); performLogin();
assertAccount(); assertAccount();
testTokenExchange();
} }
@Test @Test
public void facebookLogin() { public void bitbucketLogin() throws InterruptedException {
currentTestProvider = BITBUCKET;
performLogin();
assertAccount();
testTokenExchange();
}
// disabled as I can't get this to work with automated login
//@Test
public void gitLabLogin() throws InterruptedException {
currentTestProvider = GITLAB;
// I can't get automated login to work. inspected elements in browser, are not found in the GitLabLoginPage.
performLogin();
assertAccount();
testTokenExchange();
}
protected void manualLogin() throws InterruptedException {
System.out.println("****** START MANUAL LOGIN ******");
System.out.println("****** START MANUAL LOGIN ******");
System.out.println("****** START MANUAL LOGIN ******");
Thread.sleep(2000);
for (int i = 0; i < 60; i++) {
List<UserRepresentation> users = adminClient.realm(REALM).users().search(null, null, null);
if (users.size() > 0) return;
System.out.println("....waiting");
Thread.sleep(1000);
}
}
@Test
public void facebookLogin() throws InterruptedException {
currentTestProvider = FACEBOOK; currentTestProvider = FACEBOOK;
performLogin(); performLogin();
assertAccount(); assertAccount();
testTokenExchange();
} }
@Test @Test
public void githubLogin() { public void githubLogin() throws InterruptedException {
//Thread.sleep(100000000);
currentTestProvider = GITHUB; currentTestProvider = GITHUB;
performLogin(); performLogin();
assertAccount(); assertAccount();
testTokenExchange();
} }
@Test @Test
@ -201,7 +300,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
} }
@Test @Test
public void stackoverflowLogin() { public void stackoverflowLogin() throws InterruptedException {
currentTestProvider = STACKOVERFLOW; currentTestProvider = STACKOVERFLOW;
performLogin(); performLogin();
assertUpdateProfile(false, false, true); assertUpdateProfile(false, false, true);
@ -211,6 +310,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
private IdentityProviderRepresentation buildIdp(Provider provider) { private IdentityProviderRepresentation buildIdp(Provider provider) {
IdentityProviderRepresentation idp = IdentityProviderBuilder.create().alias(provider.id()).providerId(provider.id()).build(); IdentityProviderRepresentation idp = IdentityProviderBuilder.create().alias(provider.id()).providerId(provider.id()).build();
idp.setEnabled(true); idp.setEnabled(true);
idp.setStoreToken(true);
idp.getConfig().put("clientId", getConfig(provider, "clientId")); idp.getConfig().put("clientId", getConfig(provider, "clientId"));
idp.getConfig().put("clientSecret", getConfig(provider, "clientSecret")); idp.getConfig().put("clientSecret", getConfig(provider, "clientSecret"));
if (provider == STACKOVERFLOW) { if (provider == STACKOVERFLOW) {
@ -289,4 +389,119 @@ public class SocialLoginTest extends AbstractKeycloakTest {
updateAccountPage.submit(); updateAccountPage.submit();
} }
protected void testTokenExchange() {
testingClient.server().run(SocialLoginTest::setupClientExchangePermissions);
List<UserRepresentation> users = adminClient.realm(REALM).users().search(null, null, null);
Assert.assertEquals(1, users.size());
String username = users.get(0).getUsername();
Client httpClient = ClientBuilder.newClient();
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
.path(REALM)
.path("protocol/openid-connect/token");
// obtain social token
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.REQUESTED_SUBJECT, username)
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_ISSUER, currentTestProvider.id())
));
Assert.assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
response.close();
String socialToken = tokenResponse.getToken();
Assert.assertNotNull(socialToken);
// remove all users
removeUser();
users = adminClient.realm(REALM).users().search(null, null, null);
Assert.assertEquals(0, users.size());
// now try external exchange where we trust social provider and import the external token.
response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, socialToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, currentTestProvider.id())
));
Assert.assertEquals(200, response.getStatus());
tokenResponse = response.readEntity(AccessTokenResponse.class);
response.close();
users = adminClient.realm(REALM).users().search(null, null, null);
Assert.assertEquals(1, users.size());
Assert.assertEquals(username, users.get(0).getUsername());
// remove all users
removeUser();
users = adminClient.realm(REALM).users().search(null, null, null);
Assert.assertEquals(0, users.size());
///// Test that we can update social token from session with stored tokens turned off.
// turn off store token
IdentityProviderRepresentation idp = adminClient.realm(REALM).identityProviders().get(currentTestProvider.id).toRepresentation();
idp.setStoreToken(false);
adminClient.realm(REALM).identityProviders().get(idp.getAlias()).update(idp);
// first exchange social token to get a user session that should store the social token there
response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, socialToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, currentTestProvider.id())
));
Assert.assertEquals(200, response.getStatus());
tokenResponse = response.readEntity(AccessTokenResponse.class);
String keycloakToken = tokenResponse.getToken();
response.close();
// now take keycloak token and make sure it can get back the social token from the user session since stored tokens are off
response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, keycloakToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_ISSUER, currentTestProvider.id())
));
Assert.assertEquals(200, response.getStatus());
tokenResponse = response.readEntity(AccessTokenResponse.class);
response.close();
Assert.assertEquals(socialToken, tokenResponse.getToken());
// turn on store token
idp = adminClient.realm(REALM).identityProviders().get(currentTestProvider.id).toRepresentation();
idp.setStoreToken(true);
adminClient.realm(REALM).identityProviders().get(idp.getAlias()).update(idp);
httpClient.close();
}
} }