diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index bedef8d97d..20d6e7388c 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -98,6 +98,7 @@ public interface OAuth2Constants {
String SUBJECT_TOKEN="subject_token";
String SUBJECT_TOKEN_TYPE="subject_token_type";
String REQUESTED_TOKEN_TYPE="requested_token_type";
+ String ISSUED_TOKEN_TYPE="issued_token_type";
String REQUESTED_ISSUER="requested_issuer";
String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
diff --git a/pom.xml b/pom.xml
index d4d990411e..7ae460f9bf 100755
--- a/pom.xml
+++ b/pom.xml
@@ -43,7 +43,7 @@
7.2.0.Final
- 11.0.0.Beta1
+ 11.0.0.CR1-SNAPSHOT
1.2.2.Final
7.1.0.Beta1-redhat-5
1.2.2.Final
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
index 0320299c83..231dbc1d46 100755
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
@@ -16,22 +16,34 @@
*/
package org.keycloak.broker.provider;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessToken;
import org.keycloak.sessions.AuthenticationSessionModel;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
/**
* @author Pedro Igor
*/
public abstract class AbstractIdentityProvider implements IdentityProvider {
+ public static final String ACCOUNT_LINK_URL = "account-link-url";
protected final KeycloakSession session;
private final C config;
@@ -74,6 +86,62 @@ public abstract class AbstractIdentityProvider
}
+ public Response exchangeNotSupported() {
+ Map error = new HashMap<>();
+ error.put("error", "invalid_target");
+ error.put("error_description", "target_exchange_unsupported");
+ return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+
+ public Response exchangeNotLinked(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "invalid_target");
+ }
+
+ protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token, String reason) {
+ Map error = new HashMap<>();
+ error.put("error", "invalid_target");
+ error.put("error_description", reason);
+ String accountLinkUrl = getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token);
+ if (accountLinkUrl != null) error.put(ACCOUNT_LINK_URL, accountLinkUrl);
+ return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+
+ protected String getLinkingUrl(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token) {
+ if (authorizedClient.getClientId().equals(token.getIssuedFor())) {
+ String provider = getConfig().getAlias();
+ String clientId = authorizedClient.getClientId();
+ String nonce = UUID.randomUUID().toString();
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ String input = nonce + tokenUserSession.getId() + clientId + provider;
+ byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
+ String hash = Base64Url.encode(check);
+ return KeycloakUriBuilder.fromUri(uriInfo.getBaseUri())
+ .path("/realms/{realm}/broker/{provider}/link")
+ .queryParam("nonce", nonce)
+ .queryParam("hash", hash)
+ .queryParam("client_id", clientId)
+ .build(authorizedClient.getRealm().getName(), provider)
+ .toString();
+ }
+ return null;
+ }
+
+ public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "token_expired");
+ }
+
+ public Response exchangeUnsupportedRequiredType() {
+ Map error = new HashMap<>();
+ error.put("error", "invalid_target");
+ error.put("error_description", "response_token_type_unsupported");
+ return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+
@Override
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java
index 36755d4588..64f9db113d 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java
@@ -23,6 +23,7 @@ import org.keycloak.representations.AccessToken;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
/**
* @author Bill Burke
@@ -38,5 +39,5 @@ public interface TokenExchangeTo {
* @param params form parameters received for requested exchange
* @return
*/
- Response exchangeTo(ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap params);
+ Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap params);
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index f4a877e17e..84314930fc 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -24,22 +24,32 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.broker.provider.TokenExchangeTo;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
@@ -51,7 +61,7 @@ import java.util.regex.Pattern;
/**
* @author Pedro Igor
*/
-public abstract class AbstractOAuth2IdentityProvider extends AbstractIdentityProvider {
+public abstract class AbstractOAuth2IdentityProvider extends AbstractIdentityProvider implements TokenExchangeTo {
protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
@@ -136,14 +146,76 @@ public abstract class AbstractOAuth2IdentityProvider params) {
+ String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
+ if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
+ return exchangeUnsupportedRequiredType();
+ }
+ if (!getConfig().isStoreToken()) {
+ String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
+ if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
+ return exchangeNotSupported();
+ }
+ return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ } else {
+ return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ }
+
+ protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
+ if (model == null || model.getToken() == null) {
+ return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ String accessToken = extractTokenFromResponse(model.getToken(), getAccessTokenResponseParameter());
+ if (accessToken == null) {
+ model.setToken(null);
+ session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ AccessTokenResponse tokenResponse = new AccessTokenResponse();
+ tokenResponse.setToken(accessToken);
+ tokenResponse.setIdToken(null);
+ tokenResponse.setRefreshToken(null);
+ tokenResponse.setRefreshExpiresIn(0);
+ tokenResponse.getOtherClaims().clear();
+ tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+ return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+
+ protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
+ if (accessToken == null) {
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ AccessTokenResponse tokenResponse = new AccessTokenResponse();
+ tokenResponse.setToken(accessToken);
+ tokenResponse.setIdToken(null);
+ tokenResponse.setRefreshToken(null);
+ tokenResponse.setRefreshExpiresIn(0);
+ tokenResponse.getOtherClaims().clear();
+ tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+ return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+
+
public BrokeredIdentityContext getFederatedIdentity(String response) {
- String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
+ String accessToken = extractTokenFromResponse(response, getAccessTokenResponseParameter());
if (accessToken == null) {
throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
}
- return doGetFederatedIdentity(accessToken);
+ BrokeredIdentityContext context = doGetFederatedIdentity(accessToken);
+ context.getContextData().put(FEDERATED_ACCESS_TOKEN, accessToken);
+ return context;
+ }
+
+ protected String getAccessTokenResponseParameter() {
+ return OAUTH2_PARAMETER_ACCESS_TOKEN;
}
@@ -186,6 +258,12 @@ public abstract class AbstractOAuth2IdentityProvider {
+public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider {
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
@@ -68,6 +74,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider 0 && currentTime > exp) {
- String response = refreshToken(session, userSession);
+ String response = refreshTokenForLogout(session, userSession);
AccessTokenResponse tokenResponse = null;
try {
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
@@ -215,8 +222,108 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider 0) {
+ long accessTokenExpiration = Time.currentTime() + newResponse.getExpiresIn();
+ newResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
+ response = JsonSerialization.writeValueAsString(newResponse);
+ }
+ String oldToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
+ if (oldToken != null && oldToken.equals(tokenResponse.getToken())) {
+ long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
+ tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
+ tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
+ tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
+ tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
+
+ }
+ model.setToken(response);
+ tokenResponse = newResponse;
+ } else if (exp != null) {
+ tokenResponse.setExpiresIn(exp - Time.currentTime());
+ }
+ tokenResponse.setIdToken(null);
+ tokenResponse.setRefreshToken(null);
+ tokenResponse.setRefreshExpiresIn(0);
+ tokenResponse.getOtherClaims().clear();
+ tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+ return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ try {
+ long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
+ String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
+ String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
+ String idToken = tokenUserSession.getNote(FEDERATED_ID_TOKEN);
+ if (expiration == 0 || expiration > Time.currentTime()) {
+ AccessTokenResponse tokenResponse = new AccessTokenResponse();
+ tokenResponse.setExpiresIn(expiration);
+ tokenResponse.setToken(accessToken);
+ tokenResponse.setIdToken(null);
+ tokenResponse.setRefreshToken(null);
+ tokenResponse.setRefreshExpiresIn(0);
+ tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+ return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+ String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
+ .param("refresh_token", refreshToken)
+ .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
+ .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
+ .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
+ if (response.contains("error")) {
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
+ long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
+ tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
+ tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
+ tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
+ tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
+ newResponse.setIdToken(null);
+ newResponse.setRefreshToken(null);
+ newResponse.setRefreshExpiresIn(0);
+ newResponse.getOtherClaims().clear();
+ newResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+ newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+ return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
@Override
public BrokeredIdentityContext getFederatedIdentity(String response) {
AccessTokenResponse tokenResponse = null;
@@ -235,6 +342,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider 0) {
+ long accessTokenExpiration = Time.currentTime() + tokenResponse.getExpiresIn();
+ tokenResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
+ response1 = JsonSerialization.writeValueAsString(tokenResponse);
+ }
+ response = response1;
identity.setToken(response);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index b0b3e198ac..c9c394a2e1 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -23,6 +23,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.broker.provider.IdentityProvider;
+import org.keycloak.broker.provider.TokenExchangeTo;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Base64Url;
@@ -34,6 +36,7 @@ import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -53,6 +56,7 @@ import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
+import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
@@ -65,6 +69,7 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.util.Map;
import java.util.Objects;
@@ -582,10 +587,32 @@ public class TokenEndpoint {
String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
if (requestedIssuer == null) {
+ return exchangeClientToClient(authResult);
+ } else {
+ return exchangeToIdentityProvider(authResult, requestedIssuer);
+ }
+ }
+ public Response exchangeToIdentityProvider(AuthenticationManager.AuthResult authResult, String requestedIssuer) {
+ IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
+ if (providerModel == null) {
+ event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
}
- return exchangeClientToClient(authResult);
+ IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
+ if (!(provider instanceof TokenExchangeTo)) {
+ event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
+ }
+ if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, providerModel)) {
+ logger.debug("Client not allowed to exchange for linked token");
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+ }
+ Response response = ((TokenExchangeTo)provider).exchangeTo(uriInfo, client, authResult.getSession(), authResult.getUser(), authResult.getToken(), formParams);
+ return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+
}
public Response exchangeClientToClient(AuthenticationManager.AuthResult subject) {
@@ -617,24 +644,6 @@ public class TokenEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
}
- boolean exchangeFromAllowed = false;
- for (String aud : subject.getToken().getAudience()) {
- ClientModel audClient = realm.getClientByClientId(aud);
- if (audClient == null) continue;
- if (audClient.equals(client)) {
- exchangeFromAllowed = true;
- break;
- }
- if (AdminPermissions.management(session, realm).clients().canExchangeFrom(client, audClient)) {
- exchangeFromAllowed = true;
- break;
- }
- }
- if (!exchangeFromAllowed) {
- logger.debug("Client does not have exchange rights for audience of provided token");
- event.error(Errors.NOT_ALLOWED);
- throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
- }
if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
logger.debug("Client does not have exchange rights for target audience");
event.error(Errors.NOT_ALLOWED);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
index 4c08c204b2..3f07b47c18 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
@@ -19,12 +19,15 @@ package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;
@@ -43,8 +46,11 @@ import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -402,5 +408,58 @@ public class IdentityProviderResource {
}
+ /**
+ * Return object stating whether client Authorization permissions have been initialized or not and a reference
+ *
+ * @return
+ */
+ @Path("management/permissions")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ public ManagementPermissionReference getManagementPermissions() {
+ this.auth.realm().requireViewIdentityProviders();
+
+ AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
+ if (!permissions.idps().isPermissionsEnabled(identityProviderModel)) {
+ return new ManagementPermissionReference();
+ }
+ return toMgmtRef(identityProviderModel, permissions);
+ }
+
+ public static ManagementPermissionReference toMgmtRef(IdentityProviderModel model, AdminPermissionManagement permissions) {
+ ManagementPermissionReference ref = new ManagementPermissionReference();
+ ref.setEnabled(true);
+ ref.setResource(permissions.idps().resource(model).getId());
+ ref.setScopePermissions(permissions.idps().getPermissions(model));
+ return ref;
+ }
+
+
+ /**
+ * Return object stating whether client Authorization permissions have been initialized or not and a reference
+ *
+ *
+ * @return initialized manage permissions reference
+ */
+ @Path("management/permissions")
+ @PUT
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.APPLICATION_JSON)
+ @NoCache
+ public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
+ this.auth.realm().requireManageIdentityProviders();
+ AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
+ permissions.idps().setPermissionsEnabled(identityProviderModel, ref.isEnabled());
+ if (ref.isEnabled()) {
+ return toMgmtRef(identityProviderModel, permissions);
+ } else {
+ return new ManagementPermissionReference();
+ }
+ }
+
+
+
+
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
index d8eb94a4b4..4dfce43700 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
@@ -27,7 +27,6 @@ import org.keycloak.models.ClientModel;
public interface AdminPermissionManagement {
public static final String MANAGE_SCOPE = "manage";
public static final String VIEW_SCOPE = "view";
- public static final String EXCHANGE_FROM_SCOPE="exchange-from";
public static final String EXCHANGE_TO_SCOPE="exchange-to";
ClientModel getRealmManagementClient();
@@ -38,6 +37,7 @@ public interface AdminPermissionManagement {
UserPermissionManagement users();
GroupPermissionManagement groups();
ClientPermissionManagement clients();
+ IdentityProviderPermissionManagement idps();
ResourceServer realmResourceServer();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
index ccf9679609..03bfa73b47 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
@@ -41,12 +41,8 @@ public interface ClientPermissionManagement {
Map getPermissions(ClientModel client);
- boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from);
-
boolean canExchangeTo(ClientModel authorizedClient, ClientModel to);
- Policy exchangeFromPermission(ClientModel client);
-
Policy exchangeToPermission(ClientModel client);
Policy mapRolesPermission(ClientModel client);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
index bbb7bf4d29..8aeb9abdf4 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
@@ -18,10 +18,8 @@ package org.keycloak.services.resources.admin.permissions;
import org.jboss.logging.Logger;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.common.ClientModelIdentity;
import org.keycloak.authorization.common.DefaultEvaluationContext;
-import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
@@ -32,8 +30,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.representations.AccessToken;
import org.keycloak.services.ForbiddenException;
import java.util.Arrays;
@@ -44,7 +40,6 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
-import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_FROM_SCOPE;
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
/**
@@ -54,7 +49,7 @@ import static org.keycloak.services.resources.admin.permissions.AdminPermissionM
* @author Bill Burke
* @version $Revision: 1 $
*/
-class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionManagement {
+class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionManagement {
private static final Logger logger = Logger.getLogger(ClientPermissions.class);
protected final KeycloakSession session;
protected final RealmModel realm;
@@ -95,11 +90,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
}
- private String getExchangeFromPermissionName(ClientModel client) {
- return EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId();
- }
-
- private void initialize(ClientModel client) {
+ private void initialize(ClientModel client) {
ResourceServer server = root.findOrCreateResourceServer(client);
Scope manageScope = manageScope(server);
if (manageScope == null) {
@@ -116,7 +107,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server);
Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server);
Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server);
- Scope exchangeFromScope = root.initializeScope(EXCHANGE_FROM_SCOPE, server);
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
String resourceName = getResourceName(client);
@@ -131,7 +121,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
scopeset.add(mapRoleScope);
scopeset.add(mapRoleClientScope);
scopeset.add(mapRoleCompositeScope);
- scopeset.add(exchangeFromScope);
scopeset.add(exchangeToScope);
resource.updateScopes(scopeset);
}
@@ -170,11 +159,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
if (exchangeToPermission == null) {
Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
}
- String exchangeFromPermissionName = getExchangeFromPermissionName(client);
- Policy exchangeFromPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeFromPermissionName, server.getId());
- if (exchangeFromPermission == null) {
- Helper.addEmptyScopePermission(authz, server, exchangeFromPermissionName, resource, exchangeFromScope);
- }
}
private void deletePolicy(String name, ResourceServer server) {
@@ -195,7 +179,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
deletePolicy(getMapRolesCompositePermissionName(client), server);
deletePolicy(getConfigurePermissionName(client), server);
deletePolicy(getExchangeToPermissionName(client), server);
- deletePolicy(getExchangeFromPermissionName(client), server);
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.getId());;
if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
}
@@ -223,10 +206,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.MANAGE_SCOPE, server.getId());
}
- private Scope exchangeFromScope(ResourceServer server) {
- return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_FROM_SCOPE, server.getId());
- }
-
private Scope exchangeToScope(ResourceServer server) {
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
}
@@ -314,59 +293,10 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId());
scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId());
scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId());
- scopes.put(EXCHANGE_FROM_SCOPE, exchangeFromPermission(client).getId());
scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId());
return scopes;
}
- @Override
- public boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from) {
- if (!authorizedClient.equals(from)) {
- ResourceServer server = resourceServer(from);
- if (server == null) {
- logger.debug("No resource server set up for target client");
- return false;
- }
-
- Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(from), server.getId());
- if (resource == null) {
- logger.debug("No resource object set up for target client");
- return false;
- }
-
- Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(from), server.getId());
- if (policy == null) {
- logger.debug("No permission object set up for target client");
- return false;
- }
-
- Set associatedPolicies = policy.getAssociatedPolicies();
- // if no policies attached to permission then just do default behavior
- if (associatedPolicies == null || associatedPolicies.isEmpty()) {
- logger.debug("No policies set up for permission on target client");
- return false;
- }
-
- Scope scope = exchangeFromScope(server);
- if (scope == null) {
- logger.debug(EXCHANGE_FROM_SCOPE + " not initialized");
- return false;
- }
- ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
- EvaluationContext context = new DefaultEvaluationContext(identity, session) {
- @Override
- public Map> getBaseAttributes() {
- Map> attributes = super.getBaseAttributes();
- attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
- return attributes;
- }
-
- };
- return root.evaluatePermission(resource, scope, server, context);
- }
- return true;
- }
-
@Override
public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) {
@@ -601,13 +531,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
return root.evaluatePermission(resource, scope, server);
}
- @Override
- public Policy exchangeFromPermission(ClientModel client) {
- ResourceServer server = resourceServer(client);
- if (server == null) return null;
- return authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(client), server.getId());
- }
-
@Override
public Policy exchangeToPermission(ClientModel client) {
ResourceServer server = resourceServer(client);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissionManagement.java
new file mode 100644
index 0000000000..a5b595dfea
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissionManagement.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 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.services.resources.admin.permissions;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderModel;
+
+import java.util.Map;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public interface IdentityProviderPermissionManagement {
+ boolean isPermissionsEnabled(IdentityProviderModel idp);
+
+ void setPermissionsEnabled(IdentityProviderModel idp, boolean enable);
+
+ Resource resource(IdentityProviderModel idp);
+
+ Map getPermissions(IdentityProviderModel idp);
+
+ boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to);
+
+ Policy exchangeToPermission(IdentityProviderModel idp);
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java
new file mode 100644
index 0000000000..71661b12cc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2016 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.services.resources.admin.permissions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.common.ClientModelIdentity;
+import org.keycloak.authorization.common.DefaultEvaluationContext;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
+
+/**
+ * Manages default policies for all users.
+ *
+ *
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+class IdentityProviderPermissions implements IdentityProviderPermissionManagement {
+ private static final Logger logger = Logger.getLogger(IdentityProviderPermissions.class);
+ protected final KeycloakSession session;
+ protected final RealmModel realm;
+ protected final AuthorizationProvider authz;
+ protected final MgmtPermissions root;
+
+ public IdentityProviderPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) {
+ this.session = session;
+ this.realm = realm;
+ this.authz = authz;
+ this.root = root;
+ }
+
+ private String getResourceName(IdentityProviderModel idp) {
+ return "idp.resource." + idp.getInternalId();
+ }
+
+ private String getExchangeToPermissionName(IdentityProviderModel idp) {
+ return EXCHANGE_TO_SCOPE + ".permission.idp." + idp.getInternalId();
+ }
+
+ private void initialize(IdentityProviderModel idp) {
+ ResourceServer server = root.initializeRealmResourceServer();
+ Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
+
+ String resourceName = getResourceName(idp);
+ Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
+ if (resource == null) {
+ resource = authz.getStoreFactory().getResourceStore().create(resourceName, server, server.getClientId());
+ resource.setType("IdentityProvider");
+ Set scopeset = new HashSet<>();
+ scopeset.add(exchangeToScope);
+ resource.updateScopes(scopeset);
+ }
+ String exchangeToPermissionName = getExchangeToPermissionName(idp);
+ Policy exchangeToPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeToPermissionName, server.getId());
+ if (exchangeToPermission == null) {
+ Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
+ }
+ }
+
+ private void deletePolicy(String name, ResourceServer server) {
+ Policy policy = authz.getStoreFactory().getPolicyStore().findByName(name, server.getId());
+ if (policy != null) {
+ authz.getStoreFactory().getPolicyStore().delete(policy.getId());
+ }
+
+ }
+
+ private void deletePermissions(IdentityProviderModel idp) {
+ ResourceServer server = root.initializeRealmResourceServer();
+ if (server == null) return;
+ deletePolicy(getExchangeToPermissionName(idp), server);
+ Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());;
+ if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
+ }
+
+ @Override
+ public boolean isPermissionsEnabled(IdentityProviderModel idp) {
+ ResourceServer server = root.initializeRealmResourceServer();
+ if (server == null) return false;
+
+ return authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId()) != null;
+ }
+
+ @Override
+ public void setPermissionsEnabled(IdentityProviderModel idp, boolean enable) {
+ if (enable) {
+ initialize(idp);
+ } else {
+ deletePermissions(idp);
+ }
+ }
+
+
+
+ private Scope exchangeToScope(ResourceServer server) {
+ return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
+ }
+
+ @Override
+ public Resource resource(IdentityProviderModel idp) {
+ ResourceServer server = root.initializeRealmResourceServer();
+ if (server == null) return null;
+ Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());
+ if (resource == null) return null;
+ return resource;
+ }
+
+
+ @Override
+ public Map getPermissions(IdentityProviderModel idp) {
+ initialize(idp);
+ Map scopes = new LinkedHashMap<>();
+ scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(idp).getId());
+ return scopes;
+ }
+
+ @Override
+ public boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to) {
+
+ if (!authorizedClient.equals(to)) {
+ ResourceServer server = root.initializeRealmResourceServer();
+ if (server == null) {
+ logger.debug("No resource server set up for target idp");
+ return false;
+ }
+
+ Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(to), server.getId());
+ if (resource == null) {
+ logger.debug("No resource object set up for target idp");
+ return false;
+ }
+
+ Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(to), server.getId());
+ if (policy == null) {
+ logger.debug("No permission object set up for target idp");
+ return false;
+ }
+
+ Set associatedPolicies = policy.getAssociatedPolicies();
+ // if no policies attached to permission then just do default behavior
+ if (associatedPolicies == null || associatedPolicies.isEmpty()) {
+ logger.debug("No policies set up for permission on target idp");
+ return false;
+ }
+
+ Scope scope = exchangeToScope(server);
+ if (scope == null) {
+ logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
+ return false;
+ }
+ ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
+ EvaluationContext context = new DefaultEvaluationContext(identity, session) {
+ @Override
+ public Map> getBaseAttributes() {
+ Map> attributes = super.getBaseAttributes();
+ attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
+ return attributes;
+ }
+
+ };
+ return root.evaluatePermission(resource, scope, server, context);
+ }
+ return true;
+ }
+
+ @Override
+ public Policy exchangeToPermission(IdentityProviderModel idp) {
+ ResourceServer server = root.initializeRealmResourceServer();
+ if (server == null) return null;
+ return authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(idp), server.getId());
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
index fe4a11fe93..80812f2d5d 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
@@ -67,6 +67,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
protected GroupPermissions groups;
protected RealmPermissions realmPermissions;
protected ClientPermissions clientPermissions;
+ protected IdentityProviderPermissions idpPermissions;
MgmtPermissions(KeycloakSession session, RealmModel realm) {
@@ -223,6 +224,13 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
return clientPermissions;
}
+ @Override
+ public IdentityProviderPermissions idps() {
+ if (idpPermissions != null) return idpPermissions;
+ idpPermissions = new IdentityProviderPermissions(session, realm, authz, this);
+ return idpPermissions;
+ }
+
@Override
public GroupPermissions groups() {
if (groups != null) return groups;
diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
index 77009e7f86..ad9dc40925 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -17,18 +17,26 @@
package org.keycloak.social.twitter;
import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.broker.provider.TokenExchangeTo;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
@@ -44,6 +52,7 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
@@ -52,7 +61,10 @@ import java.net.URI;
* @author Stian Thorgersen
*/
public class TwitterIdentityProvider extends AbstractIdentityProvider implements
- SocialIdentityProvider {
+ SocialIdentityProvider, TokenExchangeTo {
+
+ String TWITTER_TOKEN_TYPE="twitter";
+
protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
@@ -90,6 +102,62 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider params) {
+ String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
+ if (requestedType != null && !requestedType.equals(TWITTER_TOKEN_TYPE)) {
+ return exchangeUnsupportedRequiredType();
+ }
+ if (!getConfig().isStoreToken()) {
+ String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
+ if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
+ return exchangeNotSupported();
+ }
+ return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ } else {
+ return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ }
+
+ protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
+ FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
+ if (model == null || model.getToken() == null) {
+ return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ String accessToken = model.getToken();
+ if (accessToken == null) {
+ model.setToken(null);
+ session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ AccessTokenResponse tokenResponse = new AccessTokenResponse();
+ tokenResponse.setToken(accessToken);
+ tokenResponse.setIdToken(null);
+ tokenResponse.setRefreshToken(null);
+ tokenResponse.setRefreshExpiresIn(0);
+ tokenResponse.getOtherClaims().clear();
+ tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, TWITTER_TOKEN_TYPE);
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+ return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+
+ protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
+ String accessToken = tokenUserSession.getNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN);
+ if (accessToken == null) {
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ }
+ AccessTokenResponse tokenResponse = new AccessTokenResponse();
+ tokenResponse.setToken(accessToken);
+ tokenResponse.setIdToken(null);
+ tokenResponse.setRefreshToken(null);
+ tokenResponse.setRefreshExpiresIn(0);
+ tokenResponse.getOtherClaims().clear();
+ tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, TWITTER_TOKEN_TYPE);
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+ return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+
+
protected class Endpoint {
protected RealmModel realm;
protected AuthenticationCallback callback;
@@ -142,6 +210,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProviderBill Burke
+ * @version $Revision: 1 $
+ */
+@WebServlet("/client-linking")
+public class LinkAndExchangeServlet extends HttpServlet {
+ public AccessTokenResponse doTokenExchange(String realm, String token, String requestedIssuer,
+ String clientId, String clientSecret) throws Exception {
+ CloseableHttpClient client = new DefaultHttpClient();
+ try {
+ String exchangeUrl = KeycloakUriBuilder.fromUri(ServletTestUtils.getAuthServerUrlBase())
+ .path("/auth/realms/{realm}/protocol/openid-connect/token").build(realm).toString();
+
+ HttpPost post = new HttpPost(exchangeUrl);
+
+ List parameters = new LinkedList();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN, token));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_ISSUER, requestedIssuer));
+
+ if (clientSecret != null) {
+ String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+ post.setHeader("Authorization", authorization);
+ } else {
+ parameters.add(new BasicNameValuePair("client_id", clientId));
+
+ }
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ post.setEntity(formEntity);
+ CloseableHttpResponse response = client.execute(post);
+ AccessTokenResponse tokenResponse = JsonSerialization.readValue(response.getEntity().getContent(), AccessTokenResponse.class);
+ response.close();
+ return tokenResponse;
+ } finally {
+ client.close();
+ }
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setHeader("Cache-Control", "no-cache");
+ if (request.getRequestURI().endsWith("/link") && request.getParameter("response") == null) {
+ String provider = request.getParameter("provider");
+ String realm = request.getParameter("realm");
+ KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+ AccessToken token = session.getToken();
+ String tokenString = session.getTokenString();
+
+ String clientId = token.getAudience()[0];
+ String linkUrl = null;
+ try {
+ AccessTokenResponse response = doTokenExchange(realm, tokenString, provider, clientId, "password");
+ String error = (String)response.getOtherClaims().get("error");
+ if (error != null) {
+ System.out.println("*** error : " + error);
+ System.out.println("*** link-url: " + response.getOtherClaims().get("account-link-url"));
+ linkUrl = (String)response.getOtherClaims().get("account-link-url");
+ } else {
+ Assert.assertNotNull(response.getToken());
+ resp.setStatus(200);
+ resp.setContentType("text/html");
+ PrintWriter pw = resp.getWriter();
+ pw.printf("%s", "Client Linking");
+ pw.println("Account Linked");
+ pw.print("");
+ pw.flush();
+ return;
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ String redirectUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString())
+ .replaceQuery(null)
+ .queryParam("response", "true").build().toString();
+ String accountLinkUrl = KeycloakUriBuilder.fromUri(linkUrl)
+ .queryParam("redirect_uri", redirectUri).build().toString();
+ resp.setStatus(302);
+ resp.setHeader("Location", accountLinkUrl);
+ } else if (request.getRequestURI().endsWith("/link") && request.getParameter("response") != null) {
+ resp.setStatus(200);
+ resp.setContentType("text/html");
+ PrintWriter pw = resp.getWriter();
+ pw.printf("%s", "Client Linking");
+ String error = request.getParameter("link_error");
+ if (error != null) {
+ pw.println("Link error: " + error);
+ } else {
+ pw.println("Account Linked");
+ }
+ pw.print("");
+ pw.flush();
+ } else {
+ resp.setStatus(200);
+ resp.setContentType("text/html");
+ PrintWriter pw = resp.getWriter();
+ pw.printf("%s", "Client Linking");
+ pw.println("Unknown request: " + request.getRequestURL().toString());
+ pw.print("");
+ pw.flush();
+
+ }
+
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
new file mode 100644
index 0000000000..bbc5cdb902
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
@@ -0,0 +1,657 @@
+/*
+ * Copyright 2016 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.adapter.servlet;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.FederatedIdentityRepresentation;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
+import org.keycloak.testsuite.ActionURIUtils;
+import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.broker.BrokerTestTools;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
+import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
+import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
+import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapterTest {
+ public static final String CHILD_IDP = "child";
+ public static final String PARENT_IDP = "parent-idp";
+ public static final String PARENT_USERNAME = "parent";
+
+ @Page
+ protected LoginUpdateProfilePage loginUpdateProfilePage;
+
+ @Page
+ protected AccountUpdateProfilePage profilePage;
+
+ @Page
+ private LoginPage loginPage;
+
+ @Page
+ protected ErrorPage errorPage;
+
+ public static class ClientApp extends AbstractPageWithInjectedUrl {
+
+ public static final String DEPLOYMENT_NAME = "client-linking";
+
+ @ArquillianResource
+ @OperateOnDeployment(DEPLOYMENT_NAME)
+ private URL url;
+
+ @Override
+ public URL getInjectedUrl() {
+ return url;
+ }
+
+ }
+
+ @Page
+ private ClientApp appPage;
+
+ @Override
+ public void beforeAuthTest() {
+ }
+
+ @Override
+ public void addAdapterTestRealms(List testRealms) {
+ RealmRepresentation realm = new RealmRepresentation();
+ realm.setRealm(CHILD_IDP);
+ realm.setEnabled(true);
+ ClientRepresentation servlet = new ClientRepresentation();
+ servlet.setClientId("client-linking");
+ servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ String uri = "/client-linking";
+ if (!isRelative()) {
+ uri = appServerContextRootPage.toString() + uri;
+ }
+ servlet.setAdminUrl(uri);
+ servlet.setDirectAccessGrantsEnabled(true);
+ servlet.setBaseUrl(uri);
+ servlet.setRedirectUris(new LinkedList<>());
+ servlet.getRedirectUris().add(uri + "/*");
+ servlet.setSecret("password");
+ servlet.setFullScopeAllowed(true);
+ realm.setClients(new LinkedList<>());
+ realm.getClients().add(servlet);
+ testRealms.add(realm);
+
+
+ realm = new RealmRepresentation();
+ realm.setRealm(PARENT_IDP);
+ realm.setEnabled(true);
+
+ testRealms.add(realm);
+
+ }
+
+
+ @Deployment(name = ClientApp.DEPLOYMENT_NAME)
+ protected static WebArchive accountLink() {
+ return servletDeployment(ClientApp.DEPLOYMENT_NAME, LinkAndExchangeServlet.class, ServletTestUtils.class);
+ }
+
+ @Before
+ public void addIdpUser() {
+ RealmResource realm = adminClient.realms().realm(PARENT_IDP);
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername(PARENT_USERNAME);
+ user.setEnabled(true);
+ String userId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+
+ }
+
+ private String childUserId = null;
+
+
+ @Before
+ public void addChildUser() {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername("child");
+ user.setEnabled(true);
+ childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+ UserRepresentation user2 = new UserRepresentation();
+ user2.setUsername("child2");
+ user2.setEnabled(true);
+ String user2Id = createUserAndResetPasswordWithAdminClient(realm, user2, "password");
+
+ // have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions
+ realm.roles().create(new RoleRepresentation("user", null, false));
+ RoleRepresentation role = realm.roles().get("user").toRepresentation();
+ List roles = new LinkedList<>();
+ roles.add(role);
+ realm.users().get(childUserId).roles().realmLevel().add(roles);
+ realm.users().get(user2Id).roles().realmLevel().add(roles);
+ ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0);
+ role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation();
+ roles.clear();
+ roles.add(role);
+ realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles);
+ realm.users().get(user2Id).roles().clientLevel(brokerService.getId()).add(roles);
+
+ }
+
+ public static void setupRealm(KeycloakSession session) {
+ RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
+ ClientModel client = realm.getClientByClientId("client-linking");
+ IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
+ Assert.assertNotNull(idp);
+
+ AdminPermissionManagement management = AdminPermissions.management(session, realm);
+ management.idps().setPermissionsEnabled(idp, true);
+ ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
+ clientRep.setName("toIdp");
+ clientRep.addClient(client.getId());
+ ResourceServer server = management.realmResourceServer();
+ Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
+ management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
+
+ }
+ @Before
+ public void createBroker() {
+ createParentChild();
+ testingClient.server().run(AbstractLinkAndExchangeTest::setupRealm);
+ }
+
+ public void createParentChild() {
+ BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext);
+ }
+
+
+ @Test
+ public void testErrorConditions() throws Exception {
+
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ ClientRepresentation client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
+
+ UriBuilder redirectUri = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("link")
+ .queryParam("response", "true");
+
+ UriBuilder directLinking = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth")
+ .path("realms/child/broker/{provider}/link")
+ .queryParam("client_id", "client-linking")
+ .queryParam("redirect_uri", redirectUri.build())
+ .queryParam("hash", Base64Url.encode("crap".getBytes()))
+ .queryParam("nonce", UUID.randomUUID().toString());
+
+ String linkUrl = directLinking
+ .build(PARENT_IDP).toString();
+
+ // test not logged in
+
+ navigateTo(linkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+
+ Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_logged_in"));
+
+ logoutAll();
+
+ // now log in
+
+ navigateTo( appPage.getInjectedUrl() + "/hello");
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
+ Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
+
+ // now test CSRF with bad hash.
+
+ navigateTo(linkUrl);
+
+ Assert.assertTrue(driver.getPageSource().contains("We're sorry..."));
+
+ logoutAll();
+
+ // now log in again with client that does not have scope
+
+ String accountId = adminClient.realms().realm(CHILD_IDP).clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0).getId();
+ RoleRepresentation manageAccount = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT).toRepresentation();
+ RoleRepresentation manageLinks = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT_LINKS).toRepresentation();
+ RoleRepresentation userRole = adminClient.realms().realm(CHILD_IDP).roles().get("user").toRepresentation();
+
+ client.setFullScopeAllowed(false);
+ ClientResource clientResource = adminClient.realms().realm(CHILD_IDP).clients().get(client.getId());
+ clientResource.update(client);
+
+ List roles = new LinkedList<>();
+ roles.add(userRole);
+ clientResource.getScopeMappings().realmLevel().add(roles);
+
+ navigateTo( appPage.getInjectedUrl() + "/hello");
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
+ Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
+
+
+ UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("link");
+ String clientLinkUrl = linkBuilder.clone()
+ .queryParam("realm", CHILD_IDP)
+ .queryParam("provider", PARENT_IDP).build().toString();
+
+
+ navigateTo(clientLinkUrl);
+
+ Assert.assertTrue(driver.getCurrentUrl().contains("error=not_allowed"));
+
+ logoutAll();
+
+ // add MANAGE_ACCOUNT_LINKS scope should pass.
+
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+
+ roles = new LinkedList<>();
+ roles.add(manageLinks);
+ clientResource.getScopeMappings().clientLevel(accountId).add(roles);
+
+ navigateTo(clientLinkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+ loginPage.login(PARENT_USERNAME, "password");
+
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
+ Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
+
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertFalse(links.isEmpty());
+
+ realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
+
+ logoutAll();
+
+ navigateTo(clientLinkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+
+ Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
+
+ logoutAll();
+
+ // add MANAGE_ACCOUNT scope should pass
+
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+
+ roles = new LinkedList<>();
+ roles.add(manageAccount);
+ clientResource.getScopeMappings().clientLevel(accountId).add(roles);
+
+ navigateTo(clientLinkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+ loginPage.login(PARENT_USERNAME, "password");
+
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
+ Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
+
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertFalse(links.isEmpty());
+
+ realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
+
+ logoutAll();
+
+ navigateTo(clientLinkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+
+ Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
+
+ logoutAll();
+
+
+ // undo fullScopeAllowed
+
+ client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
+ client.setFullScopeAllowed(true);
+ clientResource.update(client);
+
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ logoutAll();
+
+
+
+
+
+
+ }
+
+ @Test
+ public void testAccountLink() throws Exception {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("link");
+ String linkUrl = linkBuilder.clone()
+ .queryParam("realm", CHILD_IDP)
+ .queryParam("provider", PARENT_IDP).build().toString();
+ System.out.println("linkUrl: " + linkUrl);
+ navigateTo(linkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP));
+ loginPage.login("child", "password");
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+ loginPage.login(PARENT_USERNAME, "password");
+ System.out.println("After linking: " + driver.getCurrentUrl());
+ System.out.println(driver.getPageSource());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
+ Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, "client-linking", "password");
+ Assert.assertNotNull(response.getAccessToken());
+ Assert.assertNull(response.getError());
+ Client httpClient = ClientBuilder.newClient();
+ String firstToken = getToken(response, httpClient);
+ Assert.assertNotNull(firstToken);
+
+
+ navigateTo(linkUrl);
+ Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
+ String nextToken = getToken(response, httpClient);
+ Assert.assertNotNull(nextToken);
+ Assert.assertNotEquals(firstToken, nextToken);
+
+
+
+
+
+
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertFalse(links.isEmpty());
+
+ realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ logoutAll();
+
+
+ }
+
+ private String getToken(OAuthClient.AccessTokenResponse response, Client httpClient) throws Exception {
+ String idpToken = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
+ .path("realms")
+ .path("child/broker")
+ .path(PARENT_IDP)
+ .path("token")
+ .request()
+ .header("Authorization", "Bearer " + response.getAccessToken())
+ .get(String.class);
+ AccessTokenResponse res = JsonSerialization.readValue(idpToken, AccessTokenResponse.class);
+ return res.getToken();
+ }
+
+ public void logoutAll() {
+ String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString();
+ navigateTo(logoutUri);
+ logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_IDP).toString();
+ navigateTo(logoutUri);
+ }
+
+ @Test
+ public void testLinkOnlyProvider() throws Exception {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ IdentityProviderRepresentation rep = realm.identityProviders().get(PARENT_IDP).toRepresentation();
+ rep.setLinkOnly(true);
+ realm.identityProviders().get(PARENT_IDP).update(rep);
+ try {
+
+ List links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("link");
+ String linkUrl = linkBuilder.clone()
+ .queryParam("realm", CHILD_IDP)
+ .queryParam("provider", PARENT_IDP).build().toString();
+ navigateTo(linkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+
+ // should not be on login page. This is what we are testing
+ Assert.assertFalse(driver.getPageSource().contains(PARENT_IDP));
+
+ // now test that we can still link.
+ loginPage.login("child", "password");
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+ loginPage.login(PARENT_USERNAME, "password");
+ System.out.println("After linking: " + driver.getCurrentUrl());
+ System.out.println(driver.getPageSource());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
+ Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
+
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertFalse(links.isEmpty());
+
+ realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ logoutAll();
+
+ System.out.println("testing link-only attack");
+
+ navigateTo(linkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+
+ System.out.println("login page uri is: " + driver.getCurrentUrl());
+
+ // ok, now scrape the code from page
+ String pageSource = driver.getPageSource();
+ String action = ActionURIUtils.getActionURIFromPageSource(pageSource);
+ System.out.println("action uri: " + action);
+
+ Map queryParams = ActionURIUtils.parseQueryParamsFromActionURI(action);
+ System.out.println("query params: " + queryParams);
+
+ // now try and use the code to login to remote link-only idp
+
+ String uri = "/auth/realms/child/broker/parent-idp/login";
+
+ uri = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot())
+ .path(uri)
+ .queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
+ .queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
+ .build().toString();
+
+ System.out.println("hack uri: " + uri);
+
+ navigateTo(uri);
+
+ Assert.assertTrue(driver.getPageSource().contains("Could not send authentication request to identity provider."));
+
+
+
+
+
+ } finally {
+
+ rep.setLinkOnly(false);
+ realm.identityProviders().get(PARENT_IDP).update(rep);
+ }
+
+
+ }
+
+
+ @Test
+ public void testAccountNotLinkedAutomatically() throws Exception {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ // Login to account mgmt first
+ profilePage.open(CHILD_IDP);
+ WaitUtils.waitForPageToLoad(driver);
+
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ profilePage.assertCurrent();
+
+ // Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie
+ UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("nosuch");
+ String linkUrl = linkBuilder.clone()
+ .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
+ .build().toString();
+
+ navigateTo(linkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.clickSocial(PARENT_IDP);
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+ loginPage.login(PARENT_USERNAME, "password");
+
+ // Test I was not automatically linked.
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ loginUpdateProfilePage.assertCurrent();
+ loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com");
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please logout first.", errorPage.getError());
+
+ logoutAll();
+
+ // Remove newly created user
+ String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId();
+ getCleanup("child").addUserId(newUserId);
+ }
+
+
+ @Test
+ public void testAccountLinkingExpired() throws Exception {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ // Login to account mgmt first
+ profilePage.open(CHILD_IDP);
+ WaitUtils.waitForPageToLoad(driver);
+
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ profilePage.assertCurrent();
+
+ // Now in another tab, request account linking
+ UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("link");
+ String linkUrl = linkBuilder.clone()
+ .queryParam("realm", CHILD_IDP)
+ .queryParam("provider", PARENT_IDP).build().toString();
+ navigateTo(linkUrl);
+
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+
+ // Logout "child" userSession in the meantime (for example through admin request)
+ realm.logoutAll();
+
+ // Finish login on parent.
+ loginPage.login(PARENT_USERNAME, "password");
+
+ // Test I was not automatically linked
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError());
+
+ logoutAll();
+ }
+
+ private void navigateTo(String uri) {
+ driver.navigate().to(uri);
+ WaitUtils.waitForPageToLoad(driver);
+ }
+
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java
new file mode 100644
index 0000000000..523015692d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 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.adapter.undertow.servlet;
+
+import org.junit.Test;
+import org.keycloak.testsuite.adapter.servlet.AbstractClientInitiatedAccountLinkTest;
+import org.keycloak.testsuite.adapter.servlet.AbstractLinkAndExchangeTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author Vlastislav Ramik
+ */
+@AppServerContainer("auth-server-undertow")
+public class UndertowLinkAndExchangeTest extends AbstractLinkAndExchangeTest {
+
+ //@Test
+ public void testUi() throws Exception {
+ Thread.sleep(1000000000);
+
+ }
+
+ @Override
+ @Test
+ public void testAccountLink() throws Exception {
+ super.testAccountLink();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
index 2889118efd..fc6b5aef2c 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
@@ -102,15 +102,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
illegal.setFullScopeAllowed(false);
- ClientModel illegalTo = realm.addClient("illegal-to");
- illegalTo.setClientId("illegal-to");
- illegalTo.setPublicClient(false);
- illegalTo.setDirectAccessGrantsEnabled(true);
- illegalTo.setEnabled(true);
- illegalTo.setSecret("secret");
- illegalTo.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
- illegalTo.setFullScopeAllowed(false);
-
ClientModel legal = realm.addClient("legal");
legal.setClientId("legal");
legal.setPublicClient(false);
@@ -131,15 +122,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy);
- management.clients().setPermissionsEnabled(clientExchanger, true);
- ClientPolicyRepresentation client2Rep = new ClientPolicyRepresentation();
- client2Rep.setName("from");
- client2Rep.addClient(legal.getId());
- client2Rep.addClient(illegalTo.getId());
- Policy client2Policy = management.authz().getStoreFactory().getPolicyStore().create(client2Rep, server);
- management.clients().exchangeFromPermission(clientExchanger).addAssociatedPolicy(client2Policy);
-
-
UserModel user = session.users().addUser(realm, "user");
user.setEnabled(true);
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
@@ -194,10 +176,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret");
Assert.assertEquals(403, response.getStatusCode());
}
- {
- response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal-to", "secret");
- Assert.assertEquals(403, response.getStatusCode());
- }
}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index f261105214..b857d94d06 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1341,7 +1341,6 @@ manage-authz-group-scope-description=Policies that decide if an admin can manage
view-authz-group-scope-description=Policies that decide if an admin can view this group
view-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group
exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
-exchange-from-authz-client-scope-description=Policies that decide which clients are allowed to exchange tokens that were generated for this client.
manage-authz-client-scope-description=Policies that decide if an admin can manage this client
configure-authz-client-scope-description=Reduced management permissions for admin. Cannot set scope, template, or protocol mappers.
view-authz-client-scope-description=Policies that decide if an admin can view this client