Merge pull request #4420 from patriot1burke/master
token exchange for idp
This commit is contained in:
commit
5484e088aa
19 changed files with 1571 additions and 135 deletions
|
@ -98,6 +98,7 @@ public interface OAuth2Constants {
|
||||||
String SUBJECT_TOKEN="subject_token";
|
String SUBJECT_TOKEN="subject_token";
|
||||||
String SUBJECT_TOKEN_TYPE="subject_token_type";
|
String SUBJECT_TOKEN_TYPE="subject_token_type";
|
||||||
String REQUESTED_TOKEN_TYPE="requested_token_type";
|
String REQUESTED_TOKEN_TYPE="requested_token_type";
|
||||||
|
String ISSUED_TOKEN_TYPE="issued_token_type";
|
||||||
String REQUESTED_ISSUER="requested_issuer";
|
String REQUESTED_ISSUER="requested_issuer";
|
||||||
String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
|
String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
|
||||||
String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
|
String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
|
||||||
|
|
|
@ -16,22 +16,34 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.broker.provider;
|
package org.keycloak.broker.provider;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Pedro Igor
|
* @author Pedro Igor
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> {
|
public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> {
|
||||||
|
|
||||||
|
public static final String ACCOUNT_LINK_URL = "account-link-url";
|
||||||
protected final KeycloakSession session;
|
protected final KeycloakSession session;
|
||||||
private final C config;
|
private final C config;
|
||||||
|
|
||||||
|
@ -74,6 +86,62 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Response exchangeNotSupported() {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("error", "invalid_target");
|
||||||
|
error.put("error_description", "target_exchange_unsupported");
|
||||||
|
return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response exchangeNotLinked(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
|
||||||
|
return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "invalid_target");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token, String reason) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("error", "invalid_target");
|
||||||
|
error.put("error_description", reason);
|
||||||
|
String accountLinkUrl = getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token);
|
||||||
|
if (accountLinkUrl != null) error.put(ACCOUNT_LINK_URL, accountLinkUrl);
|
||||||
|
return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getLinkingUrl(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token) {
|
||||||
|
if (authorizedClient.getClientId().equals(token.getIssuedFor())) {
|
||||||
|
String provider = getConfig().getAlias();
|
||||||
|
String clientId = authorizedClient.getClientId();
|
||||||
|
String nonce = UUID.randomUUID().toString();
|
||||||
|
MessageDigest md = null;
|
||||||
|
try {
|
||||||
|
md = MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
String input = nonce + tokenUserSession.getId() + clientId + provider;
|
||||||
|
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||||
|
String hash = Base64Url.encode(check);
|
||||||
|
return KeycloakUriBuilder.fromUri(uriInfo.getBaseUri())
|
||||||
|
.path("/realms/{realm}/broker/{provider}/link")
|
||||||
|
.queryParam("nonce", nonce)
|
||||||
|
.queryParam("hash", hash)
|
||||||
|
.queryParam("client_id", clientId)
|
||||||
|
.build(authorizedClient.getRealm().getName(), provider)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
|
||||||
|
return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "token_expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response exchangeUnsupportedRequiredType() {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("error", "invalid_target");
|
||||||
|
error.put("error_description", "response_token_type_unsupported");
|
||||||
|
return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.representations.AccessToken;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -38,5 +39,5 @@ public interface TokenExchangeTo {
|
||||||
* @param params form parameters received for requested exchange
|
* @param params form parameters received for requested exchange
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
Response exchangeTo(ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params);
|
Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,22 +24,32 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
|
||||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||||
|
import org.keycloak.broker.provider.TokenExchangeTo;
|
||||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
@ -51,7 +61,7 @@ import java.util.regex.Pattern;
|
||||||
/**
|
/**
|
||||||
* @author Pedro Igor
|
* @author Pedro Igor
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> {
|
public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements TokenExchangeTo {
|
||||||
protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
|
protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
|
||||||
|
|
||||||
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
|
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
|
||||||
|
@ -136,14 +146,76 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params) {
|
||||||
|
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
|
||||||
|
if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
|
||||||
|
return exchangeUnsupportedRequiredType();
|
||||||
|
}
|
||||||
|
if (!getConfig().isStoreToken()) {
|
||||||
|
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
|
||||||
|
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
|
||||||
|
return exchangeNotSupported();
|
||||||
|
}
|
||||||
|
return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
} else {
|
||||||
|
return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
|
||||||
|
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
|
||||||
|
if (model == null || model.getToken() == null) {
|
||||||
|
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
String accessToken = extractTokenFromResponse(model.getToken(), getAccessTokenResponseParameter());
|
||||||
|
if (accessToken == null) {
|
||||||
|
model.setToken(null);
|
||||||
|
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
|
tokenResponse.setToken(accessToken);
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
tokenResponse.setRefreshToken(null);
|
||||||
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
|
tokenResponse.getOtherClaims().clear();
|
||||||
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
|
||||||
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
|
||||||
|
String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
||||||
|
if (accessToken == null) {
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
|
tokenResponse.setToken(accessToken);
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
tokenResponse.setRefreshToken(null);
|
||||||
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
|
tokenResponse.getOtherClaims().clear();
|
||||||
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
|
||||||
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public BrokeredIdentityContext getFederatedIdentity(String response) {
|
public BrokeredIdentityContext getFederatedIdentity(String response) {
|
||||||
String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
|
String accessToken = extractTokenFromResponse(response, getAccessTokenResponseParameter());
|
||||||
|
|
||||||
if (accessToken == null) {
|
if (accessToken == null) {
|
||||||
throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
|
throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return doGetFederatedIdentity(accessToken);
|
BrokeredIdentityContext context = doGetFederatedIdentity(accessToken);
|
||||||
|
context.getContextData().put(FEDERATED_ACCESS_TOKEN, accessToken);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getAccessTokenResponseParameter() {
|
||||||
|
return OAUTH2_PARAMETER_ACCESS_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,6 +258,12 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
|
|
||||||
protected abstract String getDefaultScopes();
|
protected abstract String getDefaultScopes();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
||||||
|
String token = (String) context.getContextData().get(FEDERATED_ACCESS_TOKEN);
|
||||||
|
if (token != null) authSession.setUserSessionNote(FEDERATED_ACCESS_TOKEN, token);
|
||||||
|
}
|
||||||
|
|
||||||
protected class Endpoint {
|
protected class Endpoint {
|
||||||
protected AuthenticationCallback callback;
|
protected AuthenticationCallback callback;
|
||||||
protected RealmModel realm;
|
protected RealmModel realm;
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.broker.oidc;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
|
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
|
||||||
import org.keycloak.broker.oidc.util.JsonSimpleHttp;
|
import org.keycloak.broker.oidc.util.JsonSimpleHttp;
|
||||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||||
|
@ -32,9 +33,13 @@ import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
import org.keycloak.keys.loader.PublicKeyStorageManager;
|
import org.keycloak.keys.loader.PublicKeyStorageManager;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
@ -50,6 +55,7 @@ import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
@ -59,7 +65,7 @@ import java.security.PublicKey;
|
||||||
/**
|
/**
|
||||||
* @author Pedro Igor
|
* @author Pedro Igor
|
||||||
*/
|
*/
|
||||||
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> {
|
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> {
|
||||||
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
|
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
|
||||||
|
|
||||||
public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
|
public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
|
||||||
|
@ -68,6 +74,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
public static final String USER_INFO = "UserInfo";
|
public static final String USER_INFO = "UserInfo";
|
||||||
public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
|
public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
|
||||||
public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
|
public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
|
||||||
|
public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration";
|
||||||
|
|
||||||
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||||
super(session, config);
|
super(session, config);
|
||||||
|
@ -170,7 +177,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
* @param userSession
|
* @param userSession
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public String refreshToken(KeycloakSession session, UserSessionModel userSession) {
|
public String refreshTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
|
||||||
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
|
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
|
||||||
try {
|
try {
|
||||||
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
||||||
|
@ -187,7 +194,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
long exp = Long.parseLong(userSession.getNote(FEDERATED_TOKEN_EXPIRATION));
|
long exp = Long.parseLong(userSession.getNote(FEDERATED_TOKEN_EXPIRATION));
|
||||||
int currentTime = Time.currentTime();
|
int currentTime = Time.currentTime();
|
||||||
if (exp > 0 && currentTime > exp) {
|
if (exp > 0 && currentTime > exp) {
|
||||||
String response = refreshToken(session, userSession);
|
String response = refreshTokenForLogout(session, userSession);
|
||||||
AccessTokenResponse tokenResponse = null;
|
AccessTokenResponse tokenResponse = null;
|
||||||
try {
|
try {
|
||||||
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||||
|
@ -215,8 +222,108 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
|
|
||||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
|
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
|
||||||
|
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
|
||||||
|
if (model == null || model.getToken() == null) {
|
||||||
|
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(model.getToken(), AccessTokenResponse.class);
|
||||||
|
Long exp = (Long)tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
|
||||||
|
if (exp != null && (long)exp < Time.currentTime()) {
|
||||||
|
if (tokenResponse.getRefreshToken() == null) {
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
||||||
|
.param("refresh_token", tokenResponse.getRefreshToken())
|
||||||
|
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
||||||
|
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
||||||
|
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
|
||||||
|
if (response.contains("error")) {
|
||||||
|
model.setToken(null);
|
||||||
|
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||||
|
if (newResponse.getExpiresIn() > 0) {
|
||||||
|
long accessTokenExpiration = Time.currentTime() + newResponse.getExpiresIn();
|
||||||
|
newResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
|
||||||
|
response = JsonSerialization.writeValueAsString(newResponse);
|
||||||
|
}
|
||||||
|
String oldToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
||||||
|
if (oldToken != null && oldToken.equals(tokenResponse.getToken())) {
|
||||||
|
long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
|
||||||
|
tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
|
||||||
|
tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
|
||||||
|
tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
|
||||||
|
tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
|
||||||
|
|
||||||
|
}
|
||||||
|
model.setToken(response);
|
||||||
|
tokenResponse = newResponse;
|
||||||
|
} else if (exp != null) {
|
||||||
|
tokenResponse.setExpiresIn(exp - Time.currentTime());
|
||||||
|
}
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
tokenResponse.setRefreshToken(null);
|
||||||
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
|
tokenResponse.getOtherClaims().clear();
|
||||||
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
|
||||||
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
|
||||||
|
try {
|
||||||
|
long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
|
||||||
|
String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
|
||||||
|
String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
||||||
|
String idToken = tokenUserSession.getNote(FEDERATED_ID_TOKEN);
|
||||||
|
if (expiration == 0 || expiration > Time.currentTime()) {
|
||||||
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
|
tokenResponse.setExpiresIn(expiration);
|
||||||
|
tokenResponse.setToken(accessToken);
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
tokenResponse.setRefreshToken(null);
|
||||||
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
|
||||||
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
||||||
|
.param("refresh_token", refreshToken)
|
||||||
|
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
||||||
|
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
||||||
|
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
|
||||||
|
if (response.contains("error")) {
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||||
|
long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
|
||||||
|
tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
|
||||||
|
tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
|
||||||
|
tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
|
||||||
|
tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
|
||||||
|
newResponse.setIdToken(null);
|
||||||
|
newResponse.setRefreshToken(null);
|
||||||
|
newResponse.setRefreshExpiresIn(0);
|
||||||
|
newResponse.getOtherClaims().clear();
|
||||||
|
newResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
|
||||||
|
return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BrokeredIdentityContext getFederatedIdentity(String response) {
|
public BrokeredIdentityContext getFederatedIdentity(String response) {
|
||||||
AccessTokenResponse tokenResponse = null;
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
@ -235,6 +342,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
|
BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
|
||||||
|
|
||||||
if (getConfig().isStoreToken()) {
|
if (getConfig().isStoreToken()) {
|
||||||
|
String response1 = response;
|
||||||
|
if (tokenResponse.getExpiresIn() > 0) {
|
||||||
|
long accessTokenExpiration = Time.currentTime() + tokenResponse.getExpiresIn();
|
||||||
|
tokenResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
|
||||||
|
response1 = JsonSerialization.writeValueAsString(tokenResponse);
|
||||||
|
}
|
||||||
|
response = response1;
|
||||||
identity.setToken(response);
|
identity.setToken(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
|
import org.keycloak.broker.provider.TokenExchangeTo;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
@ -34,6 +36,7 @@ import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
@ -53,6 +56,7 @@ import org.keycloak.services.managers.ClientManager;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.resources.Cors;
|
import org.keycloak.services.resources.Cors;
|
||||||
|
import org.keycloak.services.resources.IdentityBrokerService;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
@ -65,6 +69,7 @@ import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -582,10 +587,32 @@ public class TokenEndpoint {
|
||||||
String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
|
String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
|
||||||
|
|
||||||
if (requestedIssuer == null) {
|
if (requestedIssuer == null) {
|
||||||
|
return exchangeClientToClient(authResult);
|
||||||
|
} else {
|
||||||
|
return exchangeToIdentityProvider(authResult, requestedIssuer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response exchangeToIdentityProvider(AuthenticationManager.AuthResult authResult, String requestedIssuer) {
|
||||||
|
IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
|
||||||
|
if (providerModel == null) {
|
||||||
|
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
|
||||||
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
return exchangeClientToClient(authResult);
|
IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
|
||||||
|
if (!(provider instanceof TokenExchangeTo)) {
|
||||||
|
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
|
||||||
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, providerModel)) {
|
||||||
|
logger.debug("Client not allowed to exchange for linked token");
|
||||||
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
||||||
|
}
|
||||||
|
Response response = ((TokenExchangeTo)provider).exchangeTo(uriInfo, client, authResult.getSession(), authResult.getUser(), authResult.getToken(), formParams);
|
||||||
|
return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response exchangeClientToClient(AuthenticationManager.AuthResult subject) {
|
public Response exchangeClientToClient(AuthenticationManager.AuthResult subject) {
|
||||||
|
@ -617,24 +644,6 @@ public class TokenEndpoint {
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean exchangeFromAllowed = false;
|
|
||||||
for (String aud : subject.getToken().getAudience()) {
|
|
||||||
ClientModel audClient = realm.getClientByClientId(aud);
|
|
||||||
if (audClient == null) continue;
|
|
||||||
if (audClient.equals(client)) {
|
|
||||||
exchangeFromAllowed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (AdminPermissions.management(session, realm).clients().canExchangeFrom(client, audClient)) {
|
|
||||||
exchangeFromAllowed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!exchangeFromAllowed) {
|
|
||||||
logger.debug("Client does not have exchange rights for audience of provided token");
|
|
||||||
event.error(Errors.NOT_ALLOWED);
|
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
|
||||||
}
|
|
||||||
if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
|
if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
|
||||||
logger.debug("Client does not have exchange rights for target audience");
|
logger.debug("Client does not have exchange rights for target audience");
|
||||||
event.error(Errors.NOT_ALLOWED);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
|
|
@ -19,12 +19,15 @@ package org.keycloak.services.resources.admin;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.NotFoundException;
|
import org.jboss.resteasy.spi.NotFoundException;
|
||||||
|
import org.keycloak.authorization.model.Resource;
|
||||||
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
import org.keycloak.broker.provider.IdentityProviderFactory;
|
import org.keycloak.broker.provider.IdentityProviderFactory;
|
||||||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
@ -43,8 +46,11 @@ import org.keycloak.representations.idm.ConfigPropertyRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ManagementPermissionReference;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
|
@ -402,5 +408,58 @@ public class IdentityProviderResource {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return object stating whether client Authorization permissions have been initialized or not and a reference
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Path("management/permissions")
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@NoCache
|
||||||
|
public ManagementPermissionReference getManagementPermissions() {
|
||||||
|
this.auth.realm().requireViewIdentityProviders();
|
||||||
|
|
||||||
|
AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
|
||||||
|
if (!permissions.idps().isPermissionsEnabled(identityProviderModel)) {
|
||||||
|
return new ManagementPermissionReference();
|
||||||
|
}
|
||||||
|
return toMgmtRef(identityProviderModel, permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ManagementPermissionReference toMgmtRef(IdentityProviderModel model, AdminPermissionManagement permissions) {
|
||||||
|
ManagementPermissionReference ref = new ManagementPermissionReference();
|
||||||
|
ref.setEnabled(true);
|
||||||
|
ref.setResource(permissions.idps().resource(model).getId());
|
||||||
|
ref.setScopePermissions(permissions.idps().getPermissions(model));
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return object stating whether client Authorization permissions have been initialized or not and a reference
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return initialized manage permissions reference
|
||||||
|
*/
|
||||||
|
@Path("management/permissions")
|
||||||
|
@PUT
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@NoCache
|
||||||
|
public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
|
||||||
|
this.auth.realm().requireManageIdentityProviders();
|
||||||
|
AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
|
||||||
|
permissions.idps().setPermissionsEnabled(identityProviderModel, ref.isEnabled());
|
||||||
|
if (ref.isEnabled()) {
|
||||||
|
return toMgmtRef(identityProviderModel, permissions);
|
||||||
|
} else {
|
||||||
|
return new ManagementPermissionReference();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ import org.keycloak.models.ClientModel;
|
||||||
public interface AdminPermissionManagement {
|
public interface AdminPermissionManagement {
|
||||||
public static final String MANAGE_SCOPE = "manage";
|
public static final String MANAGE_SCOPE = "manage";
|
||||||
public static final String VIEW_SCOPE = "view";
|
public static final String VIEW_SCOPE = "view";
|
||||||
public static final String EXCHANGE_FROM_SCOPE="exchange-from";
|
|
||||||
public static final String EXCHANGE_TO_SCOPE="exchange-to";
|
public static final String EXCHANGE_TO_SCOPE="exchange-to";
|
||||||
|
|
||||||
ClientModel getRealmManagementClient();
|
ClientModel getRealmManagementClient();
|
||||||
|
@ -38,6 +37,7 @@ public interface AdminPermissionManagement {
|
||||||
UserPermissionManagement users();
|
UserPermissionManagement users();
|
||||||
GroupPermissionManagement groups();
|
GroupPermissionManagement groups();
|
||||||
ClientPermissionManagement clients();
|
ClientPermissionManagement clients();
|
||||||
|
IdentityProviderPermissionManagement idps();
|
||||||
|
|
||||||
ResourceServer realmResourceServer();
|
ResourceServer realmResourceServer();
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,12 +41,8 @@ public interface ClientPermissionManagement {
|
||||||
|
|
||||||
Map<String, String> getPermissions(ClientModel client);
|
Map<String, String> getPermissions(ClientModel client);
|
||||||
|
|
||||||
boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from);
|
|
||||||
|
|
||||||
boolean canExchangeTo(ClientModel authorizedClient, ClientModel to);
|
boolean canExchangeTo(ClientModel authorizedClient, ClientModel to);
|
||||||
|
|
||||||
Policy exchangeFromPermission(ClientModel client);
|
|
||||||
|
|
||||||
Policy exchangeToPermission(ClientModel client);
|
Policy exchangeToPermission(ClientModel client);
|
||||||
|
|
||||||
Policy mapRolesPermission(ClientModel client);
|
Policy mapRolesPermission(ClientModel client);
|
||||||
|
|
|
@ -18,10 +18,8 @@ package org.keycloak.services.resources.admin.permissions;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
import org.keycloak.authorization.attribute.Attributes;
|
|
||||||
import org.keycloak.authorization.common.ClientModelIdentity;
|
import org.keycloak.authorization.common.ClientModelIdentity;
|
||||||
import org.keycloak.authorization.common.DefaultEvaluationContext;
|
import org.keycloak.authorization.common.DefaultEvaluationContext;
|
||||||
import org.keycloak.authorization.identity.Identity;
|
|
||||||
import org.keycloak.authorization.model.Policy;
|
import org.keycloak.authorization.model.Policy;
|
||||||
import org.keycloak.authorization.model.Resource;
|
import org.keycloak.authorization.model.Resource;
|
||||||
import org.keycloak.authorization.model.ResourceServer;
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
|
@ -32,8 +30,6 @@ import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientTemplateModel;
|
import org.keycloak.models.ClientTemplateModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.services.ForbiddenException;
|
import org.keycloak.services.ForbiddenException;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -44,7 +40,6 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_FROM_SCOPE;
|
|
||||||
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
|
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,7 +49,7 @@ import static org.keycloak.services.resources.admin.permissions.AdminPermissionM
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionManagement {
|
class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionManagement {
|
||||||
private static final Logger logger = Logger.getLogger(ClientPermissions.class);
|
private static final Logger logger = Logger.getLogger(ClientPermissions.class);
|
||||||
protected final KeycloakSession session;
|
protected final KeycloakSession session;
|
||||||
protected final RealmModel realm;
|
protected final RealmModel realm;
|
||||||
|
@ -95,11 +90,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
|
return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getExchangeFromPermissionName(ClientModel client) {
|
private void initialize(ClientModel client) {
|
||||||
return EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize(ClientModel client) {
|
|
||||||
ResourceServer server = root.findOrCreateResourceServer(client);
|
ResourceServer server = root.findOrCreateResourceServer(client);
|
||||||
Scope manageScope = manageScope(server);
|
Scope manageScope = manageScope(server);
|
||||||
if (manageScope == null) {
|
if (manageScope == null) {
|
||||||
|
@ -116,7 +107,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server);
|
Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server);
|
||||||
Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server);
|
Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server);
|
||||||
Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server);
|
Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server);
|
||||||
Scope exchangeFromScope = root.initializeScope(EXCHANGE_FROM_SCOPE, server);
|
|
||||||
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
|
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
|
||||||
|
|
||||||
String resourceName = getResourceName(client);
|
String resourceName = getResourceName(client);
|
||||||
|
@ -131,7 +121,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
scopeset.add(mapRoleScope);
|
scopeset.add(mapRoleScope);
|
||||||
scopeset.add(mapRoleClientScope);
|
scopeset.add(mapRoleClientScope);
|
||||||
scopeset.add(mapRoleCompositeScope);
|
scopeset.add(mapRoleCompositeScope);
|
||||||
scopeset.add(exchangeFromScope);
|
|
||||||
scopeset.add(exchangeToScope);
|
scopeset.add(exchangeToScope);
|
||||||
resource.updateScopes(scopeset);
|
resource.updateScopes(scopeset);
|
||||||
}
|
}
|
||||||
|
@ -170,11 +159,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
if (exchangeToPermission == null) {
|
if (exchangeToPermission == null) {
|
||||||
Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
|
Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
|
||||||
}
|
}
|
||||||
String exchangeFromPermissionName = getExchangeFromPermissionName(client);
|
|
||||||
Policy exchangeFromPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeFromPermissionName, server.getId());
|
|
||||||
if (exchangeFromPermission == null) {
|
|
||||||
Helper.addEmptyScopePermission(authz, server, exchangeFromPermissionName, resource, exchangeFromScope);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deletePolicy(String name, ResourceServer server) {
|
private void deletePolicy(String name, ResourceServer server) {
|
||||||
|
@ -195,7 +179,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
deletePolicy(getMapRolesCompositePermissionName(client), server);
|
deletePolicy(getMapRolesCompositePermissionName(client), server);
|
||||||
deletePolicy(getConfigurePermissionName(client), server);
|
deletePolicy(getConfigurePermissionName(client), server);
|
||||||
deletePolicy(getExchangeToPermissionName(client), server);
|
deletePolicy(getExchangeToPermissionName(client), server);
|
||||||
deletePolicy(getExchangeFromPermissionName(client), server);
|
|
||||||
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.getId());;
|
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.getId());;
|
||||||
if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
|
if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
|
||||||
}
|
}
|
||||||
|
@ -223,10 +206,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.MANAGE_SCOPE, server.getId());
|
return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.MANAGE_SCOPE, server.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Scope exchangeFromScope(ResourceServer server) {
|
|
||||||
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_FROM_SCOPE, server.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Scope exchangeToScope(ResourceServer server) {
|
private Scope exchangeToScope(ResourceServer server) {
|
||||||
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
|
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
|
||||||
}
|
}
|
||||||
|
@ -314,59 +293,10 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId());
|
scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId());
|
||||||
scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId());
|
scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId());
|
||||||
scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId());
|
scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId());
|
||||||
scopes.put(EXCHANGE_FROM_SCOPE, exchangeFromPermission(client).getId());
|
|
||||||
scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId());
|
scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId());
|
||||||
return scopes;
|
return scopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from) {
|
|
||||||
if (!authorizedClient.equals(from)) {
|
|
||||||
ResourceServer server = resourceServer(from);
|
|
||||||
if (server == null) {
|
|
||||||
logger.debug("No resource server set up for target client");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(from), server.getId());
|
|
||||||
if (resource == null) {
|
|
||||||
logger.debug("No resource object set up for target client");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(from), server.getId());
|
|
||||||
if (policy == null) {
|
|
||||||
logger.debug("No permission object set up for target client");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
|
|
||||||
// if no policies attached to permission then just do default behavior
|
|
||||||
if (associatedPolicies == null || associatedPolicies.isEmpty()) {
|
|
||||||
logger.debug("No policies set up for permission on target client");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Scope scope = exchangeFromScope(server);
|
|
||||||
if (scope == null) {
|
|
||||||
logger.debug(EXCHANGE_FROM_SCOPE + " not initialized");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
|
|
||||||
EvaluationContext context = new DefaultEvaluationContext(identity, session) {
|
|
||||||
@Override
|
|
||||||
public Map<String, Collection<String>> getBaseAttributes() {
|
|
||||||
Map<String, Collection<String>> attributes = super.getBaseAttributes();
|
|
||||||
attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
return root.evaluatePermission(resource, scope, server, context);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) {
|
public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) {
|
||||||
|
|
||||||
|
@ -601,13 +531,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
|
||||||
return root.evaluatePermission(resource, scope, server);
|
return root.evaluatePermission(resource, scope, server);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Policy exchangeFromPermission(ClientModel client) {
|
|
||||||
ResourceServer server = resourceServer(client);
|
|
||||||
if (server == null) return null;
|
|
||||||
return authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(client), server.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Policy exchangeToPermission(ClientModel client) {
|
public Policy exchangeToPermission(ClientModel client) {
|
||||||
ResourceServer server = resourceServer(client);
|
ResourceServer server = resourceServer(client);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.services.resources.admin.permissions;
|
||||||
|
|
||||||
|
import org.keycloak.authorization.model.Policy;
|
||||||
|
import org.keycloak.authorization.model.Resource;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public interface IdentityProviderPermissionManagement {
|
||||||
|
boolean isPermissionsEnabled(IdentityProviderModel idp);
|
||||||
|
|
||||||
|
void setPermissionsEnabled(IdentityProviderModel idp, boolean enable);
|
||||||
|
|
||||||
|
Resource resource(IdentityProviderModel idp);
|
||||||
|
|
||||||
|
Map<String, String> getPermissions(IdentityProviderModel idp);
|
||||||
|
|
||||||
|
boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to);
|
||||||
|
|
||||||
|
Policy exchangeToPermission(IdentityProviderModel idp);
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.services.resources.admin.permissions;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
|
import org.keycloak.authorization.common.ClientModelIdentity;
|
||||||
|
import org.keycloak.authorization.common.DefaultEvaluationContext;
|
||||||
|
import org.keycloak.authorization.model.Policy;
|
||||||
|
import org.keycloak.authorization.model.Resource;
|
||||||
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
|
import org.keycloak.authorization.model.Scope;
|
||||||
|
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages default policies for all users.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
class IdentityProviderPermissions implements IdentityProviderPermissionManagement {
|
||||||
|
private static final Logger logger = Logger.getLogger(IdentityProviderPermissions.class);
|
||||||
|
protected final KeycloakSession session;
|
||||||
|
protected final RealmModel realm;
|
||||||
|
protected final AuthorizationProvider authz;
|
||||||
|
protected final MgmtPermissions root;
|
||||||
|
|
||||||
|
public IdentityProviderPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) {
|
||||||
|
this.session = session;
|
||||||
|
this.realm = realm;
|
||||||
|
this.authz = authz;
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getResourceName(IdentityProviderModel idp) {
|
||||||
|
return "idp.resource." + idp.getInternalId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getExchangeToPermissionName(IdentityProviderModel idp) {
|
||||||
|
return EXCHANGE_TO_SCOPE + ".permission.idp." + idp.getInternalId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialize(IdentityProviderModel idp) {
|
||||||
|
ResourceServer server = root.initializeRealmResourceServer();
|
||||||
|
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
|
||||||
|
|
||||||
|
String resourceName = getResourceName(idp);
|
||||||
|
Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
|
||||||
|
if (resource == null) {
|
||||||
|
resource = authz.getStoreFactory().getResourceStore().create(resourceName, server, server.getClientId());
|
||||||
|
resource.setType("IdentityProvider");
|
||||||
|
Set<Scope> scopeset = new HashSet<>();
|
||||||
|
scopeset.add(exchangeToScope);
|
||||||
|
resource.updateScopes(scopeset);
|
||||||
|
}
|
||||||
|
String exchangeToPermissionName = getExchangeToPermissionName(idp);
|
||||||
|
Policy exchangeToPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeToPermissionName, server.getId());
|
||||||
|
if (exchangeToPermission == null) {
|
||||||
|
Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deletePolicy(String name, ResourceServer server) {
|
||||||
|
Policy policy = authz.getStoreFactory().getPolicyStore().findByName(name, server.getId());
|
||||||
|
if (policy != null) {
|
||||||
|
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deletePermissions(IdentityProviderModel idp) {
|
||||||
|
ResourceServer server = root.initializeRealmResourceServer();
|
||||||
|
if (server == null) return;
|
||||||
|
deletePolicy(getExchangeToPermissionName(idp), server);
|
||||||
|
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());;
|
||||||
|
if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPermissionsEnabled(IdentityProviderModel idp) {
|
||||||
|
ResourceServer server = root.initializeRealmResourceServer();
|
||||||
|
if (server == null) return false;
|
||||||
|
|
||||||
|
return authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId()) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPermissionsEnabled(IdentityProviderModel idp, boolean enable) {
|
||||||
|
if (enable) {
|
||||||
|
initialize(idp);
|
||||||
|
} else {
|
||||||
|
deletePermissions(idp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private Scope exchangeToScope(ResourceServer server) {
|
||||||
|
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Resource resource(IdentityProviderModel idp) {
|
||||||
|
ResourceServer server = root.initializeRealmResourceServer();
|
||||||
|
if (server == null) return null;
|
||||||
|
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());
|
||||||
|
if (resource == null) return null;
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getPermissions(IdentityProviderModel idp) {
|
||||||
|
initialize(idp);
|
||||||
|
Map<String, String> scopes = new LinkedHashMap<>();
|
||||||
|
scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(idp).getId());
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to) {
|
||||||
|
|
||||||
|
if (!authorizedClient.equals(to)) {
|
||||||
|
ResourceServer server = root.initializeRealmResourceServer();
|
||||||
|
if (server == null) {
|
||||||
|
logger.debug("No resource server set up for target idp");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(to), server.getId());
|
||||||
|
if (resource == null) {
|
||||||
|
logger.debug("No resource object set up for target idp");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(to), server.getId());
|
||||||
|
if (policy == null) {
|
||||||
|
logger.debug("No permission object set up for target idp");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
|
||||||
|
// if no policies attached to permission then just do default behavior
|
||||||
|
if (associatedPolicies == null || associatedPolicies.isEmpty()) {
|
||||||
|
logger.debug("No policies set up for permission on target idp");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scope scope = exchangeToScope(server);
|
||||||
|
if (scope == null) {
|
||||||
|
logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
|
||||||
|
EvaluationContext context = new DefaultEvaluationContext(identity, session) {
|
||||||
|
@Override
|
||||||
|
public Map<String, Collection<String>> getBaseAttributes() {
|
||||||
|
Map<String, Collection<String>> attributes = super.getBaseAttributes();
|
||||||
|
attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
return root.evaluatePermission(resource, scope, server, context);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Policy exchangeToPermission(IdentityProviderModel idp) {
|
||||||
|
ResourceServer server = root.initializeRealmResourceServer();
|
||||||
|
if (server == null) return null;
|
||||||
|
return authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(idp), server.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -67,6 +67,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
|
||||||
protected GroupPermissions groups;
|
protected GroupPermissions groups;
|
||||||
protected RealmPermissions realmPermissions;
|
protected RealmPermissions realmPermissions;
|
||||||
protected ClientPermissions clientPermissions;
|
protected ClientPermissions clientPermissions;
|
||||||
|
protected IdentityProviderPermissions idpPermissions;
|
||||||
|
|
||||||
|
|
||||||
MgmtPermissions(KeycloakSession session, RealmModel realm) {
|
MgmtPermissions(KeycloakSession session, RealmModel realm) {
|
||||||
|
@ -223,6 +224,13 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
|
||||||
return clientPermissions;
|
return clientPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IdentityProviderPermissions idps() {
|
||||||
|
if (idpPermissions != null) return idpPermissions;
|
||||||
|
idpPermissions = new IdentityProviderPermissions(session, realm, authz, this);
|
||||||
|
return idpPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupPermissions groups() {
|
public GroupPermissions groups() {
|
||||||
if (groups != null) return groups;
|
if (groups != null) return groups;
|
||||||
|
|
|
@ -17,18 +17,26 @@
|
||||||
package org.keycloak.social.twitter;
|
package org.keycloak.social.twitter;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
|
||||||
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
|
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
|
||||||
import org.keycloak.broker.provider.AbstractIdentityProvider;
|
import org.keycloak.broker.provider.AbstractIdentityProvider;
|
||||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||||
|
import org.keycloak.broker.provider.TokenExchangeTo;
|
||||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
@ -44,6 +52,7 @@ import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -52,7 +61,10 @@ import java.net.URI;
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2IdentityProviderConfig> implements
|
public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2IdentityProviderConfig> implements
|
||||||
SocialIdentityProvider<OAuth2IdentityProviderConfig> {
|
SocialIdentityProvider<OAuth2IdentityProviderConfig>, TokenExchangeTo {
|
||||||
|
|
||||||
|
String TWITTER_TOKEN_TYPE="twitter";
|
||||||
|
|
||||||
|
|
||||||
protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
|
protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
|
||||||
|
|
||||||
|
@ -90,6 +102,62 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token, MultivaluedMap<String, String> params) {
|
||||||
|
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
|
||||||
|
if (requestedType != null && !requestedType.equals(TWITTER_TOKEN_TYPE)) {
|
||||||
|
return exchangeUnsupportedRequiredType();
|
||||||
|
}
|
||||||
|
if (!getConfig().isStoreToken()) {
|
||||||
|
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
|
||||||
|
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
|
||||||
|
return exchangeNotSupported();
|
||||||
|
}
|
||||||
|
return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
} else {
|
||||||
|
return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
|
||||||
|
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
|
||||||
|
if (model == null || model.getToken() == null) {
|
||||||
|
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
String accessToken = model.getToken();
|
||||||
|
if (accessToken == null) {
|
||||||
|
model.setToken(null);
|
||||||
|
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
|
tokenResponse.setToken(accessToken);
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
tokenResponse.setRefreshToken(null);
|
||||||
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
|
tokenResponse.getOtherClaims().clear();
|
||||||
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, TWITTER_TOKEN_TYPE);
|
||||||
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
|
||||||
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
|
||||||
|
String accessToken = tokenUserSession.getNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN);
|
||||||
|
if (accessToken == null) {
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
|
||||||
|
}
|
||||||
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
|
tokenResponse.setToken(accessToken);
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
tokenResponse.setRefreshToken(null);
|
||||||
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
|
tokenResponse.getOtherClaims().clear();
|
||||||
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, TWITTER_TOKEN_TYPE);
|
||||||
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
|
||||||
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected class Endpoint {
|
protected class Endpoint {
|
||||||
protected RealmModel realm;
|
protected RealmModel realm;
|
||||||
protected AuthenticationCallback callback;
|
protected AuthenticationCallback callback;
|
||||||
|
@ -142,6 +210,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
identity.setUsername(twitterUser.getScreenName());
|
identity.setUsername(twitterUser.getScreenName());
|
||||||
identity.setName(twitterUser.getName());
|
identity.setName(twitterUser.getName());
|
||||||
|
|
||||||
|
|
||||||
StringBuilder tokenBuilder = new StringBuilder();
|
StringBuilder tokenBuilder = new StringBuilder();
|
||||||
|
|
||||||
tokenBuilder.append("{");
|
tokenBuilder.append("{");
|
||||||
|
@ -150,8 +219,12 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
tokenBuilder.append("\"screen_name\":").append("\"").append(oAuthAccessToken.getScreenName()).append("\"").append(",");
|
tokenBuilder.append("\"screen_name\":").append("\"").append(oAuthAccessToken.getScreenName()).append("\"").append(",");
|
||||||
tokenBuilder.append("\"user_id\":").append("\"").append(oAuthAccessToken.getUserId()).append("\"");
|
tokenBuilder.append("\"user_id\":").append("\"").append(oAuthAccessToken.getUserId()).append("\"");
|
||||||
tokenBuilder.append("}");
|
tokenBuilder.append("}");
|
||||||
|
String token = tokenBuilder.toString();
|
||||||
|
if (getConfig().isStoreToken()) {
|
||||||
|
identity.setToken(token);
|
||||||
|
}
|
||||||
|
identity.getContextData().put(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, token);
|
||||||
|
|
||||||
identity.setToken(tokenBuilder.toString());
|
|
||||||
identity.setIdpConfig(getConfig());
|
identity.setIdpConfig(getConfig());
|
||||||
identity.setCode(state);
|
identity.setCode(state);
|
||||||
|
|
||||||
|
@ -178,4 +251,11 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
||||||
return Response.ok(identity.getToken()).type(MediaType.APPLICATION_JSON).build();
|
return Response.ok(identity.getToken()).type(MediaType.APPLICATION_JSON).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
|
||||||
|
authSession.setUserSessionNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, (String)context.getContextData().get(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.adapter.servlet;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.annotation.WebServlet;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
@WebServlet("/client-linking")
|
||||||
|
public class LinkAndExchangeServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private String getPostDataString(HashMap<String, String> params) throws UnsupportedEncodingException{
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
boolean first = true;
|
||||||
|
for(Map.Entry<String, String> entry : params.entrySet()){
|
||||||
|
if (first)
|
||||||
|
first = false;
|
||||||
|
else
|
||||||
|
result.append("&");
|
||||||
|
|
||||||
|
result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
|
||||||
|
result.append("=");
|
||||||
|
result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessTokenResponse doTokenExchange(String realm, String token, String requestedIssuer,
|
||||||
|
String clientId, String clientSecret) throws Exception {
|
||||||
|
try {
|
||||||
|
String exchangeUrl = KeycloakUriBuilder.fromUri(ServletTestUtils.getAuthServerUrlBase())
|
||||||
|
.path("/auth/realms/{realm}/protocol/openid-connect/token").build(realm).toString();
|
||||||
|
|
||||||
|
URL url = new URL(exchangeUrl);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setDoInput(true);
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
HashMap<String, String> parameters = new HashMap<>();
|
||||||
|
if (clientSecret != null) {
|
||||||
|
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||||
|
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, authorization);
|
||||||
|
} else {
|
||||||
|
parameters.put("client_id", clientId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE);
|
||||||
|
parameters.put(OAuth2Constants.SUBJECT_TOKEN, token);
|
||||||
|
parameters.put(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
parameters.put(OAuth2Constants.REQUESTED_ISSUER, requestedIssuer);
|
||||||
|
|
||||||
|
OutputStream os = conn.getOutputStream();
|
||||||
|
BufferedWriter writer = new BufferedWriter(
|
||||||
|
new OutputStreamWriter(os, "UTF-8"));
|
||||||
|
writer.write(getPostDataString(parameters));
|
||||||
|
|
||||||
|
writer.flush();
|
||||||
|
writer.close();
|
||||||
|
os.close();
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class);
|
||||||
|
conn.getInputStream().close();
|
||||||
|
return tokenResponse;
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
|
||||||
|
resp.setHeader("Cache-Control", "no-cache");
|
||||||
|
if (request.getRequestURI().endsWith("/link") && request.getParameter("response") == null) {
|
||||||
|
String provider = request.getParameter("provider");
|
||||||
|
String realm = request.getParameter("realm");
|
||||||
|
KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
AccessToken token = session.getToken();
|
||||||
|
String tokenString = session.getTokenString();
|
||||||
|
|
||||||
|
String clientId = token.getAudience()[0];
|
||||||
|
String linkUrl = null;
|
||||||
|
try {
|
||||||
|
AccessTokenResponse response = doTokenExchange(realm, tokenString, provider, clientId, "password");
|
||||||
|
String error = (String)response.getOtherClaims().get("error");
|
||||||
|
if (error != null) {
|
||||||
|
System.out.println("*** error : " + error);
|
||||||
|
System.out.println("*** link-url: " + response.getOtherClaims().get("account-link-url"));
|
||||||
|
linkUrl = (String)response.getOtherClaims().get("account-link-url");
|
||||||
|
} else {
|
||||||
|
Assert.assertNotNull(response.getToken());
|
||||||
|
resp.setStatus(200);
|
||||||
|
resp.setContentType("text/html");
|
||||||
|
PrintWriter pw = resp.getWriter();
|
||||||
|
pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
|
||||||
|
pw.println("Account Linked");
|
||||||
|
pw.print("</body></html>");
|
||||||
|
pw.flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
String redirectUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString())
|
||||||
|
.replaceQuery(null)
|
||||||
|
.queryParam("response", "true").build().toString();
|
||||||
|
String accountLinkUrl = KeycloakUriBuilder.fromUri(linkUrl)
|
||||||
|
.queryParam("redirect_uri", redirectUri).build().toString();
|
||||||
|
resp.setStatus(302);
|
||||||
|
resp.setHeader("Location", accountLinkUrl);
|
||||||
|
} else if (request.getRequestURI().endsWith("/link") && request.getParameter("response") != null) {
|
||||||
|
resp.setStatus(200);
|
||||||
|
resp.setContentType("text/html");
|
||||||
|
PrintWriter pw = resp.getWriter();
|
||||||
|
pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
|
||||||
|
String error = request.getParameter("link_error");
|
||||||
|
if (error != null) {
|
||||||
|
pw.println("Link error: " + error);
|
||||||
|
} else {
|
||||||
|
pw.println("Account Linked");
|
||||||
|
}
|
||||||
|
pw.print("</body></html>");
|
||||||
|
pw.flush();
|
||||||
|
} else {
|
||||||
|
resp.setStatus(200);
|
||||||
|
resp.setContentType("text/html");
|
||||||
|
PrintWriter pw = resp.getWriter();
|
||||||
|
pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
|
||||||
|
pw.println("Unknown request: " + request.getRequestURL().toString());
|
||||||
|
pw.print("</body></html>");
|
||||||
|
pw.flush();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,657 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.adapter.servlet;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.container.test.api.Deployment;
|
||||||
|
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
|
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.authorization.model.Policy;
|
||||||
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
|
import org.keycloak.testsuite.ActionURIUtils;
|
||||||
|
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||||
|
import org.keycloak.testsuite.broker.BrokerTestTools;
|
||||||
|
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
|
||||||
|
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
||||||
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.ClientBuilder;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
|
||||||
|
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
|
||||||
|
import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
|
||||||
|
import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapterTest {
|
||||||
|
public static final String CHILD_IDP = "child";
|
||||||
|
public static final String PARENT_IDP = "parent-idp";
|
||||||
|
public static final String PARENT_USERNAME = "parent";
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginUpdateProfilePage loginUpdateProfilePage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AccountUpdateProfilePage profilePage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
private LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
|
public static class ClientApp extends AbstractPageWithInjectedUrl {
|
||||||
|
|
||||||
|
public static final String DEPLOYMENT_NAME = "client-linking";
|
||||||
|
|
||||||
|
@ArquillianResource
|
||||||
|
@OperateOnDeployment(DEPLOYMENT_NAME)
|
||||||
|
private URL url;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getInjectedUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Page
|
||||||
|
private ClientApp appPage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeAuthTest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
RealmRepresentation realm = new RealmRepresentation();
|
||||||
|
realm.setRealm(CHILD_IDP);
|
||||||
|
realm.setEnabled(true);
|
||||||
|
ClientRepresentation servlet = new ClientRepresentation();
|
||||||
|
servlet.setClientId("client-linking");
|
||||||
|
servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
|
String uri = "/client-linking";
|
||||||
|
if (!isRelative()) {
|
||||||
|
uri = appServerContextRootPage.toString() + uri;
|
||||||
|
}
|
||||||
|
servlet.setAdminUrl(uri);
|
||||||
|
servlet.setDirectAccessGrantsEnabled(true);
|
||||||
|
servlet.setBaseUrl(uri);
|
||||||
|
servlet.setRedirectUris(new LinkedList<>());
|
||||||
|
servlet.getRedirectUris().add(uri + "/*");
|
||||||
|
servlet.setSecret("password");
|
||||||
|
servlet.setFullScopeAllowed(true);
|
||||||
|
realm.setClients(new LinkedList<>());
|
||||||
|
realm.getClients().add(servlet);
|
||||||
|
testRealms.add(realm);
|
||||||
|
|
||||||
|
|
||||||
|
realm = new RealmRepresentation();
|
||||||
|
realm.setRealm(PARENT_IDP);
|
||||||
|
realm.setEnabled(true);
|
||||||
|
|
||||||
|
testRealms.add(realm);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Deployment(name = ClientApp.DEPLOYMENT_NAME)
|
||||||
|
protected static WebArchive accountLink() {
|
||||||
|
return servletDeployment(ClientApp.DEPLOYMENT_NAME, LinkAndExchangeServlet.class, ServletTestUtils.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void addIdpUser() {
|
||||||
|
RealmResource realm = adminClient.realms().realm(PARENT_IDP);
|
||||||
|
UserRepresentation user = new UserRepresentation();
|
||||||
|
user.setUsername(PARENT_USERNAME);
|
||||||
|
user.setEnabled(true);
|
||||||
|
String userId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String childUserId = null;
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void addChildUser() {
|
||||||
|
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||||
|
UserRepresentation user = new UserRepresentation();
|
||||||
|
user.setUsername("child");
|
||||||
|
user.setEnabled(true);
|
||||||
|
childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
|
||||||
|
UserRepresentation user2 = new UserRepresentation();
|
||||||
|
user2.setUsername("child2");
|
||||||
|
user2.setEnabled(true);
|
||||||
|
String user2Id = createUserAndResetPasswordWithAdminClient(realm, user2, "password");
|
||||||
|
|
||||||
|
// have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions
|
||||||
|
realm.roles().create(new RoleRepresentation("user", null, false));
|
||||||
|
RoleRepresentation role = realm.roles().get("user").toRepresentation();
|
||||||
|
List<RoleRepresentation> roles = new LinkedList<>();
|
||||||
|
roles.add(role);
|
||||||
|
realm.users().get(childUserId).roles().realmLevel().add(roles);
|
||||||
|
realm.users().get(user2Id).roles().realmLevel().add(roles);
|
||||||
|
ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0);
|
||||||
|
role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation();
|
||||||
|
roles.clear();
|
||||||
|
roles.add(role);
|
||||||
|
realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles);
|
||||||
|
realm.users().get(user2Id).roles().clientLevel(brokerService.getId()).add(roles);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setupRealm(KeycloakSession session) {
|
||||||
|
RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
|
||||||
|
ClientModel client = realm.getClientByClientId("client-linking");
|
||||||
|
IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
|
||||||
|
Assert.assertNotNull(idp);
|
||||||
|
|
||||||
|
AdminPermissionManagement management = AdminPermissions.management(session, realm);
|
||||||
|
management.idps().setPermissionsEnabled(idp, true);
|
||||||
|
ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
|
||||||
|
clientRep.setName("toIdp");
|
||||||
|
clientRep.addClient(client.getId());
|
||||||
|
ResourceServer server = management.realmResourceServer();
|
||||||
|
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
|
||||||
|
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
|
||||||
|
|
||||||
|
}
|
||||||
|
@Before
|
||||||
|
public void createBroker() {
|
||||||
|
createParentChild();
|
||||||
|
testingClient.server().run(AbstractLinkAndExchangeTest::setupRealm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createParentChild() {
|
||||||
|
BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testErrorConditions() throws Exception {
|
||||||
|
|
||||||
|
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||||
|
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
ClientRepresentation client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
|
||||||
|
|
||||||
|
UriBuilder redirectUri = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||||
|
.path("link")
|
||||||
|
.queryParam("response", "true");
|
||||||
|
|
||||||
|
UriBuilder directLinking = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth")
|
||||||
|
.path("realms/child/broker/{provider}/link")
|
||||||
|
.queryParam("client_id", "client-linking")
|
||||||
|
.queryParam("redirect_uri", redirectUri.build())
|
||||||
|
.queryParam("hash", Base64Url.encode("crap".getBytes()))
|
||||||
|
.queryParam("nonce", UUID.randomUUID().toString());
|
||||||
|
|
||||||
|
String linkUrl = directLinking
|
||||||
|
.build(PARENT_IDP).toString();
|
||||||
|
|
||||||
|
// test not logged in
|
||||||
|
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_logged_in"));
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
// now log in
|
||||||
|
|
||||||
|
navigateTo( appPage.getInjectedUrl() + "/hello");
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
|
||||||
|
|
||||||
|
// now test CSRF with bad hash.
|
||||||
|
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("We're sorry..."));
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
// now log in again with client that does not have scope
|
||||||
|
|
||||||
|
String accountId = adminClient.realms().realm(CHILD_IDP).clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0).getId();
|
||||||
|
RoleRepresentation manageAccount = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT).toRepresentation();
|
||||||
|
RoleRepresentation manageLinks = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT_LINKS).toRepresentation();
|
||||||
|
RoleRepresentation userRole = adminClient.realms().realm(CHILD_IDP).roles().get("user").toRepresentation();
|
||||||
|
|
||||||
|
client.setFullScopeAllowed(false);
|
||||||
|
ClientResource clientResource = adminClient.realms().realm(CHILD_IDP).clients().get(client.getId());
|
||||||
|
clientResource.update(client);
|
||||||
|
|
||||||
|
List<RoleRepresentation> roles = new LinkedList<>();
|
||||||
|
roles.add(userRole);
|
||||||
|
clientResource.getScopeMappings().realmLevel().add(roles);
|
||||||
|
|
||||||
|
navigateTo( appPage.getInjectedUrl() + "/hello");
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
|
||||||
|
|
||||||
|
|
||||||
|
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||||
|
.path("link");
|
||||||
|
String clientLinkUrl = linkBuilder.clone()
|
||||||
|
.queryParam("realm", CHILD_IDP)
|
||||||
|
.queryParam("provider", PARENT_IDP).build().toString();
|
||||||
|
|
||||||
|
|
||||||
|
navigateTo(clientLinkUrl);
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().contains("error=not_allowed"));
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
// add MANAGE_ACCOUNT_LINKS scope should pass.
|
||||||
|
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
|
||||||
|
roles = new LinkedList<>();
|
||||||
|
roles.add(manageLinks);
|
||||||
|
clientResource.getScopeMappings().clientLevel(accountId).add(roles);
|
||||||
|
|
||||||
|
navigateTo(clientLinkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||||
|
loginPage.login(PARENT_USERNAME, "password");
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||||
|
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertFalse(links.isEmpty());
|
||||||
|
|
||||||
|
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
navigateTo(clientLinkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
// add MANAGE_ACCOUNT scope should pass
|
||||||
|
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
|
||||||
|
roles = new LinkedList<>();
|
||||||
|
roles.add(manageAccount);
|
||||||
|
clientResource.getScopeMappings().clientLevel(accountId).add(roles);
|
||||||
|
|
||||||
|
navigateTo(clientLinkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||||
|
loginPage.login(PARENT_USERNAME, "password");
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||||
|
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertFalse(links.isEmpty());
|
||||||
|
|
||||||
|
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
navigateTo(clientLinkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
|
||||||
|
// undo fullScopeAllowed
|
||||||
|
|
||||||
|
client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
|
||||||
|
client.setFullScopeAllowed(true);
|
||||||
|
clientResource.update(client);
|
||||||
|
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAccountLink() throws Exception {
|
||||||
|
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||||
|
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||||
|
.path("link");
|
||||||
|
String linkUrl = linkBuilder.clone()
|
||||||
|
.queryParam("realm", CHILD_IDP)
|
||||||
|
.queryParam("provider", PARENT_IDP).build().toString();
|
||||||
|
System.out.println("linkUrl: " + linkUrl);
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||||
|
loginPage.login(PARENT_USERNAME, "password");
|
||||||
|
System.out.println("After linking: " + driver.getCurrentUrl());
|
||||||
|
System.out.println(driver.getPageSource());
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, "client-linking", "password");
|
||||||
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
|
Assert.assertNull(response.getError());
|
||||||
|
Client httpClient = ClientBuilder.newClient();
|
||||||
|
String firstToken = getToken(response, httpClient);
|
||||||
|
Assert.assertNotNull(firstToken);
|
||||||
|
|
||||||
|
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||||
|
String nextToken = getToken(response, httpClient);
|
||||||
|
Assert.assertNotNull(nextToken);
|
||||||
|
Assert.assertNotEquals(firstToken, nextToken);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertFalse(links.isEmpty());
|
||||||
|
|
||||||
|
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getToken(OAuthClient.AccessTokenResponse response, Client httpClient) throws Exception {
|
||||||
|
String idpToken = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
|
||||||
|
.path("realms")
|
||||||
|
.path("child/broker")
|
||||||
|
.path(PARENT_IDP)
|
||||||
|
.path("token")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", "Bearer " + response.getAccessToken())
|
||||||
|
.get(String.class);
|
||||||
|
AccessTokenResponse res = JsonSerialization.readValue(idpToken, AccessTokenResponse.class);
|
||||||
|
return res.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void logoutAll() {
|
||||||
|
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString();
|
||||||
|
navigateTo(logoutUri);
|
||||||
|
logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_IDP).toString();
|
||||||
|
navigateTo(logoutUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLinkOnlyProvider() throws Exception {
|
||||||
|
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||||
|
IdentityProviderRepresentation rep = realm.identityProviders().get(PARENT_IDP).toRepresentation();
|
||||||
|
rep.setLinkOnly(true);
|
||||||
|
realm.identityProviders().get(PARENT_IDP).update(rep);
|
||||||
|
try {
|
||||||
|
|
||||||
|
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||||
|
.path("link");
|
||||||
|
String linkUrl = linkBuilder.clone()
|
||||||
|
.queryParam("realm", CHILD_IDP)
|
||||||
|
.queryParam("provider", PARENT_IDP).build().toString();
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
|
||||||
|
// should not be on login page. This is what we are testing
|
||||||
|
Assert.assertFalse(driver.getPageSource().contains(PARENT_IDP));
|
||||||
|
|
||||||
|
// now test that we can still link.
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||||
|
loginPage.login(PARENT_USERNAME, "password");
|
||||||
|
System.out.println("After linking: " + driver.getCurrentUrl());
|
||||||
|
System.out.println(driver.getPageSource());
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||||
|
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertFalse(links.isEmpty());
|
||||||
|
|
||||||
|
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
System.out.println("testing link-only attack");
|
||||||
|
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
|
||||||
|
System.out.println("login page uri is: " + driver.getCurrentUrl());
|
||||||
|
|
||||||
|
// ok, now scrape the code from page
|
||||||
|
String pageSource = driver.getPageSource();
|
||||||
|
String action = ActionURIUtils.getActionURIFromPageSource(pageSource);
|
||||||
|
System.out.println("action uri: " + action);
|
||||||
|
|
||||||
|
Map<String, String> queryParams = ActionURIUtils.parseQueryParamsFromActionURI(action);
|
||||||
|
System.out.println("query params: " + queryParams);
|
||||||
|
|
||||||
|
// now try and use the code to login to remote link-only idp
|
||||||
|
|
||||||
|
String uri = "/auth/realms/child/broker/parent-idp/login";
|
||||||
|
|
||||||
|
uri = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot())
|
||||||
|
.path(uri)
|
||||||
|
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
||||||
|
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
||||||
|
.build().toString();
|
||||||
|
|
||||||
|
System.out.println("hack uri: " + uri);
|
||||||
|
|
||||||
|
navigateTo(uri);
|
||||||
|
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Could not send authentication request to identity provider."));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
rep.setLinkOnly(false);
|
||||||
|
realm.identityProviders().get(PARENT_IDP).update(rep);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAccountNotLinkedAutomatically() throws Exception {
|
||||||
|
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||||
|
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
// Login to account mgmt first
|
||||||
|
profilePage.open(CHILD_IDP);
|
||||||
|
WaitUtils.waitForPageToLoad(driver);
|
||||||
|
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
profilePage.assertCurrent();
|
||||||
|
|
||||||
|
// Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie
|
||||||
|
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||||
|
.path("nosuch");
|
||||||
|
String linkUrl = linkBuilder.clone()
|
||||||
|
.queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
|
||||||
|
.build().toString();
|
||||||
|
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.clickSocial(PARENT_IDP);
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||||
|
loginPage.login(PARENT_USERNAME, "password");
|
||||||
|
|
||||||
|
// Test I was not automatically linked.
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
loginUpdateProfilePage.assertCurrent();
|
||||||
|
loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com");
|
||||||
|
|
||||||
|
errorPage.assertCurrent();
|
||||||
|
Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please logout first.", errorPage.getError());
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
|
||||||
|
// Remove newly created user
|
||||||
|
String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId();
|
||||||
|
getCleanup("child").addUserId(newUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAccountLinkingExpired() throws Exception {
|
||||||
|
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||||
|
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
// Login to account mgmt first
|
||||||
|
profilePage.open(CHILD_IDP);
|
||||||
|
WaitUtils.waitForPageToLoad(driver);
|
||||||
|
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||||
|
loginPage.login("child", "password");
|
||||||
|
profilePage.assertCurrent();
|
||||||
|
|
||||||
|
// Now in another tab, request account linking
|
||||||
|
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||||
|
.path("link");
|
||||||
|
String linkUrl = linkBuilder.clone()
|
||||||
|
.queryParam("realm", CHILD_IDP)
|
||||||
|
.queryParam("provider", PARENT_IDP).build().toString();
|
||||||
|
navigateTo(linkUrl);
|
||||||
|
|
||||||
|
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||||
|
|
||||||
|
// Logout "child" userSession in the meantime (for example through admin request)
|
||||||
|
realm.logoutAll();
|
||||||
|
|
||||||
|
// Finish login on parent.
|
||||||
|
loginPage.login(PARENT_USERNAME, "password");
|
||||||
|
|
||||||
|
// Test I was not automatically linked
|
||||||
|
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||||
|
Assert.assertTrue(links.isEmpty());
|
||||||
|
|
||||||
|
errorPage.assertCurrent();
|
||||||
|
Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError());
|
||||||
|
|
||||||
|
logoutAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigateTo(String uri) {
|
||||||
|
driver.navigate().to(uri);
|
||||||
|
WaitUtils.waitForPageToLoad(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.adapter.undertow.servlet;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.testsuite.adapter.servlet.AbstractClientInitiatedAccountLinkTest;
|
||||||
|
import org.keycloak.testsuite.adapter.servlet.AbstractLinkAndExchangeTest;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
|
||||||
|
*/
|
||||||
|
@AppServerContainer("auth-server-undertow")
|
||||||
|
public class UndertowLinkAndExchangeTest extends AbstractLinkAndExchangeTest {
|
||||||
|
|
||||||
|
//@Test
|
||||||
|
public void testUi() throws Exception {
|
||||||
|
Thread.sleep(1000000000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
public void testAccountLink() throws Exception {
|
||||||
|
super.testAccountLink();
|
||||||
|
}
|
||||||
|
}
|
|
@ -102,15 +102,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
|
||||||
illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
illegal.setFullScopeAllowed(false);
|
illegal.setFullScopeAllowed(false);
|
||||||
|
|
||||||
ClientModel illegalTo = realm.addClient("illegal-to");
|
|
||||||
illegalTo.setClientId("illegal-to");
|
|
||||||
illegalTo.setPublicClient(false);
|
|
||||||
illegalTo.setDirectAccessGrantsEnabled(true);
|
|
||||||
illegalTo.setEnabled(true);
|
|
||||||
illegalTo.setSecret("secret");
|
|
||||||
illegalTo.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
|
||||||
illegalTo.setFullScopeAllowed(false);
|
|
||||||
|
|
||||||
ClientModel legal = realm.addClient("legal");
|
ClientModel legal = realm.addClient("legal");
|
||||||
legal.setClientId("legal");
|
legal.setClientId("legal");
|
||||||
legal.setPublicClient(false);
|
legal.setPublicClient(false);
|
||||||
|
@ -131,15 +122,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
|
||||||
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
|
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
|
||||||
management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy);
|
management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy);
|
||||||
|
|
||||||
management.clients().setPermissionsEnabled(clientExchanger, true);
|
|
||||||
ClientPolicyRepresentation client2Rep = new ClientPolicyRepresentation();
|
|
||||||
client2Rep.setName("from");
|
|
||||||
client2Rep.addClient(legal.getId());
|
|
||||||
client2Rep.addClient(illegalTo.getId());
|
|
||||||
Policy client2Policy = management.authz().getStoreFactory().getPolicyStore().create(client2Rep, server);
|
|
||||||
management.clients().exchangeFromPermission(clientExchanger).addAssociatedPolicy(client2Policy);
|
|
||||||
|
|
||||||
|
|
||||||
UserModel user = session.users().addUser(realm, "user");
|
UserModel user = session.users().addUser(realm, "user");
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
|
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
|
||||||
|
@ -194,10 +176,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
|
||||||
response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret");
|
response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret");
|
||||||
Assert.assertEquals(403, response.getStatusCode());
|
Assert.assertEquals(403, response.getStatusCode());
|
||||||
}
|
}
|
||||||
{
|
|
||||||
response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal-to", "secret");
|
|
||||||
Assert.assertEquals(403, response.getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1341,7 +1341,6 @@ manage-authz-group-scope-description=Policies that decide if an admin can manage
|
||||||
view-authz-group-scope-description=Policies that decide if an admin can view this group
|
view-authz-group-scope-description=Policies that decide if an admin can view this group
|
||||||
view-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group
|
view-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group
|
||||||
exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
|
exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
|
||||||
exchange-from-authz-client-scope-description=Policies that decide which clients are allowed to exchange tokens that were generated for this client.
|
|
||||||
manage-authz-client-scope-description=Policies that decide if an admin can manage this client
|
manage-authz-client-scope-description=Policies that decide if an admin can manage this client
|
||||||
configure-authz-client-scope-description=Reduced management permissions for admin. Cannot set scope, template, or protocol mappers.
|
configure-authz-client-scope-description=Reduced management permissions for admin. Cannot set scope, template, or protocol mappers.
|
||||||
view-authz-client-scope-description=Policies that decide if an admin can view this client
|
view-authz-client-scope-description=Policies that decide if an admin can view this client
|
||||||
|
|
Loading…
Reference in a new issue