keycloak-aplcache
Changes
server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java 61(+29 -32)
server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java 3(+1 -2)
services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java 2(+2 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java 6(+6 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java 41(+36 -5)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 36(+35 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java 39(+39 -0)
Details
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 20d6e73..244de98 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 cd9a421..701e2d8 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<C extends IdentityProviderModel>
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<String, String> 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;
- }
-
- public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
- return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "token_expired");
+ 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) {
+ 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 f97887a..0478051 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<String, String> params);
+ Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> 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 5e3a9b3..bfe9073 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 468dc4b..4e2ea95 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<C extends OAuth2IdentityPro
}
@Override
- public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params) {
+ public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> 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<C extends OAuth2IdentityPro
if (!getConfig().isStoreToken()) {
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
- return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
- return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
} else {
- return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
}
- protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
if (model == null || model.getToken() == null) {
- return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
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);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse tokenResponse = new AccessTokenResponse();
tokenResponse.setToken(accessToken);
@@ -182,14 +182,14 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
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));
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
- protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
if (accessToken == null) {
- return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse tokenResponse = new AccessTokenResponse();
tokenResponse.setToken(accessToken);
@@ -198,7 +198,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
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));
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index ca6a76f..1dbce4c 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -225,10 +225,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
- protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+ protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
if (model == null || model.getToken() == null) {
- return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
try {
String modelTokenString = model.getToken();
@@ -236,7 +236,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
Integer exp = (Integer)tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
if (exp != null && exp < Time.currentTime()) {
if (tokenResponse.getRefreshToken() == null) {
- return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
.param("refresh_token", tokenResponse.getRefreshToken())
@@ -247,7 +247,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
model.setToken(null);
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
- return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
if (newResponse.getExpiresIn() > 0) {
@@ -274,14 +274,14 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
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));
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
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) {
+ protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
try {
long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
@@ -295,7 +295,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
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));
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
@@ -305,7 +305,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
if (response.contains("error")) {
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
- return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
@@ -318,7 +318,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
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));
+ newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
} catch (IOException e) {
throw new RuntimeException(e);
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 2a2f80e..056932d 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
@@ -41,6 +41,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
@@ -56,6 +57,7 @@ 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.AdminAuth;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
@@ -122,7 +124,7 @@ public class TokenEndpoint {
}
@POST
- public Response build() {
+ public Response processGrantRequest() {
formParams = request.getDecodedFormParameters();
grantType = formParams.getFirst(OIDCLoginProtocol.GRANT_TYPE_PARAM);
@@ -133,15 +135,15 @@ public class TokenEndpoint {
switch (action) {
case AUTHORIZATION_CODE:
- return buildAuthorizationCodeAccessTokenResponse();
+ return codeToToken();
case REFRESH_TOKEN:
- return buildRefreshToken();
+ return refreshTokenGrant();
case PASSWORD:
- return buildResourceOwnerPasswordCredentialsGrant();
+ return resourceOwnerPasswordCredentialsGrant();
case CLIENT_CREDENTIALS:
- return buildClientCredentialsGrant();
+ return clientCredentialsGrant();
case TOKEN_EXCHANGE:
- return buildTokenExchange();
+ return tokenExchange();
}
throw new RuntimeException("Unknown action " + action);
@@ -216,7 +218,7 @@ public class TokenEndpoint {
event.detail(Details.GRANT_TYPE, grantType);
}
- public Response buildAuthorizationCodeAccessTokenResponse() {
+ public Response codeToToken() {
String code = formParams.getFirst(OAuth2Constants.CODE);
if (code == null) {
event.error(Errors.INVALID_CODE);
@@ -370,7 +372,7 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
- public Response buildRefreshToken() {
+ public Response refreshTokenGrant() {
String refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN);
if (refreshToken == null) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
@@ -431,7 +433,7 @@ public class TokenEndpoint {
}
}
- public Response buildResourceOwnerPasswordCredentialsGrant() {
+ public Response resourceOwnerPasswordCredentialsGrant() {
event.detail(Details.AUTH_METHOD, "oauth_credentials");
if (!client.isDirectAccessGrantsEnabled()) {
@@ -495,7 +497,7 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
- public Response buildClientCredentialsGrant() {
+ public Response clientCredentialsGrant() {
if (client.isBearerOnly()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
@@ -564,34 +566,93 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
- public Response buildTokenExchange() {
+ public Response tokenExchange() {
event.detail(Details.AUTH_METHOD, "oauth_credentials");
+ event.client(client);
+
+ UserModel tokenUser = null;
+ UserSessionModel tokenSession = null;
+ AccessToken token = null;
String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
- String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
- if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
- event.error(Errors.INVALID_TOKEN);
- throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
+ if (subjectToken != null) {
+ String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+ if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
+
+ }
+
+
+ AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
+ if (authResult == null) {
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
+ }
+ tokenUser = authResult.getUser();
+ tokenSession = authResult.getSession();
+ token = authResult.getToken();
}
+ String requestedSubject = formParams.getFirst(OAuth2Constants.REQUESTED_SUBJECT);
+ if (requestedSubject != null) {
+ event.detail(Details.REQUESTED_SUBJECT, requestedSubject);
+ UserModel requestedUser = session.users().getUserByUsername(requestedSubject, realm);
+ if (requestedUser == null) {
+ requestedUser = session.users().getUserById(requestedSubject, realm);
+ }
- AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
- if (authResult == null) {
- event.error(Errors.INVALID_TOKEN);
- throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
+ if (requestedUser == null) {
+ // We always returned access denied to avoid username fishing
+ logger.debug("Requested subject not found");
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+
+ }
+
+ if (token != null) {
+ event.detail(Details.IMPERSONATOR, tokenUser.getUsername());
+ // for this case, the user represented by the token, must have permission to impersonate.
+ AdminAuth auth = new AdminAuth(realm, token, tokenUser, client);
+ if (!AdminPermissions.evaluator(session, realm, auth).users().canImpersonate(requestedUser)) {
+ logger.debug("Token user not allowed to exchange for requested subject");
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+ }
+
+ } else {
+ // no token is being exchanged, this is a direct exchange. Client must be authenticated, not public, and must be allowed
+ // to impersonate
+ if (client.isPublicClient()) {
+ logger.debug("Public clients cannot exchange tokens");
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+
+ }
+ if (!AdminPermissions.management(session, realm).users().canClientImpersonate(client, requestedUser)) {
+ logger.debug("Client not allowed to exchange for requested subject");
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+ }
+ }
+
+ String sessionId = KeycloakModelUtils.generateId();
+ tokenUser = requestedUser;
+ tokenSession = session.sessions().createUserSession(sessionId, realm, requestedUser, requestedUser.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
}
String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
if (requestedIssuer == null) {
- return exchangeClientToClient(authResult.getUser(), authResult.getSession());
+ return exchangeClientToClient(tokenUser, tokenSession);
} else {
- return exchangeToIdentityProvider(authResult, requestedIssuer);
+ return exchangeToIdentityProvider(tokenUser, tokenSession, requestedIssuer);
}
}
- public Response exchangeToIdentityProvider(AuthenticationManager.AuthResult authResult, String requestedIssuer) {
+ public Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) {
+ event.detail(Details.REQUESTED_ISSUER, requestedIssuer);
IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
if (providerModel == null) {
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
@@ -608,7 +669,7 @@ public class TokenEndpoint {
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
- Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, client, authResult.getSession(), authResult.getUser(), authResult.getToken(), formParams);
+ Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, client, targetUserSession, targetUser, formParams);
return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
@@ -622,27 +683,17 @@ public class TokenEndpoint {
throw new ErrorResponseException("unsupported_requested_token_type", "Unsupported requested token type", Response.Status.BAD_REQUEST);
}
+ ClientModel targetClient = client;
String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
- if (audience == null) {
- event.error(Errors.INVALID_REQUEST);
- throw new ErrorResponseException("invalid_audience", "Audience parameter required", Response.Status.BAD_REQUEST);
-
- }
- ClientModel targetClient = null;
if (audience != null) {
targetClient = realm.getClientByClientId(audience);
}
- if (targetClient == null) {
- event.error(Errors.INVALID_CLIENT);
- throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST);
- }
-
- if (targetClient.isConsentRequired()) {
+ if (targetClient.isConsentRequired()) {
event.error(Errors.CONSENT_DENIED);
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
}
- if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
+ if (!targetClient.equals(client) && !AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
logger.debug("Client does not have exchange rights for target audience");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
@@ -678,6 +729,7 @@ public class TokenEndpoint {
}
AccessTokenResponse res = responseBuilder.build();
+ event.detail(Details.AUDIENCE, targetClient.getClientId());
event.success();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java
index 40b1f8f..f0e2a86 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java
@@ -49,6 +49,8 @@ public interface UserPermissionEvaluator {
boolean canImpersonate(UserModel user);
+ boolean isImpersonatable(UserModel user);
+
boolean canImpersonate();
void requireImpersonate(UserModel user);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java
index d465378..0ac8431 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java
@@ -18,7 +18,9 @@ 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.RoleModel;
+import org.keycloak.models.UserModel;
import java.util.Map;
@@ -46,4 +48,8 @@ public interface UserPermissionManagement {
Policy adminImpersonatingPermission();
Policy userImpersonatedPermission();
+
+ boolean canClientImpersonate(ClientModel client, UserModel user);
+
+ boolean isImpersonatable(UserModel user);
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
index 0078497..55b7fb4 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
@@ -18,13 +18,17 @@ 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.common.UserModelIdentity;
import org.keycloak.authorization.identity.Identity;
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.AdminRoles;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.ImpersonationConstants;
import org.keycloak.models.KeycloakSession;
@@ -32,6 +36,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ForbiddenException;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -473,15 +479,33 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
}
@Override
+ public boolean canClientImpersonate(ClientModel client, UserModel user) {
+ ClientModelIdentity identity = new ClientModelIdentity(session, client);
+ EvaluationContext context = new DefaultEvaluationContext(identity, session) {
+ @Override
+ public Map<String, Collection<String>> getBaseAttributes() {
+ Map<String, Collection<String>> 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 a3312d8..2b104fe 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<OAuth2Iden
}
@Override
- public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token, MultivaluedMap<String, String> params) {
+ public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> 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<OAuth2Iden
if (!getConfig().isStoreToken()) {
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
- return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
- return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
} else {
- return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
}
- protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
+ protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
if (model == null || model.getToken() == null) {
- return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
String accessToken = model.getToken();
if (accessToken == null) {
model.setToken(null);
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
- return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse tokenResponse = new AccessTokenResponse();
tokenResponse.setToken(accessToken);
@@ -137,14 +137,14 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
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));
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
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) {
+ protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
String accessToken = tokenUserSession.getNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN);
if (accessToken == null) {
- return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse tokenResponse = new AccessTokenResponse();
tokenResponse.setToken(accessToken);
@@ -153,7 +153,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
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));
+ tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 07935bc..7a3a5b3 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -404,7 +404,7 @@ public class OAuthClient {
}
public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience,
- String clientId, String clientSecret) throws Exception {
+ String clientId, String clientSecret) throws Exception {
CloseableHttpClient client = newCloseableHttpClient();
try {
HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
@@ -447,6 +447,40 @@ public class OAuthClient {
}
}
+ public AccessTokenResponse doTokenExchange(String realm, String clientId, String clientSecret, Map<String, String> params) throws Exception {
+ CloseableHttpClient client = newCloseableHttpClient();
+ try {
+ HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
+
+ List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
+ for (Map.Entry<String, String> 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 a382f27..162e7fd 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 0000000..735d0db
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<RealmRepresentation> 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<AccessToken> 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<AccessToken> 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<AccessToken> 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<AccessToken> 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<AccessToken> 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<AccessToken> 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<AccessToken> 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<AccessToken> 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<AccessToken> 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();
+ }
+
+
+ }
+
+
+}