KEYCLOAK-5491 KEYCLOAK-5492
This commit is contained in:
parent
c999a0d8f9
commit
f927ee7b4e
15 changed files with 735 additions and 287 deletions
|
@ -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";
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ public interface UserPermissionEvaluator {
|
|||
|
||||
boolean canImpersonate(UserModel user);
|
||||
|
||||
boolean isImpersonatable(UserModel user);
|
||||
|
||||
boolean canImpersonate();
|
||||
|
||||
void requireImpersonate(UserModel user);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue