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