Merge pull request #4420 from patriot1burke/master

token exchange for idp
This commit is contained in:
Bill Burke 2017-08-24 20:26:18 -04:00 committed by GitHub
commit 5484e088aa
19 changed files with 1571 additions and 135 deletions

View file

@ -98,6 +98,7 @@ public interface OAuth2Constants {
String SUBJECT_TOKEN="subject_token";
String SUBJECT_TOKEN_TYPE="subject_token_type";
String REQUESTED_TOKEN_TYPE="requested_token_type";
String ISSUED_TOKEN_TYPE="issued_token_type";
String REQUESTED_ISSUER="requested_issuer";
String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";

View file

@ -16,22 +16,34 @@
*/
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.models.ClientModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
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
*/
public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> {
public static final String ACCOUNT_LINK_URL = "account-link-url";
protected final KeycloakSession session;
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
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {

View file

@ -23,6 +23,7 @@ import org.keycloak.representations.AccessToken;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/**
* @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
* @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);
}

View file

@ -24,22 +24,32 @@ 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.util.SimpleHttp;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
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.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
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;
@ -51,7 +61,7 @@ import java.util.regex.Pattern;
/**
* @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);
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
@ -136,14 +146,76 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
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) {
String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
String accessToken = extractTokenFromResponse(response, getAccessTokenResponseParameter());
if (accessToken == null) {
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();
@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 AuthenticationCallback callback;
protected RealmModel realm;

View file

@ -18,6 +18,7 @@ package org.keycloak.broker.oidc;
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.oidc.util.JsonSimpleHttp;
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.crypto.RSAProvider;
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.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
@ -50,6 +55,7 @@ import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
@ -59,7 +65,7 @@ import java.security.PublicKey;
/**
* @author Pedro Igor
*/
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> {
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> {
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
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 FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration";
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
super(session, config);
@ -170,7 +177,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
* @param userSession
* @return
*/
public String refreshToken(KeycloakSession session, UserSessionModel userSession) {
public String refreshTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
try {
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));
int currentTime = Time.currentTime();
if (exp > 0 && currentTime > exp) {
String response = refreshToken(session, userSession);
String response = refreshTokenForLogout(session, userSession);
AccessTokenResponse tokenResponse = null;
try {
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
@ -215,8 +222,108 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
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
public BrokeredIdentityContext getFederatedIdentity(String response) {
AccessTokenResponse tokenResponse = null;
@ -235,6 +342,13 @@ 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 = response1;
identity.setToken(response);
}

View file

@ -23,6 +23,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
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.common.ClientConnection;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Base64Url;
@ -34,6 +36,7 @@ import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
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;
@ -53,6 +56,7 @@ import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.sessions.AuthenticationSessionModel;
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.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;
@ -582,10 +587,32 @@ public class TokenEndpoint {
String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
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) {
@ -617,24 +644,6 @@ public class TokenEndpoint {
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)) {
logger.debug("Client does not have exchange rights for target audience");
event.error(Errors.NOT_ALLOWED);

View file

@ -19,12 +19,15 @@ package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
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.IdentityProviderFactory;
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderMapperModel;
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.IdentityProviderMapperTypeRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.services.ErrorResponse;
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.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();
}
}
}

View file

@ -27,7 +27,6 @@ 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_FROM_SCOPE="exchange-from";
public static final String EXCHANGE_TO_SCOPE="exchange-to";
ClientModel getRealmManagementClient();
@ -38,6 +37,7 @@ public interface AdminPermissionManagement {
UserPermissionManagement users();
GroupPermissionManagement groups();
ClientPermissionManagement clients();
IdentityProviderPermissionManagement idps();
ResourceServer realmResourceServer();
}

View file

@ -41,12 +41,8 @@ public interface ClientPermissionManagement {
Map<String, String> getPermissions(ClientModel client);
boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from);
boolean canExchangeTo(ClientModel authorizedClient, ClientModel to);
Policy exchangeFromPermission(ClientModel client);
Policy exchangeToPermission(ClientModel client);
Policy mapRolesPermission(ClientModel client);

View file

@ -18,10 +18,8 @@ package org.keycloak.services.resources.admin.permissions;
import org.jboss.logging.Logger;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.common.ClientModelIdentity;
import org.keycloak.authorization.common.DefaultEvaluationContext;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
@ -32,8 +30,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ForbiddenException;
import java.util.Arrays;
@ -44,7 +40,6 @@ import java.util.LinkedHashMap;
import java.util.Map;
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;
/**
@ -54,7 +49,7 @@ import static org.keycloak.services.resources.admin.permissions.AdminPermissionM
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionManagement {
class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionManagement {
private static final Logger logger = Logger.getLogger(ClientPermissions.class);
protected final KeycloakSession session;
protected final RealmModel realm;
@ -95,11 +90,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
}
private String getExchangeFromPermissionName(ClientModel client) {
return EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId();
}
private void initialize(ClientModel client) {
private void initialize(ClientModel client) {
ResourceServer server = root.findOrCreateResourceServer(client);
Scope manageScope = manageScope(server);
if (manageScope == null) {
@ -116,7 +107,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
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 exchangeFromScope = root.initializeScope(EXCHANGE_FROM_SCOPE, server);
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
String resourceName = getResourceName(client);
@ -131,7 +121,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
scopeset.add(mapRoleScope);
scopeset.add(mapRoleClientScope);
scopeset.add(mapRoleCompositeScope);
scopeset.add(exchangeFromScope);
scopeset.add(exchangeToScope);
resource.updateScopes(scopeset);
}
@ -170,11 +159,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
if (exchangeToPermission == null) {
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) {
@ -195,7 +179,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
deletePolicy(getMapRolesCompositePermissionName(client), server);
deletePolicy(getConfigurePermissionName(client), server);
deletePolicy(getExchangeToPermissionName(client), server);
deletePolicy(getExchangeFromPermissionName(client), server);
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.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());
}
private Scope exchangeFromScope(ResourceServer server) {
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_FROM_SCOPE, server.getId());
}
private Scope exchangeToScope(ResourceServer server) {
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_CLIENT_SCOPE, mapRolesClientScopePermission(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());
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
public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) {
@ -601,13 +531,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
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
public Policy exchangeToPermission(ClientModel client) {
ResourceServer server = resourceServer(client);

View file

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

View file

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

View file

@ -67,6 +67,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
protected GroupPermissions groups;
protected RealmPermissions realmPermissions;
protected ClientPermissions clientPermissions;
protected IdentityProviderPermissions idpPermissions;
MgmtPermissions(KeycloakSession session, RealmModel realm) {
@ -223,6 +224,13 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
return clientPermissions;
}
@Override
public IdentityProviderPermissions idps() {
if (idpPermissions != null) return idpPermissions;
idpPermissions = new IdentityProviderPermissions(session, realm, authz, this);
return idpPermissions;
}
@Override
public GroupPermissions groups() {
if (groups != null) return groups;

View file

@ -17,18 +17,26 @@
package org.keycloak.social.twitter;
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.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.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
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.managers.ClientSessionCode;
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.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
@ -52,7 +61,10 @@ import java.net.URI;
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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);
@ -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 RealmModel realm;
protected AuthenticationCallback callback;
@ -142,6 +210,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
identity.setUsername(twitterUser.getScreenName());
identity.setName(twitterUser.getName());
StringBuilder tokenBuilder = new StringBuilder();
tokenBuilder.append("{");
@ -150,8 +219,12 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
tokenBuilder.append("\"screen_name\":").append("\"").append(oAuthAccessToken.getScreenName()).append("\"").append(",");
tokenBuilder.append("\"user_id\":").append("\"").append(oAuthAccessToken.getUserId()).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.setCode(state);
@ -178,4 +251,11 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
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));
}
}

View file

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

View file

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

View file

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

View file

@ -102,15 +102,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
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");
legal.setClientId("legal");
legal.setPublicClient(false);
@ -131,15 +122,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
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");
user.setEnabled(true);
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");
Assert.assertEquals(403, response.getStatusCode());
}
{
response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal-to", "secret");
Assert.assertEquals(403, response.getStatusCode());
}
}

View file

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