KEYCLOAK-5490 (#4477)

This commit is contained in:
Bill Burke 2017-09-15 05:36:48 -04:00 committed by Stian Thorgersen
parent 6ec5264f20
commit 3e6adbc904
24 changed files with 435 additions and 418 deletions

View file

@ -94,7 +94,11 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
}
public Response exchangeNotLinked(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "invalid_target");
return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "identity provider is not linked");
}
public Response exchangeNotLinkedNoStore(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "identity provider is not linked, can only link to current user session");
}
protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token, String reason) {

View file

@ -29,7 +29,7 @@ import javax.ws.rs.core.UriInfo;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface TokenExchangeTo {
public interface ExchangeTokenToIdentityProviderToken {
/**
*
* @param authorizedClient client requesting exchange
@ -39,5 +39,5 @@ public interface TokenExchangeTo {
* @param params form parameters received for requested exchange
* @return
*/
Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params);
Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params);
}

View file

@ -24,7 +24,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.TokenExchangeTo;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
@ -62,7 +62,7 @@ import java.util.regex.Pattern;
/**
* @author Pedro Igor
*/
public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements TokenExchangeTo {
public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements ExchangeTokenToIdentityProviderToken {
protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
@ -148,7 +148,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
}
@Override
public Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params) {
public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, 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();
@ -156,7 +156,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
if (!getConfig().isStoreToken()) {
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
return exchangeNotSupported();
return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
}
return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
} else {
@ -317,7 +317,9 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
BrokeredIdentityContext federatedIdentity = getFederatedIdentity(response);
if (getConfig().isStoreToken()) {
federatedIdentity.setToken(response);
// make sure that token wasn't already set by getFederatedIdentity();
// want to be able to allow provider to set the token itself.
if (federatedIdentity.getToken() == null)federatedIdentity.setToken(response);
}
federatedIdentity.setIdpConfig(getConfig());

View file

@ -231,9 +231,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
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()) {
String modelTokenString = model.getToken();
AccessTokenResponse tokenResponse = JsonSerialization.readValue(modelTokenString, AccessTokenResponse.class);
Integer exp = (Integer)tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
if (exp != null && exp < Time.currentTime()) {
if (tokenResponse.getRefreshToken() == null) {
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
}
@ -243,19 +244,20 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
if (response.contains("error")) {
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
model.setToken(null);
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
}
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
if (newResponse.getExpiresIn() > 0) {
long accessTokenExpiration = Time.currentTime() + newResponse.getExpiresIn();
int accessTokenExpiration = Time.currentTime() + (int)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;
int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + (int)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());
@ -302,6 +304,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
if (response.contains("error")) {
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
}
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
@ -341,13 +344,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
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 = JsonSerialization.writeValueAsString(tokenResponse);
}
response = response1;
identity.setToken(response);
}

View file

