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/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 { + + private String getPostDataString(HashMap params) throws UnsupportedEncodingException{ + StringBuilder result = new StringBuilder(); + boolean first = true; + for(Map.Entry entry : params.entrySet()){ + if (first) + first = false; + else + result.append("&"); + + result.append(URLEncoder.encode(entry.getKey(), "UTF-8")); + result.append("="); + result.append(URLEncoder.encode(entry.getValue(), "UTF-8")); + } + + return result.toString(); + } + + public AccessTokenResponse doTokenExchange(String realm, String token, String requestedIssuer, + String clientId, String clientSecret) throws Exception { + try { + String exchangeUrl = KeycloakUriBuilder.fromUri(ServletTestUtils.getAuthServerUrlBase()) + .path("/auth/realms/{realm}/protocol/openid-connect/token").build(realm).toString(); + + URL url = new URL(exchangeUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoInput(true); + conn.setDoOutput(true); + HashMap parameters = new HashMap<>(); + if (clientSecret != null) { + String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); + conn.setRequestProperty(HttpHeaders.AUTHORIZATION, authorization); + } else { + parameters.put("client_id", clientId); + + } + + parameters.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE); + parameters.put(OAuth2Constants.SUBJECT_TOKEN, token); + parameters.put(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE); + parameters.put(OAuth2Constants.REQUESTED_ISSUER, requestedIssuer); + + OutputStream os = conn.getOutputStream(); + BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(os, "UTF-8")); + writer.write(getPostDataString(parameters)); + + writer.flush(); + writer.close(); + os.close(); + AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class); + conn.getInputStream().close(); + return tokenResponse; + } finally { + } + } + + @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