diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 20d6e7388c..244de98c8b 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -95,6 +95,7 @@ public interface OAuth2Constants { String TOKEN_EXCHANGE_GRANT_TYPE="urn:ietf:params:oauth:grant-type:token-exchange"; String AUDIENCE="audience"; + String REQUESTED_SUBJECT="requested_subject"; String SUBJECT_TOKEN="subject_token"; String SUBJECT_TOKEN_TYPE="subject_token_type"; String REQUESTED_TOKEN_TYPE="requested_token_type"; 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 cd9a421088..701e2d82bf 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 @@ -93,50 +93,47 @@ public abstract class AbstractIdentityProvider 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, "identity provider is not linked"); + public Response exchangeNotLinked(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) { + return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, "identity provider is not linked"); } - public Response exchangeNotLinkedNoStore(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) { - return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "identity provider is not linked, can only link to current user session"); + public Response exchangeNotLinkedNoStore(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) { + return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, "identity provider is not linked, can only link to current user session"); } - protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token, String reason) { + protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, String reason) { Map error = new HashMap<>(); error.put("error", "invalid_target"); error.put("error_description", reason); - String accountLinkUrl = getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token); + String accountLinkUrl = getLinkingUrl(uriInfo, authorizedClient, tokenUserSession); 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(); + protected String getLinkingUrl(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession) { + 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); } - return null; + 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(); } - public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) { - return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "token_expired"); + public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) { + return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, "token_expired"); } public Response exchangeUnsupportedRequiredType() { diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java index f97887a34a..0478051e2e 100644 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java @@ -35,9 +35,8 @@ public interface ExchangeTokenToIdentityProviderToken { * @param authorizedClient client requesting exchange * @param tokenUserSession UserSessionModel of token exchanging from * @param tokenSubject UserModel of token exchanging from - * @param token access token representation of token exchanging from * @param params form parameters received for requested exchange * @return */ - Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap params); + Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap params); } diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java index 5e3a9b37e4..bfe9073cce 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Details.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java @@ -46,6 +46,9 @@ public interface Details { String NODE_HOST = "node_host"; String REASON = "reason"; String REVOKED_CLIENT = "revoked_client"; + String AUDIENCE = "audience"; + String REQUESTED_ISSUER = "requested_issuer"; + String REQUESTED_SUBJECT = "requested_subject"; String CLIENT_SESSION_STATE = "client_session_state"; String CLIENT_SESSION_HOST = "client_session_host"; String RESTART_AFTER_TIMEOUT = "restart_after_timeout"; 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 468dc4b8fd..4e2ea95446 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -148,7 +148,7 @@ public abstract class AbstractOAuth2IdentityProvider params) { + public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap params) { String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE); if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) { return exchangeUnsupportedRequiredType(); @@ -156,24 +156,24 @@ public abstract class AbstractOAuth2IdentityProvider 0) { @@ -274,14 +274,14 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0; @@ -318,7 +318,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider> getBaseAttributes() { + Map> attributes = super.getBaseAttributes(); + attributes.put("kc.client.id", Arrays.asList(client.getClientId())); + return attributes; + } + + }; + return canImpersonate(context) && isImpersonatable(user); + + } + @Override public boolean canImpersonate(UserModel user) { if (!canImpersonate()) { return false; } + return isImpersonatable(user); + } + + @Override + public boolean isImpersonatable(UserModel user) { Identity userIdentity = new UserModelIdentity(root.realm, user); - if (!root.isAdminSameRealm()) { - return true; - } ResourceServer server = root.realmResourceServer(); if (server == null) return true; @@ -502,17 +526,24 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme Scope scope = root.realmScope(USER_IMPERSONATED_SCOPE); return root.evaluatePermission(resource, scope, server, userIdentity); - } @Override public boolean canImpersonate() { if (root.hasOneAdminRole(ImpersonationConstants.IMPERSONATION_ROLE)) return true; + Identity identity = root.identity; + if (!root.isAdminSameRealm()) { return false; } + EvaluationContext context = new DefaultEvaluationContext(identity, session); + return canImpersonate(context); + } + + protected boolean canImpersonate(EvaluationContext context) { + ResourceServer server = root.realmResourceServer(); if (server == null) return false; @@ -531,7 +562,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme } Scope scope = root.realmScope(IMPERSONATE_SCOPE); - return root.evaluatePermission(resource, scope, server); + return root.evaluatePermission(resource, scope, server, context); } @Override 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 a3312d88e8..2b104feadc 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -103,7 +103,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider params) { + public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap params) { String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE); if (requestedType != null && !requestedType.equals(TWITTER_TOKEN_TYPE)) { return exchangeUnsupportedRequiredType(); @@ -111,24 +111,24 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider params) throws Exception { + CloseableHttpClient client = newCloseableHttpClient(); + try { + HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm)); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)); + for (Map.Entry entry : params.entrySet()) { + parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + + } + + 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); + + return new AccessTokenResponse(client.execute(post)); + } finally { + closeClient(client); + } + } + public JSONWebKeySet doCertsRequest(String realm) throws Exception { CloseableHttpClient client = new DefaultHttpClient(); 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 index a382f27db8..162e7fd985 100644 --- 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 @@ -45,6 +45,7 @@ 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.representations.idm.authorization.DecisionStrategy; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.testsuite.ActionURIUtils; @@ -209,15 +210,38 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP); Assert.assertNotNull(idp); + ClientModel directExchanger = realm.addClient("direct-exchanger"); + directExchanger.setClientId("direct-exchanger"); + directExchanger.setPublicClient(false); + directExchanger.setDirectAccessGrantsEnabled(true); + directExchanger.setEnabled(true); + directExchanger.setSecret("secret"); + directExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + directExchanger.setFullScopeAllowed(false); + + AdminPermissionManagement management = AdminPermissions.management(session, realm); management.idps().setPermissionsEnabled(idp, true); ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); clientRep.setName("toIdp"); clientRep.addClient(client.getId()); + clientRep.addClient(directExchanger.getId()); ResourceServer server = management.realmResourceServer(); Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server); management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy); + + // permission for user impersonation for a client + + ClientPolicyRepresentation clientImpersonateRep = new ClientPolicyRepresentation(); + clientImpersonateRep.setName("clientImpersonators"); + clientImpersonateRep.addClient(directExchanger.getId()); + server = management.realmResourceServer(); + Policy clientImpersonatePolicy = management.authz().getStoreFactory().getPolicyStore().create(clientImpersonateRep, server); + management.users().setPermissionsEnabled(true); + management.users().adminImpersonatingPermission().addAssociatedPolicy(clientImpersonatePolicy); + management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE); + } public static void turnOffTokenStore(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName(CHILD_IDP); @@ -323,6 +347,21 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte response.close(); Assert.assertNotEquals(externalToken, tokenResponse.getToken()); + // test direct exchange + response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-exchanger", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "child") + .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) + + )); + Assert.assertEquals(200, response.getStatus()); + tokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + Assert.assertNotEquals(externalToken, tokenResponse.getToken()); + logoutAll(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java new file mode 100755 index 0000000000..735d0db44e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java @@ -0,0 +1,466 @@ +/* + * 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.oauth; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.TokenVerifier; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ImpersonationConstants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; +import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.util.BasicAuthHelper; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; + +/** + * @author Stian Thorgersen + */ +public class ClientTokenExchangeTest extends AbstractKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(ClientTokenExchangeTest.class); + } + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation testRealmRep = new RealmRepresentation(); + testRealmRep.setId(TEST); + testRealmRep.setRealm(TEST); + testRealmRep.setEnabled(true); + testRealms.add(testRealmRep); + } + + public static void setupRealm(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName(TEST); + + RoleModel exampleRole = realm.addRole("example"); + + AdminPermissionManagement management = AdminPermissions.management(session, realm); + + ClientModel target = realm.addClient("target"); + target.setDirectAccessGrantsEnabled(true); + target.setEnabled(true); + target.setSecret("secret"); + target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + target.setFullScopeAllowed(false); + target.addScopeMapping(exampleRole); + + + RoleModel impersonateRole = management.getRealmManagementClient().getRole(ImpersonationConstants.IMPERSONATION_ROLE); + Assert.assertNotNull(impersonateRole); + + ClientModel clientExchanger = realm.addClient("client-exchanger"); + clientExchanger.setClientId("client-exchanger"); + clientExchanger.setPublicClient(false); + clientExchanger.setDirectAccessGrantsEnabled(true); + clientExchanger.setEnabled(true); + clientExchanger.setSecret("secret"); + clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + clientExchanger.setFullScopeAllowed(false); + clientExchanger.addScopeMapping(impersonateRole); + + + ClientModel directExchanger = realm.addClient("direct-exchanger"); + directExchanger.setClientId("direct-exchanger"); + directExchanger.setPublicClient(false); + directExchanger.setDirectAccessGrantsEnabled(true); + directExchanger.setEnabled(true); + directExchanger.setSecret("secret"); + directExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + directExchanger.setFullScopeAllowed(false); + + ClientModel illegal = realm.addClient("illegal"); + illegal.setClientId("illegal"); + illegal.setPublicClient(false); + illegal.setDirectAccessGrantsEnabled(true); + illegal.setEnabled(true); + illegal.setSecret("secret"); + illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + illegal.setFullScopeAllowed(false); + + ClientModel legal = realm.addClient("legal"); + legal.setClientId("legal"); + legal.setPublicClient(false); + legal.setDirectAccessGrantsEnabled(true); + legal.setEnabled(true); + legal.setSecret("secret"); + legal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + legal.setFullScopeAllowed(false); + + ClientModel directLegal = realm.addClient("direct-legal"); + directLegal.setClientId("direct-legal"); + directLegal.setPublicClient(false); + directLegal.setDirectAccessGrantsEnabled(true); + directLegal.setEnabled(true); + directLegal.setSecret("secret"); + directLegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + directLegal.setFullScopeAllowed(false); + + ClientModel directPublic = realm.addClient("direct-public"); + directPublic.setClientId("direct-public"); + directPublic.setPublicClient(true); + directPublic.setDirectAccessGrantsEnabled(true); + directPublic.setEnabled(true); + directPublic.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + directPublic.setFullScopeAllowed(false); + + ClientModel directNoSecret = realm.addClient("direct-no-secret"); + directNoSecret.setClientId("direct-no-secret"); + directNoSecret.setPublicClient(false); + directNoSecret.setDirectAccessGrantsEnabled(true); + directNoSecret.setEnabled(true); + directNoSecret.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + directNoSecret.setFullScopeAllowed(false); + + + // permission for client to client exchange to "target" client + management.clients().setPermissionsEnabled(target, true); + ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); + clientRep.setName("to"); + clientRep.addClient(clientExchanger.getId()); + clientRep.addClient(legal.getId()); + clientRep.addClient(directLegal.getId()); + ResourceServer server = management.realmResourceServer(); + Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server); + management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy); + + // permission for user impersonation for a client + + ClientPolicyRepresentation clientImpersonateRep = new ClientPolicyRepresentation(); + clientImpersonateRep.setName("clientImpersonators"); + clientImpersonateRep.addClient(directLegal.getId()); + clientImpersonateRep.addClient(directExchanger.getId()); + clientImpersonateRep.addClient(directPublic.getId()); + clientImpersonateRep.addClient(directNoSecret.getId()); + server = management.realmResourceServer(); + Policy clientImpersonatePolicy = management.authz().getStoreFactory().getPolicyStore().create(clientImpersonateRep, server); + management.users().setPermissionsEnabled(true); + management.users().adminImpersonatingPermission().addAssociatedPolicy(clientImpersonatePolicy); + management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE); + + UserModel user = session.users().addUser(realm, "user"); + user.setEnabled(true); + session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password")); + user.grantRole(exampleRole); + user.grantRole(impersonateRole); + + UserModel bad = session.users().addUser(realm, "bad-impersonator"); + bad.setEnabled(true); + session.userCredentialManager().updateCredential(realm, bad, UserCredentialModel.password("password")); + + UserModel impersonatedUser = session.users().addUser(realm, "impersonated-user"); + impersonatedUser.setEnabled(true); + session.userCredentialManager().updateCredential(realm, impersonatedUser, UserCredentialModel.password("password")); + impersonatedUser.grantRole(exampleRole); + + } + + @Override + protected boolean isImportAfterEachMethod() { + return true; + } + + + @Test + public void testExchange() throws Exception { + testingClient.server().run(ClientTokenExchangeTest::setupRealm); + + oauth.realm(TEST); + oauth.clientId("client-exchanger"); + + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password"); + String accessToken = response.getAccessToken(); + TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class); + AccessToken token = accessTokenVerifier.parse().getToken(); + Assert.assertEquals(token.getPreferredUsername(), "user"); + Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example")); + + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret"); + + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "legal", "secret"); + + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("legal", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret"); + Assert.assertEquals(403, response.getStatusCode()); + } + } + @Test + public void testImpersonation() throws Exception { + testingClient.server().run(ClientTokenExchangeTest::setupRealm); + + oauth.realm(TEST); + oauth.clientId("client-exchanger"); + + Client httpClient = ClientBuilder.newClient(); + + WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT) + .path("/realms") + .path(TEST) + .path("protocol/openid-connect/token"); + System.out.println("Exchange url: " + exchangeUrl.getUri().toString()); + + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret", "user", "password"); + String accessToken = tokenResponse.getAccessToken(); + TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class); + AccessToken token = accessTokenVerifier.parse().getToken(); + Assert.assertEquals(token.getPreferredUsername(), "user"); + Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example")); + + // client-exchanger can impersonate from token "user" to user "impersonated-user" + { + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + + )); + org.junit.Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + + String exchangedTokenString = accessTokenResponse.getToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor()); + Assert.assertEquals("client-exchanger", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user"); + Assert.assertNull(exchangedToken.getRealmAccess()); + } + + // client-exchanger can impersonate from token "user" to user "impersonated-user" and to "target" client + { + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + + )); + org.junit.Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + + String exchangedTokenString = accessTokenResponse.getToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + + + } + + @Test + public void testBadImpersonator() throws Exception { + testingClient.server().run(ClientTokenExchangeTest::setupRealm); + + oauth.realm(TEST); + oauth.clientId("client-exchanger"); + + Client httpClient = ClientBuilder.newClient(); + + WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT) + .path("/realms") + .path(TEST) + .path("protocol/openid-connect/token"); + System.out.println("Exchange url: " + exchangeUrl.getUri().toString()); + + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret", "bad-impersonator", "password"); + String accessToken = tokenResponse.getAccessToken(); + TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class); + AccessToken token = accessTokenVerifier.parse().getToken(); + Assert.assertEquals(token.getPreferredUsername(), "bad-impersonator"); + Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example")); + + // test that user does not have impersonator permission + { + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + + )); + org.junit.Assert.assertEquals(403, response.getStatus()); + response.close(); + } + + + } + + @Test + public void testDirectImpersonation() throws Exception { + testingClient.server().run(ClientTokenExchangeTest::setupRealm); + Client httpClient = ClientBuilder.newClient(); + + WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT) + .path("/realms") + .path(TEST) + .path("protocol/openid-connect/token"); + System.out.println("Exchange url: " + exchangeUrl.getUri().toString()); + + // direct-exchanger can impersonate from token "user" to user "impersonated-user" + { + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-exchanger", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + + )); + org.junit.Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + + String exchangedTokenString = accessTokenResponse.getToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("direct-exchanger", exchangedToken.getIssuedFor()); + Assert.assertEquals("direct-exchanger", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user"); + Assert.assertNull(exchangedToken.getRealmAccess()); + } + + // direct-legal can impersonate from token "user" to user "impersonated-user" and to "target" client + { + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + + )); + org.junit.Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + + String exchangedTokenString = accessTokenResponse.getToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("direct-legal", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + + // direct-public fails impersonation + { + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-public", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + + )); + org.junit.Assert.assertEquals(403, response.getStatus()); + response.close(); + } + + // direct-no-secret fails impersonation + { + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-no-secret", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + + )); + org.junit.Assert.assertTrue(response.getStatus() >= 400); + response.close(); + } + + + } + + +} 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 deleted file mode 100755 index fc6b5aef2c..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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.oauth; - -import org.jboss.arquillian.container.test.api.Deployment; -import org.jboss.shrinkwrap.api.spec.WebArchive; -import org.junit.Rule; -import org.junit.Test; -import org.keycloak.OAuth2Constants; -import org.keycloak.TokenVerifier; -import org.keycloak.authorization.model.Policy; -import org.keycloak.authorization.model.ResourceServer; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; -import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; -import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; -import org.keycloak.services.resources.admin.permissions.AdminPermissions; -import org.keycloak.testsuite.AbstractKeycloakTest; -import org.keycloak.testsuite.Assert; -import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.runonserver.RunOnServerDeployment; -import org.keycloak.testsuite.util.OAuthClient; - -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; - -/** - * @author Stian Thorgersen - */ -public class TokenExchangeTest extends AbstractKeycloakTest { - - @Rule - public AssertEvents events = new AssertEvents(this); - - @Deployment - public static WebArchive deploy() { - return RunOnServerDeployment.create(TokenExchangeTest.class); - } - - @Override - public void addTestRealms(List testRealms) { - RealmRepresentation testRealmRep = new RealmRepresentation(); - testRealmRep.setId(TEST); - testRealmRep.setRealm(TEST); - testRealmRep.setEnabled(true); - testRealms.add(testRealmRep); - } - - public static void setupRealm(KeycloakSession session) { - RealmModel realm = session.realms().getRealmByName(TEST); - - RoleModel exampleRole = realm.addRole("example"); - - ClientModel target = realm.addClient("target"); - target.setDirectAccessGrantsEnabled(true); - target.setEnabled(true); - target.setSecret("secret"); - target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - target.setFullScopeAllowed(false); - target.addScopeMapping(exampleRole); - - ClientModel clientExchanger = realm.addClient("client-exchanger"); - clientExchanger.setClientId("client-exchanger"); - clientExchanger.setPublicClient(false); - clientExchanger.setDirectAccessGrantsEnabled(true); - clientExchanger.setEnabled(true); - clientExchanger.setSecret("secret"); - clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - clientExchanger.setFullScopeAllowed(false); - - ClientModel illegal = realm.addClient("illegal"); - illegal.setClientId("illegal"); - illegal.setPublicClient(false); - illegal.setDirectAccessGrantsEnabled(true); - illegal.setEnabled(true); - illegal.setSecret("secret"); - illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - illegal.setFullScopeAllowed(false); - - ClientModel legal = realm.addClient("legal"); - legal.setClientId("legal"); - legal.setPublicClient(false); - legal.setDirectAccessGrantsEnabled(true); - legal.setEnabled(true); - legal.setSecret("secret"); - legal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - legal.setFullScopeAllowed(false); - - AdminPermissionManagement management = AdminPermissions.management(session, realm); - - management.clients().setPermissionsEnabled(target, true); - ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); - clientRep.setName("to"); - clientRep.addClient(clientExchanger.getId()); - clientRep.addClient(legal.getId()); - ResourceServer server = management.realmResourceServer(); - Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server); - management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy); - - UserModel user = session.users().addUser(realm, "user"); - user.setEnabled(true); - session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password")); - user.grantRole(exampleRole); - - } - - @Override - protected boolean isImportAfterEachMethod() { - return true; - } - - - @Test - public void testExchange() throws Exception { - testingClient.server().run(TokenExchangeTest::setupRealm); - - oauth.realm(TEST); - oauth.clientId("client-exchanger"); - - OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password"); - String accessToken = response.getAccessToken(); - TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class); - AccessToken token = accessTokenVerifier.parse().getToken(); - Assert.assertEquals(token.getPreferredUsername(), "user"); - Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example")); - - { - response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret"); - - String exchangedTokenString = response.getAccessToken(); - TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); - AccessToken exchangedToken = verifier.parse().getToken(); - Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor()); - Assert.assertEquals("target", exchangedToken.getAudience()[0]); - Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); - Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); - } - - { - response = oauth.doTokenExchange(TEST, accessToken, "target", "legal", "secret"); - - String exchangedTokenString = response.getAccessToken(); - TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); - AccessToken exchangedToken = verifier.parse().getToken(); - Assert.assertEquals("legal", exchangedToken.getIssuedFor()); - Assert.assertEquals("target", exchangedToken.getAudience()[0]); - Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); - Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); - } - { - response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret"); - Assert.assertEquals(403, response.getStatusCode()); - } - - - } -}