Merge pull request #4420 from patriot1burke/master
token exchange for idp
This commit is contained in:
commit
5484e088aa
19 changed files with 1571 additions and 135 deletions
|
@ -98,6 +98,7 @@ public interface OAuth2Constants {
|
|||
String SUBJECT_TOKEN="subject_token";
|
||||
String SUBJECT_TOKEN_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";
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.services.resources.admin.permissions;
|
||||
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.model.Resource;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface IdentityProviderPermissionManagement {
|
||||
boolean isPermissionsEnabled(IdentityProviderModel idp);
|
||||
|
||||
void setPermissionsEnabled(IdentityProviderModel idp, boolean enable);
|
||||
|
||||
Resource resource(IdentityProviderModel idp);
|
||||
|
||||
Map<String, String> getPermissions(IdentityProviderModel idp);
|
||||
|
||||
boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to);
|
||||
|
||||
Policy exchangeToPermission(IdentityProviderModel idp);
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.services.resources.admin.permissions;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.common.ClientModelIdentity;
|
||||
import org.keycloak.authorization.common.DefaultEvaluationContext;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.model.Resource;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.authorization.model.Scope;
|
||||
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
|
||||
|
||||
/**
|
||||
* Manages default policies for all users.
|
||||
*
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
class IdentityProviderPermissions implements IdentityProviderPermissionManagement {
|
||||
private static final Logger logger = Logger.getLogger(IdentityProviderPermissions.class);
|
||||
protected final KeycloakSession session;
|
||||
protected final RealmModel realm;
|
||||
protected final AuthorizationProvider authz;
|
||||
protected final MgmtPermissions root;
|
||||
|
||||
public IdentityProviderPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.authz = authz;
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
private String getResourceName(IdentityProviderModel idp) {
|
||||
return "idp.resource." + idp.getInternalId();
|
||||
}
|
||||
|
||||
private String getExchangeToPermissionName(IdentityProviderModel idp) {
|
||||
return EXCHANGE_TO_SCOPE + ".permission.idp." + idp.getInternalId();
|
||||
}
|
||||
|
||||
private void initialize(IdentityProviderModel idp) {
|
||||
ResourceServer server = root.initializeRealmResourceServer();
|
||||
Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
|
||||
|
||||
String resourceName = getResourceName(idp);
|
||||
Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
|
||||
if (resource == null) {
|
||||
resource = authz.getStoreFactory().getResourceStore().create(resourceName, server, server.getClientId());
|
||||
resource.setType("IdentityProvider");
|
||||
Set<Scope> scopeset = new HashSet<>();
|
||||
scopeset.add(exchangeToScope);
|
||||
resource.updateScopes(scopeset);
|
||||
}
|
||||
String exchangeToPermissionName = getExchangeToPermissionName(idp);
|
||||
Policy exchangeToPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeToPermissionName, server.getId());
|
||||
if (exchangeToPermission == null) {
|
||||
Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
|
||||
}
|
||||
}
|
||||
|
||||
private void deletePolicy(String name, ResourceServer server) {
|
||||
Policy policy = authz.getStoreFactory().getPolicyStore().findByName(name, server.getId());
|
||||
if (policy != null) {
|
||||
authz.getStoreFactory().getPolicyStore().delete(policy.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void deletePermissions(IdentityProviderModel idp) {
|
||||
ResourceServer server = root.initializeRealmResourceServer();
|
||||
if (server == null) return;
|
||||
deletePolicy(getExchangeToPermissionName(idp), server);
|
||||
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());;
|
||||
if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPermissionsEnabled(IdentityProviderModel idp) {
|
||||
ResourceServer server = root.initializeRealmResourceServer();
|
||||
if (server == null) return false;
|
||||
|
||||
return authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId()) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPermissionsEnabled(IdentityProviderModel idp, boolean enable) {
|
||||
if (enable) {
|
||||
initialize(idp);
|
||||
} else {
|
||||
deletePermissions(idp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Scope exchangeToScope(ResourceServer server) {
|
||||
return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource resource(IdentityProviderModel idp) {
|
||||
ResourceServer server = root.initializeRealmResourceServer();
|
||||
if (server == null) return null;
|
||||
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());
|
||||
if (resource == null) return null;
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, String> getPermissions(IdentityProviderModel idp) {
|
||||
initialize(idp);
|
||||
Map<String, String> scopes = new LinkedHashMap<>();
|
||||
scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(idp).getId());
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to) {
|
||||
|
||||
if (!authorizedClient.equals(to)) {
|
||||
ResourceServer server = root.initializeRealmResourceServer();
|
||||
if (server == null) {
|
||||
logger.debug("No resource server set up for target idp");
|
||||
return false;
|
||||
}
|
||||
|
||||
Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(to), server.getId());
|
||||
if (resource == null) {
|
||||
logger.debug("No resource object set up for target idp");
|
||||
return false;
|
||||
}
|
||||
|
||||
Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(to), server.getId());
|
||||
if (policy == null) {
|
||||
logger.debug("No permission object set up for target idp");
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
|
||||
// if no policies attached to permission then just do default behavior
|
||||
if (associatedPolicies == null || associatedPolicies.isEmpty()) {
|
||||
logger.debug("No policies set up for permission on target idp");
|
||||
return false;
|
||||
}
|
||||
|
||||
Scope scope = exchangeToScope(server);
|
||||
if (scope == null) {
|
||||
logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
|
||||
return false;
|
||||
}
|
||||
ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
|
||||
EvaluationContext context = new DefaultEvaluationContext(identity, session) {
|
||||
@Override
|
||||
public Map<String, Collection<String>> getBaseAttributes() {
|
||||
Map<String, Collection<String>> attributes = super.getBaseAttributes();
|
||||
attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
|
||||
return attributes;
|
||||
}
|
||||
|
||||
};
|
||||
return root.evaluatePermission(resource, scope, server, context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Policy exchangeToPermission(IdentityProviderModel idp) {
|
||||
ResourceServer server = root.initializeRealmResourceServer();
|
||||
if (server == null) return null;
|
||||
return authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(idp), server.getId());
|
||||
}
|
||||
|
||||
}
|
|
@ -67,6 +67,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
|
|||
protected GroupPermissions groups;
|
||||
protected 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;
|
||||
|
|
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.adapter.servlet;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.annotation.WebServlet;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
@WebServlet("/client-linking")
|
||||
public class LinkAndExchangeServlet extends HttpServlet {
|
||||
|
||||
private String getPostDataString(HashMap<String, String> params) throws UnsupportedEncodingException{
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean first = true;
|
||||
for(Map.Entry<String, String> entry : params.entrySet()){
|
||||
if (first)
|
||||
first = false;
|
||||
else
|
||||
result.append("&");
|
||||
|
||||
result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
|
||||
result.append("=");
|
||||
result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public AccessTokenResponse doTokenExchange(String realm, String token, String requestedIssuer,
|
||||
String clientId, String clientSecret) throws Exception {
|
||||
try {
|
||||
String exchangeUrl = KeycloakUriBuilder.fromUri(ServletTestUtils.getAuthServerUrlBase())
|
||||
.path("/auth/realms/{realm}/protocol/openid-connect/token").build(realm).toString();
|
||||
|
||||
URL url = new URL(exchangeUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoInput(true);
|
||||
conn.setDoOutput(true);
|
||||
HashMap<String, String> parameters = new HashMap<>();
|
||||
if (clientSecret != null) {
|
||||
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, authorization);
|
||||
} else {
|
||||
parameters.put("client_id", clientId);
|
||||
|
||||
}
|
||||
|
||||
parameters.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE);
|
||||
parameters.put(OAuth2Constants.SUBJECT_TOKEN, token);
|
||||
parameters.put(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||
parameters.put(OAuth2Constants.REQUESTED_ISSUER, requestedIssuer);
|
||||
|
||||
OutputStream os = conn.getOutputStream();
|
||||
BufferedWriter writer = new BufferedWriter(
|
||||
new OutputStreamWriter(os, "UTF-8"));
|
||||
writer.write(getPostDataString(parameters));
|
||||
|
||||
writer.flush();
|
||||
writer.close();
|
||||
os.close();
|
||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class);
|
||||
conn.getInputStream().close();
|
||||
return tokenResponse;
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
|
||||
resp.setHeader("Cache-Control", "no-cache");
|
||||
if (request.getRequestURI().endsWith("/link") && request.getParameter("response") == null) {
|
||||
String provider = request.getParameter("provider");
|
||||
String realm = request.getParameter("realm");
|
||||
KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||
AccessToken token = session.getToken();
|
||||
String tokenString = session.getTokenString();
|
||||
|
||||
String clientId = token.getAudience()[0];
|
||||
String linkUrl = null;
|
||||
try {
|
||||
AccessTokenResponse response = doTokenExchange(realm, tokenString, provider, clientId, "password");
|
||||
String error = (String)response.getOtherClaims().get("error");
|
||||
if (error != null) {
|
||||
System.out.println("*** error : " + error);
|
||||
System.out.println("*** link-url: " + response.getOtherClaims().get("account-link-url"));
|
||||
linkUrl = (String)response.getOtherClaims().get("account-link-url");
|
||||
} else {
|
||||
Assert.assertNotNull(response.getToken());
|
||||
resp.setStatus(200);
|
||||
resp.setContentType("text/html");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
|
||||
pw.println("Account Linked");
|
||||
pw.print("</body></html>");
|
||||
pw.flush();
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String redirectUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString())
|
||||
.replaceQuery(null)
|
||||
.queryParam("response", "true").build().toString();
|
||||
String accountLinkUrl = KeycloakUriBuilder.fromUri(linkUrl)
|
||||
.queryParam("redirect_uri", redirectUri).build().toString();
|
||||
resp.setStatus(302);
|
||||
resp.setHeader("Location", accountLinkUrl);
|
||||
} else if (request.getRequestURI().endsWith("/link") && request.getParameter("response") != null) {
|
||||
resp.setStatus(200);
|
||||
resp.setContentType("text/html");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
|
||||
String error = request.getParameter("link_error");
|
||||
if (error != null) {
|
||||
pw.println("Link error: " + error);
|
||||
} else {
|
||||
pw.println("Account Linked");
|
||||
}
|
||||
pw.print("</body></html>");
|
||||
pw.flush();
|
||||
} else {
|
||||
resp.setStatus(200);
|
||||
resp.setContentType("text/html");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
|
||||
pw.println("Unknown request: " + request.getRequestURL().toString());
|
||||
pw.print("</body></html>");
|
||||
pw.flush();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,657 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.adapter.servlet;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
import org.keycloak.testsuite.ActionURIUtils;
|
||||
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||
import org.keycloak.testsuite.broker.BrokerTestTools;
|
||||
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
|
||||
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
|
||||
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
|
||||
import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapterTest {
|
||||
public static final String CHILD_IDP = "child";
|
||||
public static final String PARENT_IDP = "parent-idp";
|
||||
public static final String PARENT_USERNAME = "parent";
|
||||
|
||||
@Page
|
||||
protected LoginUpdateProfilePage loginUpdateProfilePage;
|
||||
|
||||
@Page
|
||||
protected AccountUpdateProfilePage profilePage;
|
||||
|
||||
@Page
|
||||
private LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
public static class ClientApp extends AbstractPageWithInjectedUrl {
|
||||
|
||||
public static final String DEPLOYMENT_NAME = "client-linking";
|
||||
|
||||
@ArquillianResource
|
||||
@OperateOnDeployment(DEPLOYMENT_NAME)
|
||||
private URL url;
|
||||
|
||||
@Override
|
||||
public URL getInjectedUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Page
|
||||
private ClientApp appPage;
|
||||
|
||||
@Override
|
||||
public void beforeAuthTest() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation realm = new RealmRepresentation();
|
||||
realm.setRealm(CHILD_IDP);
|
||||
realm.setEnabled(true);
|
||||
ClientRepresentation servlet = new ClientRepresentation();
|
||||
servlet.setClientId("client-linking");
|
||||
servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
String uri = "/client-linking";
|
||||
if (!isRelative()) {
|
||||
uri = appServerContextRootPage.toString() + uri;
|
||||
}
|
||||
servlet.setAdminUrl(uri);
|
||||
servlet.setDirectAccessGrantsEnabled(true);
|
||||
servlet.setBaseUrl(uri);
|
||||
servlet.setRedirectUris(new LinkedList<>());
|
||||
servlet.getRedirectUris().add(uri + "/*");
|
||||
servlet.setSecret("password");
|
||||
servlet.setFullScopeAllowed(true);
|
||||
realm.setClients(new LinkedList<>());
|
||||
realm.getClients().add(servlet);
|
||||
testRealms.add(realm);
|
||||
|
||||
|
||||
realm = new RealmRepresentation();
|
||||
realm.setRealm(PARENT_IDP);
|
||||
realm.setEnabled(true);
|
||||
|
||||
testRealms.add(realm);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Deployment(name = ClientApp.DEPLOYMENT_NAME)
|
||||
protected static WebArchive accountLink() {
|
||||
return servletDeployment(ClientApp.DEPLOYMENT_NAME, LinkAndExchangeServlet.class, ServletTestUtils.class);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void addIdpUser() {
|
||||
RealmResource realm = adminClient.realms().realm(PARENT_IDP);
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(PARENT_USERNAME);
|
||||
user.setEnabled(true);
|
||||
String userId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
|
||||
|
||||
}
|
||||
|
||||
private String childUserId = null;
|
||||
|
||||
|
||||
@Before
|
||||
public void addChildUser() {
|
||||
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("child");
|
||||
user.setEnabled(true);
|
||||
childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
|
||||
UserRepresentation user2 = new UserRepresentation();
|
||||
user2.setUsername("child2");
|
||||
user2.setEnabled(true);
|
||||
String user2Id = createUserAndResetPasswordWithAdminClient(realm, user2, "password");
|
||||
|
||||
// have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions
|
||||
realm.roles().create(new RoleRepresentation("user", null, false));
|
||||
RoleRepresentation role = realm.roles().get("user").toRepresentation();
|
||||
List<RoleRepresentation> roles = new LinkedList<>();
|
||||
roles.add(role);
|
||||
realm.users().get(childUserId).roles().realmLevel().add(roles);
|
||||
realm.users().get(user2Id).roles().realmLevel().add(roles);
|
||||
ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0);
|
||||
role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation();
|
||||
roles.clear();
|
||||
roles.add(role);
|
||||
realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles);
|
||||
realm.users().get(user2Id).roles().clientLevel(brokerService.getId()).add(roles);
|
||||
|
||||
}
|
||||
|
||||
public static void setupRealm(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
|
||||
ClientModel client = realm.getClientByClientId("client-linking");
|
||||
IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
|
||||
Assert.assertNotNull(idp);
|
||||
|
||||
AdminPermissionManagement management = AdminPermissions.management(session, realm);
|
||||
management.idps().setPermissionsEnabled(idp, true);
|
||||
ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
|
||||
clientRep.setName("toIdp");
|
||||
clientRep.addClient(client.getId());
|
||||
ResourceServer server = management.realmResourceServer();
|
||||
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
|
||||
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
|
||||
|
||||
}
|
||||
@Before
|
||||
public void createBroker() {
|
||||
createParentChild();
|
||||
testingClient.server().run(AbstractLinkAndExchangeTest::setupRealm);
|
||||
}
|
||||
|
||||
public void createParentChild() {
|
||||
BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testErrorConditions() throws Exception {
|
||||
|
||||
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
ClientRepresentation client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
|
||||
|
||||
UriBuilder redirectUri = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||
.path("link")
|
||||
.queryParam("response", "true");
|
||||
|
||||
UriBuilder directLinking = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth")
|
||||
.path("realms/child/broker/{provider}/link")
|
||||
.queryParam("client_id", "client-linking")
|
||||
.queryParam("redirect_uri", redirectUri.build())
|
||||
.queryParam("hash", Base64Url.encode("crap".getBytes()))
|
||||
.queryParam("nonce", UUID.randomUUID().toString());
|
||||
|
||||
String linkUrl = directLinking
|
||||
.build(PARENT_IDP).toString();
|
||||
|
||||
// test not logged in
|
||||
|
||||
navigateTo(linkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
|
||||
Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_logged_in"));
|
||||
|
||||
logoutAll();
|
||||
|
||||
// now log in
|
||||
|
||||
navigateTo( appPage.getInjectedUrl() + "/hello");
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
|
||||
Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
|
||||
|
||||
// now test CSRF with bad hash.
|
||||
|
||||
navigateTo(linkUrl);
|
||||
|
||||
Assert.assertTrue(driver.getPageSource().contains("We're sorry..."));
|
||||
|
||||
logoutAll();
|
||||
|
||||
// now log in again with client that does not have scope
|
||||
|
||||
String accountId = adminClient.realms().realm(CHILD_IDP).clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0).getId();
|
||||
RoleRepresentation manageAccount = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT).toRepresentation();
|
||||
RoleRepresentation manageLinks = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT_LINKS).toRepresentation();
|
||||
RoleRepresentation userRole = adminClient.realms().realm(CHILD_IDP).roles().get("user").toRepresentation();
|
||||
|
||||
client.setFullScopeAllowed(false);
|
||||
ClientResource clientResource = adminClient.realms().realm(CHILD_IDP).clients().get(client.getId());
|
||||
clientResource.update(client);
|
||||
|
||||
List<RoleRepresentation> roles = new LinkedList<>();
|
||||
roles.add(userRole);
|
||||
clientResource.getScopeMappings().realmLevel().add(roles);
|
||||
|
||||
navigateTo( appPage.getInjectedUrl() + "/hello");
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
|
||||
Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
|
||||
|
||||
|
||||
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||
.path("link");
|
||||
String clientLinkUrl = linkBuilder.clone()
|
||||
.queryParam("realm", CHILD_IDP)
|
||||
.queryParam("provider", PARENT_IDP).build().toString();
|
||||
|
||||
|
||||
navigateTo(clientLinkUrl);
|
||||
|
||||
Assert.assertTrue(driver.getCurrentUrl().contains("error=not_allowed"));
|
||||
|
||||
logoutAll();
|
||||
|
||||
// add MANAGE_ACCOUNT_LINKS scope should pass.
|
||||
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
|
||||
roles = new LinkedList<>();
|
||||
roles.add(manageLinks);
|
||||
clientResource.getScopeMappings().clientLevel(accountId).add(roles);
|
||||
|
||||
navigateTo(clientLinkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||
loginPage.login(PARENT_USERNAME, "password");
|
||||
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertFalse(links.isEmpty());
|
||||
|
||||
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
|
||||
|
||||
logoutAll();
|
||||
|
||||
navigateTo(clientLinkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
|
||||
Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
|
||||
|
||||
logoutAll();
|
||||
|
||||
// add MANAGE_ACCOUNT scope should pass
|
||||
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
|
||||
roles = new LinkedList<>();
|
||||
roles.add(manageAccount);
|
||||
clientResource.getScopeMappings().clientLevel(accountId).add(roles);
|
||||
|
||||
navigateTo(clientLinkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||
loginPage.login(PARENT_USERNAME, "password");
|
||||
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertFalse(links.isEmpty());
|
||||
|
||||
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
|
||||
|
||||
logoutAll();
|
||||
|
||||
navigateTo(clientLinkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
|
||||
Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
|
||||
|
||||
logoutAll();
|
||||
|
||||
|
||||
// undo fullScopeAllowed
|
||||
|
||||
client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
|
||||
client.setFullScopeAllowed(true);
|
||||
clientResource.update(client);
|
||||
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
logoutAll();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccountLink() throws Exception {
|
||||
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||
.path("link");
|
||||
String linkUrl = linkBuilder.clone()
|
||||
.queryParam("realm", CHILD_IDP)
|
||||
.queryParam("provider", PARENT_IDP).build().toString();
|
||||
System.out.println("linkUrl: " + linkUrl);
|
||||
navigateTo(linkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP));
|
||||
loginPage.login("child", "password");
|
||||
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||
loginPage.login(PARENT_USERNAME, "password");
|
||||
System.out.println("After linking: " + driver.getCurrentUrl());
|
||||
System.out.println(driver.getPageSource());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, "client-linking", "password");
|
||||
Assert.assertNotNull(response.getAccessToken());
|
||||
Assert.assertNull(response.getError());
|
||||
Client httpClient = ClientBuilder.newClient();
|
||||
String firstToken = getToken(response, httpClient);
|
||||
Assert.assertNotNull(firstToken);
|
||||
|
||||
|
||||
navigateTo(linkUrl);
|
||||
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||
String nextToken = getToken(response, httpClient);
|
||||
Assert.assertNotNull(nextToken);
|
||||
Assert.assertNotEquals(firstToken, nextToken);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertFalse(links.isEmpty());
|
||||
|
||||
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
logoutAll();
|
||||
|
||||
|
||||
}
|
||||
|
||||
private String getToken(OAuthClient.AccessTokenResponse response, Client httpClient) throws Exception {
|
||||
String idpToken = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
|
||||
.path("realms")
|
||||
.path("child/broker")
|
||||
.path(PARENT_IDP)
|
||||
.path("token")
|
||||
.request()
|
||||
.header("Authorization", "Bearer " + response.getAccessToken())
|
||||
.get(String.class);
|
||||
AccessTokenResponse res = JsonSerialization.readValue(idpToken, AccessTokenResponse.class);
|
||||
return res.getToken();
|
||||
}
|
||||
|
||||
public void logoutAll() {
|
||||
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString();
|
||||
navigateTo(logoutUri);
|
||||
logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_IDP).toString();
|
||||
navigateTo(logoutUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLinkOnlyProvider() throws Exception {
|
||||
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||
IdentityProviderRepresentation rep = realm.identityProviders().get(PARENT_IDP).toRepresentation();
|
||||
rep.setLinkOnly(true);
|
||||
realm.identityProviders().get(PARENT_IDP).update(rep);
|
||||
try {
|
||||
|
||||
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||
.path("link");
|
||||
String linkUrl = linkBuilder.clone()
|
||||
.queryParam("realm", CHILD_IDP)
|
||||
.queryParam("provider", PARENT_IDP).build().toString();
|
||||
navigateTo(linkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
|
||||
// should not be on login page. This is what we are testing
|
||||
Assert.assertFalse(driver.getPageSource().contains(PARENT_IDP));
|
||||
|
||||
// now test that we can still link.
|
||||
loginPage.login("child", "password");
|
||||
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||
loginPage.login(PARENT_USERNAME, "password");
|
||||
System.out.println("After linking: " + driver.getCurrentUrl());
|
||||
System.out.println(driver.getPageSource());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
|
||||
Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
|
||||
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertFalse(links.isEmpty());
|
||||
|
||||
realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
logoutAll();
|
||||
|
||||
System.out.println("testing link-only attack");
|
||||
|
||||
navigateTo(linkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
|
||||
System.out.println("login page uri is: " + driver.getCurrentUrl());
|
||||
|
||||
// ok, now scrape the code from page
|
||||
String pageSource = driver.getPageSource();
|
||||
String action = ActionURIUtils.getActionURIFromPageSource(pageSource);
|
||||
System.out.println("action uri: " + action);
|
||||
|
||||
Map<String, String> queryParams = ActionURIUtils.parseQueryParamsFromActionURI(action);
|
||||
System.out.println("query params: " + queryParams);
|
||||
|
||||
// now try and use the code to login to remote link-only idp
|
||||
|
||||
String uri = "/auth/realms/child/broker/parent-idp/login";
|
||||
|
||||
uri = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot())
|
||||
.path(uri)
|
||||
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
||||
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
||||
.build().toString();
|
||||
|
||||
System.out.println("hack uri: " + uri);
|
||||
|
||||
navigateTo(uri);
|
||||
|
||||
Assert.assertTrue(driver.getPageSource().contains("Could not send authentication request to identity provider."));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} finally {
|
||||
|
||||
rep.setLinkOnly(false);
|
||||
realm.identityProviders().get(PARENT_IDP).update(rep);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAccountNotLinkedAutomatically() throws Exception {
|
||||
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
// Login to account mgmt first
|
||||
profilePage.open(CHILD_IDP);
|
||||
WaitUtils.waitForPageToLoad(driver);
|
||||
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
profilePage.assertCurrent();
|
||||
|
||||
// Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie
|
||||
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||
.path("nosuch");
|
||||
String linkUrl = linkBuilder.clone()
|
||||
.queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
|
||||
.build().toString();
|
||||
|
||||
navigateTo(linkUrl);
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.clickSocial(PARENT_IDP);
|
||||
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||
loginPage.login(PARENT_USERNAME, "password");
|
||||
|
||||
// Test I was not automatically linked.
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
loginUpdateProfilePage.assertCurrent();
|
||||
loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com");
|
||||
|
||||
errorPage.assertCurrent();
|
||||
Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please logout first.", errorPage.getError());
|
||||
|
||||
logoutAll();
|
||||
|
||||
// Remove newly created user
|
||||
String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId();
|
||||
getCleanup("child").addUserId(newUserId);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAccountLinkingExpired() throws Exception {
|
||||
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
||||
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
// Login to account mgmt first
|
||||
profilePage.open(CHILD_IDP);
|
||||
WaitUtils.waitForPageToLoad(driver);
|
||||
|
||||
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
|
||||
loginPage.login("child", "password");
|
||||
profilePage.assertCurrent();
|
||||
|
||||
// Now in another tab, request account linking
|
||||
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
|
||||
.path("link");
|
||||
String linkUrl = linkBuilder.clone()
|
||||
.queryParam("realm", CHILD_IDP)
|
||||
.queryParam("provider", PARENT_IDP).build().toString();
|
||||
navigateTo(linkUrl);
|
||||
|
||||
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
|
||||
|
||||
// Logout "child" userSession in the meantime (for example through admin request)
|
||||
realm.logoutAll();
|
||||
|
||||
// Finish login on parent.
|
||||
loginPage.login(PARENT_USERNAME, "password");
|
||||
|
||||
// Test I was not automatically linked
|
||||
links = realm.users().get(childUserId).getFederatedIdentity();
|
||||
Assert.assertTrue(links.isEmpty());
|
||||
|
||||
errorPage.assertCurrent();
|
||||
Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError());
|
||||
|
||||
logoutAll();
|
||||
}
|
||||
|
||||
private void navigateTo(String uri) {
|
||||
driver.navigate().to(uri);
|
||||
WaitUtils.waitForPageToLoad(driver);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.adapter.undertow.servlet;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.testsuite.adapter.servlet.AbstractClientInitiatedAccountLinkTest;
|
||||
import org.keycloak.testsuite.adapter.servlet.AbstractLinkAndExchangeTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
|
||||
*/
|
||||
@AppServerContainer("auth-server-undertow")
|
||||
public class UndertowLinkAndExchangeTest extends AbstractLinkAndExchangeTest {
|
||||
|
||||
//@Test
|
||||
public void testUi() throws Exception {
|
||||
Thread.sleep(1000000000);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@Test
|
||||
public void testAccountLink() throws Exception {
|
||||
super.testAccountLink();
|
||||
}
|
||||
}
|
|
@ -102,15 +102,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
|
|||
illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
illegal.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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue