KEYCLOAK-5491 KEYCLOAK-5492

This commit is contained in:
Bill Burke 2017-09-15 16:30:45 -04:00
parent c999a0d8f9
commit f927ee7b4e
15 changed files with 735 additions and 287 deletions

View file

@ -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";

View file

@ -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;
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() {

View file

@ -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);
}

View file

@ -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";

View file

@ -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();
}

View file

@ -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);

View file

@ -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();

View file

@ -49,6 +49,8 @@ public interface UserPermissionEvaluator {
boolean canImpersonate(UserModel user);
boolean isImpersonatable(UserModel user);
boolean canImpersonate();
void requireImpersonate(UserModel user);

View file

@ -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);
}

View file

@ -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;
@ -472,16 +478,34 @@ 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

View file

@ -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();
}

View file

@ -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();

View file

@ -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();

View file

@ -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();
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<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");
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<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());
}
}
}