@ -24,7 +24,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.TokenExchangeTo;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Base64Url;
@ -39,7 +39,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -69,7 +68,6 @@ 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.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.util.Map;
import java.util.Objects;
@ -587,7 +585,7 @@ public class TokenEndpoint {
String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
if (requestedIssuer == null) {
return exchangeClientToClient(authResult);
return exchangeClientToClient(authResult.getUser(), authResult.getSession());
} else {
return exchangeToIdentityProvider(authResult, requestedIssuer);
}
@ -601,7 +599,7 @@ public class TokenEndpoint {
}
IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
if (!(provider instanceof TokenExchangeTo)) {
if (!(provider instanceof ExchangeTokenToIdentityProviderToken)) {
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
}
@ -610,12 +608,12 @@ public class TokenEndpoint {
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);
Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(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) {
protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel targetUserSession) {
String requestedTokenType = formParams.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
if (requestedTokenType == null) {
requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE;
@ -653,25 +651,19 @@ public class TokenEndpoint {
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false);
authSession.setAuthenticatedUser(subject.getUser());
authSession.setAuthenticatedUser(targetUser);
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
UserSessionModel userSession = subject.getSession();
event.session(userSession);
event.session(targetUserSession);
AuthenticationManager.setRolesAndMappersInSession(authSession);
AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession);
AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(this.session, targetUserSession, authSession);
// Notes about client details
userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
updateUserSessionFromClientAuth(targetUserSession);
updateUserSessionFromClientAuth(userSession);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, session, userSession, clientSession)
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, this.session, targetUserSession, clientSession)
.generateAccessToken();
responseBuilder.getAccessToken().issuedFor(client.getClientId());

View file

@ -27,7 +27,7 @@ import org.keycloak.models.ClientModel;
public interface AdminPermissionManagement {
public static final String MANAGE_SCOPE = "manage";
public static final String VIEW_SCOPE = "view";
public static final String EXCHANGE_TO_SCOPE="exchange-to";
public static final String TOKEN_EXCHANGE ="token-exchange";
ClientModel getRealmManagementClient();

View file

@ -40,7 +40,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.TOKEN_EXCHANGE;
/**
* Manages default policies for all users.
@ -87,7 +87,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
}
private String getExchangeToPermissionName(ClientModel client) {
return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
return TOKEN_EXCHANGE + ".permission.client." + client.getId();
}
private void initialize(ClientModel client) {
@ -107,7 +107,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server);
Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server);
Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server);
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
Scope exchangeToScope = root.initializeScope(TOKEN_EXCHANGE, server);
String resourceName = getResourceName(client);
Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
@ -207,7 +207,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
}
private Scope exchangeToScope(ResourceServer server) {
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
return authz.getStoreFactory().getScopeStore().findByName(TOKEN_EXCHANGE, server.getId());
}
private Scope configureScope(ResourceServer server) {
@ -293,7 +293,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId());
scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId());
scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId());
scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId());
scopes.put(TOKEN_EXCHANGE, exchangeToPermission(client).getId());
return scopes;
}
@ -328,7 +328,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM
Scope scope = exchangeToScope(server);
if (scope == null) {
logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
logger.debug(TOKEN_EXCHANGE + " not initialized");
return false;
}
ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);

View file

@ -37,10 +37,10 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.TOKEN_EXCHANGE;
/**
* Manages default policies for all users.
* Manages default policies for identity providers.
*
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -65,12 +65,12 @@ class IdentityProviderPermissions implements IdentityProviderPermissionManageme
}
private String getExchangeToPermissionName(IdentityProviderModel idp) {
return EXCHANGE_TO_SCOPE + ".permission.idp." + idp.getInternalId();
return TOKEN_EXCHANGE + ".permission.idp." + idp.getInternalId();
}
private void initialize(IdentityProviderModel idp) {
ResourceServer server = root.initializeRealmResourceServer();
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
Scope exchangeToScope = root.initializeScope(TOKEN_EXCHANGE, server);
String resourceName = getResourceName(idp);
Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
@ -124,7 +124,7 @@ class IdentityProviderPermissions implements IdentityProviderPermissionManageme
private Scope exchangeToScope(ResourceServer server) {
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
return authz.getStoreFactory().getScopeStore().findByName(TOKEN_EXCHANGE, server.getId());
}
@Override
@ -141,7 +141,7 @@ class IdentityProviderPermissions implements IdentityProviderPermissionManageme
public Map<String, String> getPermissions(IdentityProviderModel idp) {
initialize(idp);
Map<String, String> scopes = new LinkedHashMap<>();
scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(idp).getId());
scopes.put(TOKEN_EXCHANGE, exchangeToPermission(idp).getId());
return scopes;
}
@ -176,7 +176,7 @@ class IdentityProviderPermissions implements IdentityProviderPermissionManageme
Scope scope = exchangeToScope(server);
if (scope == null) {
logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
logger.debug(TOKEN_EXCHANGE + " not initialized");
return false;
}
ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);

View file

@ -24,7 +24,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.TokenExchangeTo;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
@ -61,7 +61,7 @@ import java.net.URI;
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2IdentityProviderConfig> implements
SocialIdentityProvider<OAuth2IdentityProviderConfig>, TokenExchangeTo {
SocialIdentityProvider<OAuth2IdentityProviderConfig>, ExchangeTokenToIdentityProviderToken {
String TWITTER_TOKEN_TYPE="twitter";
@ -103,7 +103,7 @@ 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) {
public Response exchangeFromToken(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();
@ -111,7 +111,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
if (!getConfig().isStoreToken()) {
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
return exchangeNotSupported();
return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
}
return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
} else {

View file

@ -47,7 +47,7 @@ import java.util.Map;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@WebServlet("/client-linking")
@WebServlet("/exchange-linking")
public class LinkAndExchangeServlet extends HttpServlet {
private String getPostDataString(HashMap<String, String> params) throws UnsupportedEncodingException{
@ -100,9 +100,18 @@ public class LinkAndExchangeServlet extends HttpServlet {
writer.flush();
writer.close();
os.close();
AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class);
conn.getInputStream().close();
return tokenResponse;
if (conn.getResponseCode() == 200) {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class);
conn.getInputStream().close();
return tokenResponse;
} else if (conn.getResponseCode() == 400) {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getErrorStream(), AccessTokenResponse.class);
conn.getErrorStream().close();
return tokenResponse;
} else {
throw new RuntimeException("Unknown error!");
}
} finally {
}
}
@ -143,7 +152,9 @@ public class LinkAndExchangeServlet extends HttpServlet {
String redirectUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString())
.replaceQuery(null)
.queryParam("response", "true").build().toString();
.queryParam("response", "true")
.queryParam("realm", realm)
.queryParam("provider", provider).build().toString();
String accountLinkUrl = KeycloakUriBuilder.fromUri(linkUrl)
.queryParam("redirect_uri", redirectUri).build().toString();
resp.setStatus(302);
@ -159,6 +170,24 @@ public class LinkAndExchangeServlet extends HttpServlet {
} else {
pw.println("Account Linked");
}
pw.println("trying exchange");
try {
String provider = request.getParameter("provider");
String realm = request.getParameter("realm");
KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
AccessToken token = session.getToken();
String clientId = token.getAudience()[0];
String tokenString = session.getTokenString();
AccessTokenResponse response = doTokenExchange(realm, tokenString, provider, clientId, "password");
error = (String)response.getOtherClaims().get("error");
if (error == null) {
if (response.getToken() != null) pw.println("Exchange token received");
} else {
pw.print("Error with exchange: " + error);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
pw.print("</body></html>");
pw.flush();
} else {

View file

@ -59,10 +59,16 @@ 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.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URL;
import java.util.LinkedList;
@ -98,7 +104,7 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
public static class ClientApp extends AbstractPageWithInjectedUrl {
public static final String DEPLOYMENT_NAME = "client-linking";
public static final String DEPLOYMENT_NAME = "exchange-linking";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
@ -124,9 +130,9 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
realm.setRealm(CHILD_IDP);
realm.setEnabled(true);
ClientRepresentation servlet = new ClientRepresentation();
servlet.setClientId("client-linking");
servlet.setClientId(ClientApp.DEPLOYMENT_NAME);
servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
String uri = "/client-linking";
String uri = "/" + ClientApp.DEPLOYMENT_NAME;
if (!isRelative()) {
uri = appServerContextRootPage.toString() + uri;
}
@ -199,7 +205,7 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
public static void setupRealm(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
ClientModel client = realm.getClientByClientId("client-linking");
ClientModel client = realm.getClientByClientId(ClientApp.DEPLOYMENT_NAME);
IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
Assert.assertNotNull(idp);
@ -213,6 +219,19 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
}
public static void turnOffTokenStore(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
idp.setStoreToken(false);
realm.updateIdentityProvider(idp);
}
public static void turnOnTokenStore(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
idp.setStoreToken(true);
realm.updateIdentityProvider(idp);
}
@Before
public void createBroker() {
createParentChild();
@ -225,186 +244,101 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
@Test
public void testErrorConditions() throws Exception {
public void testAccountLink() throws Exception {
testingClient.server().run(AbstractLinkAndExchangeTest::turnOnTokenStore);
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())
String servletUri = appPage.getInjectedUrl().toString();
UriBuilder linkBuilder = UriBuilder.fromUri(servletUri)
.path("link");
String clientLinkUrl = linkBuilder.clone()
String linkUrl = 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);
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"));
Assert.assertTrue(driver.getPageSource().contains("Exchange token received"));
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);
// do exchange
String accessToken = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, ClientApp.DEPLOYMENT_NAME, "password").getAccessToken();
Client httpClient = ClientBuilder.newClient();
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
.path(CHILD_IDP)
.path("protocol/openid-connect/token");
System.out.println("Exchange url: " + exchangeUrl.getUri().toString());
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP)
));
Assert.assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
response.close();
String externalToken = tokenResponse.getToken();
Assert.assertNotNull(externalToken);
Assert.assertTrue(tokenResponse.getExpiresIn() > 0);
setTimeOffset((int)tokenResponse.getExpiresIn() + 1);
// test that token refresh happens
// get access token again because we may have timed out
accessToken = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, ClientApp.DEPLOYMENT_NAME, "password").getAccessToken();
response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP)
));
Assert.assertEquals(200, response.getStatus());
tokenResponse = response.readEntity(AccessTokenResponse.class);
response.close();
Assert.assertNotEquals(externalToken, tokenResponse.getToken());
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 {
public void testAccountLinkNoTokenStore() throws Exception {
testingClient.server().run(AbstractLinkAndExchangeTest::turnOffTokenStore);
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
Assert.assertTrue(links.isEmpty());
@ -425,50 +359,24 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
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);
Assert.assertTrue(driver.getPageSource().contains("Exchange token received"));
links = realm.users().get(childUserId).getFederatedIdentity();
Assert.assertFalse(links.isEmpty());
logoutAll();
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();
@ -477,176 +385,6 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
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();
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();
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();

View file

@ -39,4 +39,10 @@ public class UndertowLinkAndExchangeTest extends AbstractLinkAndExchangeTest {
public void testAccountLink() throws Exception {
super.testAccountLink();
}
@Override
@Test
public void testAccountLinkNoTokenStore() throws Exception {
super.testAccountLinkNoTokenStore();
}
}

View file

@ -0,0 +1,20 @@
<!--
~ 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.
-->
<Context path="/customer-portal">
<Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
</Context>

View file

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<Get name="securityHandler">
<Set name="authenticator">
<New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
<!--
<Set name="adapterConfig">
<New class="org.keycloak.representations.adapters.config.AdapterConfig">
<Set name="realm">tomcat</Set>
<Set name="resource">customer-portal</Set>
<Set name="authServerUrl">http://localhost:8180/auth</Set>
<Set name="sslRequired">external</Set>
<Set name="credentials">
<Map>
<Entry>
<Item>secret</Item>
<Item>password</Item>
</Entry>
</Map>
</Set>
<Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
</New>
</Set>
-->
</New>
</Set>
</Get>
</Configure>

View file

@ -0,0 +1,10 @@
{
"realm" : "child",
"resource" : "exchange-linking",
"auth-server-url" : "http://localhost:8180/auth",
"ssl-required" : "external",
"min-time-between-jwks-requests" : 0,
"credentials" : {
"secret": "password"
}
}

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>exchange-linking</module-name>
<servlet>
<servlet-name>Servlet</servlet-name>
<servlet-class>org.keycloak.testsuite.adapter.servlet.LinkAndExchangeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>child</realm-name>
</login-config>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>

View file

@ -0,0 +1,38 @@
{
"id": "child",
"realm": "child",
"enabled": true,
"accessTokenLifespan": 600,
"accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 6000,
"sslRequired": "external",
"registrationAllowed": false,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"users" : [
{
"username" : "bburke@redhat.com",
"enabled": true,
"email" : "bburke@redhat.com",
"firstName": "Bill",
"lastName": "Burke",
"credentials" : [
{ "type" : "password",
"value" : "password" }
]
}
],
"clients": [
{
"clientId": "exchange-linking",
"enabled": true,
"adminUrl": "/exchange-linking",
"baseUrl": "/exchange-linking",
"redirectUris": [
"/exchange-linking/*"
],
"secret": "password"
}
]
}

View file

@ -1344,7 +1344,8 @@ manage-permissions-group.tooltip=Fine grain permssions for admins that want to m
manage-authz-group-scope-description=Policies that decide if an admin can manage 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
exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
token-exchange-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
token-exchange-authz-idp-scope-description=Policies that decide which clients are allowed exchange tokens for an external token minted by this identity provider.
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.
view-authz-client-scope-description=Policies that decide if an admin can view this client

View file

@ -470,6 +470,18 @@ module.config(['$routeProvider', function ($routeProvider) {
},
controller : 'GroupPermissionsCtrl'
})
.when('/realms/:realm/identity-provider-settings/provider/:provider_id/:alias/permissions', {
templateUrl : function(params){ return resourceUrl + '/partials/authz/mgmt/broker-permissions.html'; },
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
identityProvider : function(IdentityProviderLoader) {
return IdentityProviderLoader();
}
},
controller : 'IdentityProviderPermissionCtrl'
})
;
}]);

View file

@ -2544,7 +2544,6 @@ module.controller('RealmRolePermissionsCtrl', function($scope, $http, $route, $l
$scope.permissions = data;
$scope.$watch('permissions.enabled', function(newVal, oldVal) {
if (newVal != oldVal) {
console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
var param = {enabled: $scope.permissions.enabled};
$scope.permissions= RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
}
@ -2563,7 +2562,6 @@ module.controller('ClientRolePermissionsCtrl', function($scope, $http, $route, $
$scope.permissions = data;
$scope.$watch('permissions.enabled', function(newVal, oldVal) {
if (newVal != oldVal) {
console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
var param = {enabled: $scope.permissions.enabled};
$scope.permissions = RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
}
@ -2582,7 +2580,6 @@ module.controller('UsersPermissionsCtrl', function($scope, $http, $route, $locat
$scope.permissions = data;
$scope.$watch('permissions.enabled', function(newVal, oldVal) {
if (newVal != oldVal) {
console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
var param = {enabled: $scope.permissions.enabled};
$scope.permissions = UsersManagementPermissions.update({realm: realm.realm}, param);
@ -2605,7 +2602,6 @@ module.controller('ClientPermissionsCtrl', function($scope, $http, $route, $loca
$scope.permissions = data;
$scope.$watch('permissions.enabled', function(newVal, oldVal) {
if (newVal != oldVal) {
console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
var param = {enabled: $scope.permissions.enabled};
$scope.permissions = ClientManagementPermissions.update({realm: realm.realm, client: client.id}, param);
}
@ -2616,6 +2612,23 @@ module.controller('ClientPermissionsCtrl', function($scope, $http, $route, $loca
});
});
module.controller('IdentityProviderPermissionCtrl', function($scope, $http, $route, $location, realm, identityProvider, Client, IdentityProviderManagementPermissions, Notifications) {
$scope.identityProvider = identityProvider;
$scope.realm = realm;
IdentityProviderManagementPermissions.get({realm: realm.realm, alias: identityProvider.alias}, function(data) {
$scope.permissions = data;
$scope.$watch('permissions.enabled', function(newVal, oldVal) {
if (newVal != oldVal) {
var param = {enabled: $scope.permissions.enabled};
$scope.permissions = IdentityProviderManagementPermissions.update({realm: realm.realm, alias: identityProvider.alias}, param);
}
}, true);
});
Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) {
$scope.realmManagementClientId = data[0].id;
});
});
module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $location, realm, group, GroupManagementPermissions, Client, Notifications) {
$scope.group = group;
$scope.realm = realm;
@ -2626,7 +2639,6 @@ module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $locat
$scope.permissions = data;
$scope.$watch('permissions.enabled', function(newVal, oldVal) {
if (newVal != oldVal) {
console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
var param = {enabled: $scope.permissions.enabled};
$scope.permissions = GroupManagementPermissions.update({realm: realm.realm, group: group.id}, param);
}

View file

@ -178,6 +178,17 @@ module.factory('ClientManagementPermissions', function($resource) {
});
});
module.factory('IdentityProviderManagementPermissions', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/management/permissions', {
realm : '@realm',
alias : '@alias'
}, {
update: {
method: 'PUT'
}
});
});
module.factory('GroupManagementPermissions', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:group/management/permissions', {
realm : '@realm',

View file

@ -1001,7 +1001,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
} else {
IdentityProvider.update({
realm: $scope.realm.realm,
id: $scope.identityProvider.internalId
alias: $scope.identityProvider.alias
}, $scope.identityProvider, function () {
$route.reload();
Notifications.success("The " + $scope.identityProvider.alias + " provider has been updated.");

View file

@ -0,0 +1,40 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
<li data-ng-show="!newIdentityProvider && identityProvider.displayName">{{identityProvider.displayName}}</li>
<li data-ng-show="!newIdentityProvider && !identityProvider.displayName">{{identityProvider.alias}}</li>
</ol>
<kc-tabs-identity-provider></kc-tabs-identity-provider>
<form class=form-horizontal" name="enableForm" novalidate kc-read-only="!access.manageIdentityProviders || !access.manageAuthorization">
<fieldset class="border-top">
<div class="form-group">
<label class="col-md-2 control-label" for="permissionsEnabled">{{:: 'permissions-enabled-role' | translate}}</label>
<div class="col-md-6">
<input ng-model="permissions.enabled" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'permissions-enabled-role.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
</form>
<table class="datatable table table-striped table-bordered dataTable no-footer" data-ng-show="permissions.enabled">
<thead>
<tr>
<th>{{:: 'scope-name' | translate}}</th>
<th>{{:: 'description' | translate}}</th>
<th colspan="2">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(scopeName, scopeId) in permissions.scopePermissions">
<td><a href="#/realms/{{realm.realm}}/clients/{{realmManagementClientId}}/authz/resource-server/permission/scope/{{scopeId}}">{{scopeName}}</a></td>
<td translate="{{scopeName}}-authz-idp-scope-description"></td>
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/clients/{{realmManagementClientId}}/authz/resource-server/permission/scope/{{scopeId}}">{{:: 'edit' | translate}}</td>
</tr>
</tbody>
</table>
</div>
<kc-menu></kc-menu>

View file

@ -12,5 +12,6 @@
<li ng-class="{active: !path[6] && path.length > 5}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{:: 'settings' | translate}}</a></li>
<li ng-class="{active: path[4] == 'mappers'}"><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">{{:: 'mappers' | translate}}</a></li>
<li ng-class="{active: path[6] == 'export'}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">{{:: 'export' | translate}}</a></li>
<li ng-class="{active: path[4] == 'permissions'}" data-ng-show="!newIdentityProvider && access.manageAuthorization"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/permissions">{{:: 'authz-permissions' | translate}}</a></li>
</ul>
</div>