KEYCLOAK-14113 Support for exchanging to SAML 2.0 token

This commit is contained in:
Hiroyuki Wada 2020-05-11 13:42:50 +09:00 committed by Hynek Mlnařík
parent 08dca9e89f
commit f73b51818b
5 changed files with 857 additions and 3 deletions

View file

@ -114,6 +114,7 @@ public interface OAuth2Constants {
String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token"; String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt"; String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token"; String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";
String SAML2_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:saml2";
String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket"; String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";

View file

@ -203,7 +203,21 @@ public class DocumentUtil {
* @throws TransformerException * @throws TransformerException
*/ */
public static String getDocumentAsString(Document signedDoc) throws ProcessingException, ConfigurationException { public static String getDocumentAsString(Document signedDoc) throws ProcessingException, ConfigurationException {
Source source = new DOMSource(signedDoc); return getNodeAsString(signedDoc);
}
/**
* Marshall a DOM Node into a String
*
* @param node
*
* @return
*
* @throws ProcessingException
* @throws ConfigurationException
*/
public static String getNodeAsString(Node node) throws ProcessingException, ConfigurationException {
Source source = new DOMSource(node);
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
Result streamResult = new StreamResult(sw); Result streamResult = new StreamResult(sw);

View file

@ -60,14 +60,26 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlService;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata; import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
@ -90,6 +102,8 @@ import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import org.keycloak.utils.ProfileHelper; import org.keycloak.utils.ProfileHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.OPTIONS; import javax.ws.rs.OPTIONS;
@ -102,6 +116,8 @@ import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.List; import java.util.List;
@ -872,7 +888,9 @@ public class TokenEndpoint {
String requestedTokenType = formParams.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE); String requestedTokenType = formParams.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
if (requestedTokenType == null) { if (requestedTokenType == null) {
requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE; requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE;
} else if (!requestedTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE) && !requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)) { } else if (!requestedTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE) &&
!requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE) &&
!requestedTokenType.equals(OAuth2Constants.SAML2_TOKEN_TYPE)) {
event.detail(Details.REASON, "requested_token_type unsupported"); event.detail(Details.REASON, "requested_token_type unsupported");
event.error(Errors.INVALID_REQUEST); event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST); throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
@ -904,6 +922,19 @@ public class TokenEndpoint {
String scope = formParams.getFirst(OAuth2Constants.SCOPE); String scope = formParams.getFirst(OAuth2Constants.SCOPE);
switch (requestedTokenType) {
case OAuth2Constants.ACCESS_TOKEN_TYPE:
case OAuth2Constants.REFRESH_TOKEN_TYPE:
return exchangeClientToOIDCClient(targetUser, targetUserSession, requestedTokenType, targetClient, audience, scope);
case OAuth2Constants.SAML2_TOKEN_TYPE:
return exchangeClientToSAML2Client(targetUser, targetUserSession, requestedTokenType, targetClient, audience, scope);
}
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
}
protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionModel targetUserSession, String requestedTokenType,
ClientModel targetClient, String audience, String scope) {
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false); RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(targetClient); AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(targetClient);
@ -945,6 +976,56 @@ public class TokenEndpoint {
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build(); return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
} }
protected Response exchangeClientToSAML2Client(UserModel targetUser, UserSessionModel targetUserSession, String requestedTokenType,
ClientModel targetClient, String audience, String scope) {
// Create authSession with target SAML 2.0 client and authenticated user
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory()
.getProviderFactory(LoginProtocol.class, SamlProtocol.LOGIN_PROTOCOL);
SamlService samlService = (SamlService) factory.createProtocolEndpoint(realm, event);
ResteasyProviderFactory.getInstance().injectProperties(samlService);
AuthenticationSessionModel authSession = samlService.getOrCreateLoginSessionForIdpInitiatedSso(session, realm,
targetClient, null);
if (authSession == null) {
logger.error("SAML assertion consumer url not set up");
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Client requires assertion consumer url set up", Response.Status.BAD_REQUEST);
}
authSession.setAuthenticatedUser(targetUser);
event.session(targetUserSession);
AuthenticationManager.setClientScopesInSession(authSession);
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, targetUserSession,
authSession);
updateUserSessionFromClientAuth(targetUserSession);
// Create SAML 2.0 Assertion Response
SamlClient samlClient = new SamlClient(targetClient);
SamlProtocol samlProtocol = new TokenExchangeSamlProtocol(samlClient).setEventBuilder(event).setHttpHeaders(headers).setRealm(realm)
.setSession(session).setUriInfo(session.getContext().getUri());
Response samlAssertion = samlProtocol.authenticated(authSession, targetUserSession, clientSessionCtx);
if (samlAssertion.getStatus() != 200) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Can not get SAML 2.0 token", Response.Status.BAD_REQUEST);
}
String xmlString = (String) samlAssertion.getEntity();
String encodedXML = Base64Url.encode(xmlString.getBytes(GeneralConstants.SAML_CHARSET));
int assertionLifespan = samlClient.getAssertionLifespan();
AccessTokenResponse res = new AccessTokenResponse();
res.setToken(encodedXML);
res.setTokenType("Bearer");
res.setExpiresIn(assertionLifespan <= 0 ? realm.getAccessCodeLifespan() : assertionLifespan);
res.setOtherClaims(OAuth2Constants.ISSUED_TOKEN_TYPE, requestedTokenType);
event.detail(Details.AUDIENCE, targetClient.getClientId());
event.success();
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
}
public Response exchangeExternalToken(String issuer, String subjectToken) { public Response exchangeExternalToken(String issuer, String subjectToken) {
ExchangeExternalToken externalIdp = null; ExchangeExternalToken externalIdp = null;
IdentityProviderModel externalIdpModel = null; IdentityProviderModel externalIdpModel = null;
@ -1238,4 +1319,35 @@ public class TokenEndpoint {
return codeVerifierEncoded; return codeVerifierEncoded;
} }
private class TokenExchangeSamlProtocol extends SamlProtocol {
final SamlClient samlClient;
TokenExchangeSamlProtocol(SamlClient samlClient) {
this.samlClient = samlClient;
}
@Override
protected Response buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession, String redirectUri,
Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder)
throws ConfigurationException, ProcessingException, IOException {
JaxrsSAML2BindingBuilder.PostBindingBuilder builder = bindingBuilder.postBinding(samlDocument);
Element assertionElement;
if (samlClient.requiresEncryption()) {
assertionElement = DocumentUtil.getElement(builder.getDocument(), new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
} else {
assertionElement = DocumentUtil.getElement(builder.getDocument(), new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()));
}
if (assertionElement == null) {
return Response.status(Status.BAD_REQUEST).build();
}
String assertion = DocumentUtil.getNodeAsString(assertionElement);
return Response.ok(assertion, MediaType.APPLICATION_XML_TYPE).build();
}
@Override
protected Response buildErrorResponse(boolean isPostBinding, String destination, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
return Response.status(Status.BAD_REQUEST).build();
}
}
} }

View file

@ -513,6 +513,11 @@ public class OAuthClient {
public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience, public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience,
String clientId, String clientSecret) throws Exception { String clientId, String clientSecret) throws Exception {
return doTokenExchange(realm, token, targetAudience, clientId, clientSecret, null);
}
public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience,
String clientId, String clientSecret, Map<String, String> additionalParams) throws Exception {
try (CloseableHttpClient client = httpClient.get()) { try (CloseableHttpClient client = httpClient.get()) {
HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm)); HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
@ -522,6 +527,12 @@ public class OAuthClient {
parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)); parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE));
parameters.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, targetAudience)); parameters.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, targetAudience));
if (additionalParams != null) {
for (Map.Entry<String, String> entry : additionalParams.entrySet()) {
parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
}
if (clientSecret != null) { if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization); post.setHeader("Authorization", authorization);
@ -1212,6 +1223,7 @@ public class OAuthClient {
private String idToken; private String idToken;
private String accessToken; private String accessToken;
private String issuedTokenType;
private String tokenType; private String tokenType;
private int expiresIn; private int expiresIn;
private int refreshExpiresIn; private int refreshExpiresIn;
@ -1247,6 +1259,7 @@ public class OAuthClient {
if (statusCode == 200) { if (statusCode == 200) {
idToken = (String) responseJson.get("id_token"); idToken = (String) responseJson.get("id_token");
accessToken = (String) responseJson.get("access_token"); accessToken = (String) responseJson.get("access_token");
issuedTokenType = (String) responseJson.get("issued_token_type");
tokenType = (String) responseJson.get("token_type"); tokenType = (String) responseJson.get("token_type");
expiresIn = (Integer) responseJson.get("expires_in"); expiresIn = (Integer) responseJson.get("expires_in");
refreshExpiresIn = (Integer) responseJson.get("refresh_expires_in"); refreshExpiresIn = (Integer) responseJson.get("refresh_expires_in");
@ -1301,6 +1314,10 @@ public class OAuthClient {
return refreshToken; return refreshToken;
} }
public String getIssuedTokenType() {
return issuedTokenType;
}
public String getTokenType() { public String getTokenType() {
return tokenType; return tokenType;
} }

View file

@ -0,0 +1,710 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Base64Url;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ImpersonationConstants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.BasicAuthHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertNotNull;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
import static org.keycloak.protocol.saml.SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
@AuthServerContainerExclude(AuthServer.REMOTE)
@EnableFeature(value = Profile.Feature.TOKEN_EXCHANGE, skipRestart = true)
public class ClientTokenExchangeSAML2Test extends AbstractKeycloakTest {
private static final String SAML_SIGNED_TARGET = "http://localhost:8080/saml-signed-assertion/";
private static final String SAML_ENCRYPTED_TARGET = "http://localhost:8080/saml-encrypted-assertion/";
private static final String SAML_SIGNED_AND_ENCRYPTED_TARGET = "http://localhost:8080/saml-signed-and-encrypted-assertion/";
private static final String SAML_UNSIGNED_AND_UNENCRYPTED_TARGET = "http://localhost:8080/saml-unsigned-and-unencrypted-assertion/";
private static final String REALM_PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
private static final String REALM_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
private static final String ENCRYPTION_CERTIFICATE = "MIIB1DCCAT0CBgFJGVacCDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1lbmMvMB4XDTE0MTAxNjE0MjA0NloXDTI0MTAxNjE0MjIyNlowMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3QtZW5jLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEikCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBMrfGD9QFfx5v7ld/OAto5rjkTe3R1Qei8XRXfcs83vLaqEzjEtTuLGrJEi55kXuJgBpVmQpnwCCkkjSy0JxbqLDdVi9arfWUxEGmOr01ZHycELhDNaQcFqVMPr5kRHIHgktT8hK2IgCvd3Fy9/JCgUgCPxKfhwecyEOKxUc857g==";
private static final String ENCRYPTION_PRIVATE_KEY = "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t";
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation testRealmRep = new RealmRepresentation();
testRealmRep.setId(TEST);
testRealmRep.setRealm(TEST);
testRealmRep.setEnabled(true);
testRealmRep.setPrivateKey(REALM_PRIVATE_KEY);
testRealmRep.setPublicKey(REALM_PUBLIC_KEY);
testRealmRep.setAccessCodeLifespan(60); // Used as default assertion lifespan
testRealms.add(testRealmRep);
}
public static void setupRealm(KeycloakSession session) {
addTargetClients(session);
addDirectExchanger(session);
RealmModel realm = session.realms().getRealmByName(TEST);
RoleModel exampleRole = realm.getRole("example");
AdminPermissionManagement management = AdminPermissions.management(session, realm);
RoleModel impersonateRole = management.getRealmManagementClient().getRole(ImpersonationConstants.IMPERSONATION_ROLE);
ClientModel clientExchanger = realm.addClient("client-exchanger");
clientExchanger.setClientId("client-exchanger");
clientExchanger.setPublicClient(false);
clientExchanger.setDirectAccessGrantsEnabled(true);
clientExchanger.setEnabled(true);
clientExchanger.setSecret("secret");
clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientExchanger.setFullScopeAllowed(false);
clientExchanger.addScopeMapping(impersonateRole);
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));
ClientModel illegal = realm.addClient("illegal");
illegal.setClientId("illegal");
illegal.setPublicClient(false);
illegal.setDirectAccessGrantsEnabled(true);
illegal.setEnabled(true);
illegal.setSecret("secret");
illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
illegal.setFullScopeAllowed(false);
ClientModel legal = realm.addClient("legal");
legal.setClientId("legal");
legal.setPublicClient(false);
legal.setDirectAccessGrantsEnabled(true);
legal.setEnabled(true);
legal.setSecret("secret");
legal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
legal.setFullScopeAllowed(false);
ClientModel directLegal = realm.addClient("direct-legal");
directLegal.setClientId("direct-legal");
directLegal.setPublicClient(false);
directLegal.setDirectAccessGrantsEnabled(true);
directLegal.setEnabled(true);
directLegal.setSecret("secret");
directLegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
directLegal.setFullScopeAllowed(false);
ClientModel directPublic = realm.addClient("direct-public");
directPublic.setClientId("direct-public");
directPublic.setPublicClient(true);
directPublic.setDirectAccessGrantsEnabled(true);
directPublic.setEnabled(true);
directPublic.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
directPublic.setFullScopeAllowed(false);
ClientModel directNoSecret = realm.addClient("direct-no-secret");
directNoSecret.setClientId("direct-no-secret");
directNoSecret.setPublicClient(false);
directNoSecret.setDirectAccessGrantsEnabled(true);
directNoSecret.setEnabled(true);
directNoSecret.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
directNoSecret.setFullScopeAllowed(false);
// permission for client to client exchange to "target" client
ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
clientRep.setName("to");
clientRep.addClient(clientExchanger.getId());
clientRep.addClient(legal.getId());
clientRep.addClient(directLegal.getId());
ClientModel samlSignedTarget = realm.getClientByClientId(SAML_SIGNED_TARGET);
ClientModel samlEncryptedTarget = realm.getClientByClientId(SAML_ENCRYPTED_TARGET);
ClientModel samlSignedAndEncryptedTarget = realm.getClientByClientId(SAML_SIGNED_AND_ENCRYPTED_TARGET);
ClientModel samlUnsignedAndUnencryptedTarget = realm.getClientByClientId(SAML_UNSIGNED_AND_UNENCRYPTED_TARGET);
assertNotNull(samlSignedTarget);
assertNotNull(samlEncryptedTarget);
assertNotNull(samlSignedAndEncryptedTarget);
assertNotNull(samlUnsignedAndUnencryptedTarget);
ResourceServer server = management.realmResourceServer();
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
management.clients().exchangeToPermission(samlSignedTarget).addAssociatedPolicy(clientPolicy);
management.clients().exchangeToPermission(samlEncryptedTarget).addAssociatedPolicy(clientPolicy);
management.clients().exchangeToPermission(samlSignedAndEncryptedTarget).addAssociatedPolicy(clientPolicy);
management.clients().exchangeToPermission(samlUnsignedAndUnencryptedTarget).addAssociatedPolicy(clientPolicy);
// permission for user impersonation for a client
ClientPolicyRepresentation clientImpersonateRep = new ClientPolicyRepresentation();
clientImpersonateRep.setName("clientImpersonators");
clientImpersonateRep.addClient(directLegal.getId());
clientImpersonateRep.addClient(directPublic.getId());
clientImpersonateRep.addClient(directNoSecret.getId());
server = management.realmResourceServer();
Policy clientImpersonatePolicy = management.authz().getStoreFactory().getPolicyStore().create(clientImpersonateRep, server);
management.users().setPermissionsEnabled(true);
management.users().adminImpersonatingPermission().addAssociatedPolicy(clientImpersonatePolicy);
management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
UserModel user = session.users().addUser(realm, "user");
user.setEnabled(true);
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
user.grantRole(exampleRole);
user.grantRole(impersonateRole);
UserModel bad = session.users().addUser(realm, "bad-impersonator");
bad.setEnabled(true);
session.userCredentialManager().updateCredential(realm, bad, UserCredentialModel.password("password"));
}
@Override
protected boolean isImportAfterEachMethod() {
return true;
}
@Test
@UncaughtServerErrorExpected
public void testExchangeToSAML2SignedAssertion() throws Exception {
testingClient.server().run(ClientTokenExchangeSAML2Test::setupRealm);
oauth.realm(TEST);
oauth.clientId("client-exchanger");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
Map<String, String> params = new HashMap<>();
params.put(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE);
{
response = oauth.doTokenExchange(TEST, accessToken, SAML_SIGNED_TARGET, "client-exchanger", "secret", params);
String exchangedTokenString = response.getAccessToken();
String assertionXML = new String(Base64Url.decode(exchangedTokenString), "UTF-8");
// Verify issued_token_type
Assert.assertEquals(OAuth2Constants.SAML2_TOKEN_TYPE, response.getIssuedTokenType());
// Verify assertion
Element assertionElement = DocumentUtil.getDocument(assertionXML).getDocumentElement();
Assert.assertTrue(AssertionUtil.isSignedElement(assertionElement));
AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement);
Assert.assertTrue(AssertionUtil.isSignatureValid(assertionElement, publicKeyFromString(REALM_PUBLIC_KEY)));
// Expires
Assert.assertEquals(60, response.getExpiresIn());
// Audience
AudienceRestrictionType aud = (AudienceRestrictionType) assertion.getConditions().getConditions().get(0);
Assert.assertEquals(SAML_SIGNED_TARGET, aud.getAudience().get(0).toString());
// NameID
Assert.assertEquals("user", ((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue());
// Role mapping
List<String> roles = AssertionUtil.getRoles(assertion, null);
Assert.assertTrue(roles.contains("example"));
}
{
response = oauth.doTokenExchange(TEST, accessToken, SAML_SIGNED_TARGET, "legal", "secret", params);
String exchangedTokenString = response.getAccessToken();
String assertionXML = new String(Base64Url.decode(exchangedTokenString), "UTF-8");
// Verify issued_token_type
Assert.assertEquals(OAuth2Constants.SAML2_TOKEN_TYPE, response.getIssuedTokenType());
// Verify assertion
Element assertionElement = DocumentUtil.getDocument(assertionXML).getDocumentElement();
Assert.assertTrue(AssertionUtil.isSignedElement(assertionElement));
AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement);
Assert.assertTrue(AssertionUtil.isSignatureValid(assertionElement, publicKeyFromString(REALM_PUBLIC_KEY)));
// Audience
AudienceRestrictionType aud = (AudienceRestrictionType) assertion.getConditions().getConditions().get(0);
Assert.assertEquals(SAML_SIGNED_TARGET, aud.getAudience().get(0).toString());
// NameID
Assert.assertEquals("user", ((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue());
// Role mapping
List<String> roles = AssertionUtil.getRoles(assertion, null);
Assert.assertTrue(roles.contains("example"));
}
{
response = oauth.doTokenExchange(TEST, accessToken, SAML_SIGNED_TARGET, "illegal", "secret", params);
Assert.assertEquals(403, response.getStatusCode());
}
}
@Test
@UncaughtServerErrorExpected
public void testExchangeToSAML2EncryptedAssertion() throws Exception {
testingClient.server().run(ClientTokenExchangeSAML2Test::setupRealm);
oauth.realm(TEST);
oauth.clientId("client-exchanger");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
Map<String, String> params = new HashMap<>();
params.put(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE);
{
response = oauth.doTokenExchange(TEST, accessToken, SAML_ENCRYPTED_TARGET, "client-exchanger", "secret", params);
String exchangedTokenString = response.getAccessToken();
String assertionXML = new String(Base64Url.decode(exchangedTokenString), "UTF-8");
// Verify issued_token_type
Assert.assertEquals(OAuth2Constants.SAML2_TOKEN_TYPE, response.getIssuedTokenType());
// Decrypt assertion
Document assertionDoc = DocumentUtil.getDocument(assertionXML);
Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(assertionDoc, privateKeyFromString(ENCRYPTION_PRIVATE_KEY));
Assert.assertFalse(AssertionUtil.isSignedElement(assertionElement));
AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement);
// Expires
Assert.assertEquals(30, response.getExpiresIn());
// Audience
AudienceRestrictionType aud = (AudienceRestrictionType) assertion.getConditions().getConditions().get(0);
Assert.assertEquals(SAML_ENCRYPTED_TARGET, aud.getAudience().get(0).toString());
// NameID
Assert.assertEquals("user", ((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue());
// Role mapping
List<String> roles = AssertionUtil.getRoles(assertion, null);
Assert.assertTrue(roles.contains("example"));
}
}
@Test
@UncaughtServerErrorExpected
public void testExchangeToSAML2SignedAndEncryptedAssertion() throws Exception {
testingClient.server().run(ClientTokenExchangeSAML2Test::setupRealm);
oauth.realm(TEST);
oauth.clientId("client-exchanger");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
Map<String, String> params = new HashMap<>();
params.put(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE);
{
response = oauth.doTokenExchange(TEST, accessToken, SAML_SIGNED_AND_ENCRYPTED_TARGET, "client-exchanger", "secret", params);
String exchangedTokenString = response.getAccessToken();
String assertionXML = new String(Base64Url.decode(exchangedTokenString), "UTF-8");
// Verify issued_token_type
Assert.assertEquals(OAuth2Constants.SAML2_TOKEN_TYPE, response.getIssuedTokenType());
// Verify assertion
Document assertionDoc = DocumentUtil.getDocument(assertionXML);
Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(assertionDoc, privateKeyFromString(ENCRYPTION_PRIVATE_KEY));
Assert.assertTrue(AssertionUtil.isSignedElement(assertionElement));
AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement);
Assert.assertTrue(AssertionUtil.isSignatureValid(assertionElement, publicKeyFromString(REALM_PUBLIC_KEY)));
// Audience
AudienceRestrictionType aud = (AudienceRestrictionType) assertion.getConditions().getConditions().get(0);
Assert.assertEquals(SAML_SIGNED_AND_ENCRYPTED_TARGET, aud.getAudience().get(0).toString());
// NameID
Assert.assertEquals("user", ((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue());
// Role mapping
List<String> roles = AssertionUtil.getRoles(assertion, null);
Assert.assertTrue(roles.contains("example"));
}
}
@Test
@UncaughtServerErrorExpected
public void testExchangeToSAML2UnsignedAndUnencryptedAssertion() throws Exception {
testingClient.server().run(ClientTokenExchangeSAML2Test::setupRealm);
oauth.realm(TEST);
oauth.clientId("client-exchanger");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
Map<String, String> params = new HashMap<>();
params.put(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE);
{
response = oauth.doTokenExchange(TEST, accessToken, SAML_UNSIGNED_AND_UNENCRYPTED_TARGET, "client-exchanger", "secret", params);
String exchangedTokenString = response.getAccessToken();
String assertionXML = new String(Base64Url.decode(exchangedTokenString), "UTF-8");
// Verify issued_token_type
Assert.assertEquals(OAuth2Constants.SAML2_TOKEN_TYPE, response.getIssuedTokenType());
// Verify assertion
Document assertionDoc = DocumentUtil.getDocument(assertionXML);
Assert.assertFalse(AssertionUtil.isSignedElement(assertionDoc.getDocumentElement()));
AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionDoc);
// Audience
AudienceRestrictionType aud = (AudienceRestrictionType) assertion.getConditions().getConditions().get(0);
Assert.assertEquals(SAML_UNSIGNED_AND_UNENCRYPTED_TARGET, aud.getAudience().get(0).toString());
// NameID
Assert.assertEquals("user", ((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue());
// Role mapping
List<String> roles = AssertionUtil.getRoles(assertion, null);
Assert.assertTrue(roles.contains("example"));
}
}
@Test
@UncaughtServerErrorExpected
public void testImpersonation() throws Exception {
testingClient.server().run(ClientTokenExchangeSAML2Test::setupRealm);
oauth.realm(TEST);
oauth.clientId("client-exchanger");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
Map<String, String> params = new HashMap<>();
params.put(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE);
// client-exchanger can impersonate from token "user" to user "impersonated-user" and to "target" client
{
params.put(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user");
response = oauth.doTokenExchange(TEST, accessToken, SAML_SIGNED_TARGET, "client-exchanger", "secret", params);
String exchangedTokenString = response.getAccessToken();
String assertionXML = new String(Base64Url.decode(exchangedTokenString), "UTF-8");
// Verify issued_token_type
Assert.assertEquals(OAuth2Constants.SAML2_TOKEN_TYPE, response.getIssuedTokenType());
// Verify assertion
Element assertionElement = DocumentUtil.getDocument(assertionXML).getDocumentElement();
Assert.assertTrue(AssertionUtil.isSignedElement(assertionElement));
AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement);
Assert.assertTrue(AssertionUtil.isSignatureValid(assertionElement, publicKeyFromString(REALM_PUBLIC_KEY)));
// Audience
AudienceRestrictionType aud = (AudienceRestrictionType) assertion.getConditions().getConditions().get(0);
Assert.assertEquals(SAML_SIGNED_TARGET, aud.getAudience().get(0).toString());
// NameID
Assert.assertEquals("impersonated-user", ((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue());
// Role mapping
List<String> roles = AssertionUtil.getRoles(assertion, null);
Assert.assertTrue(roles.contains("example"));
}
}
@Test
@UncaughtServerErrorExpected
public void testBadImpersonator() throws Exception {
testingClient.server().run(ClientTokenExchangeSAML2Test::setupRealm);
oauth.realm(TEST);
oauth.clientId("client-exchanger");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "bad-impersonator", "password");
String accessToken = response.getAccessToken();
TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "bad-impersonator");
Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
Map<String, String> params = new HashMap<>();
params.put(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE);
// test that user does not have impersonator permission
{
params.put(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user");
response = oauth.doTokenExchange(TEST, accessToken, SAML_SIGNED_TARGET, "client-exchanger", "secret", params);
Assert.assertEquals(403, response.getStatusCode());
}
}
@Test
@UncaughtServerErrorExpected
public void testDirectImpersonation() throws Exception {
testingClient.server().run(ClientTokenExchangeSAML2Test::setupRealm);
Client httpClient = ClientBuilder.newClient();
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
.path(TEST)
.path("protocol/openid-connect/token");
System.out.println("Exchange url: " + exchangeUrl.getUri().toString());
// direct-legal can impersonate from token "user" to user "impersonated-user" and to "target" client
{
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
.param(OAuth2Constants.AUDIENCE, SAML_SIGNED_TARGET)
));
Assert.assertEquals(200, response.getStatus());
AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
response.close();
String exchangedTokenString = accessTokenResponse.getToken();
String assertionXML = new String(Base64Url.decode(exchangedTokenString), "UTF-8");
// Verify issued_token_type
Assert.assertEquals(OAuth2Constants.SAML2_TOKEN_TYPE, accessTokenResponse.getOtherClaims().get(OAuth2Constants.ISSUED_TOKEN_TYPE));
// Verify assertion
Element assertionElement = DocumentUtil.getDocument(assertionXML).getDocumentElement();
Assert.assertTrue(AssertionUtil.isSignedElement(assertionElement));
AssertionType assertion = (AssertionType) SAMLParser.getInstance().parse(assertionElement);
Assert.assertTrue(AssertionUtil.isSignatureValid(assertionElement, publicKeyFromString(REALM_PUBLIC_KEY)));
// Audience
AudienceRestrictionType aud = (AudienceRestrictionType) assertion.getConditions().getConditions().get(0);
Assert.assertEquals(SAML_SIGNED_TARGET, aud.getAudience().get(0).toString());
// NameID
Assert.assertEquals("impersonated-user", ((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue());
// Role mapping
List<String> roles = AssertionUtil.getRoles(assertion, null);
Assert.assertTrue(roles.contains("example"));
}
// direct-public fails impersonation
{
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-public", "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
.param(OAuth2Constants.AUDIENCE, SAML_SIGNED_TARGET)
));
Assert.assertEquals(403, response.getStatus());
response.close();
}
// direct-no-secret fails impersonation
{
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-no-secret", "secret"))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
.param(OAuth2Constants.AUDIENCE, SAML_SIGNED_TARGET)
));
Assert.assertTrue(response.getStatus() >= 400);
response.close();
}
}
private static void addTargetClients(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(TEST);
// Create SAML 2.0 target clients
ClientModel samlSignedTarget = realm.addClient(SAML_SIGNED_TARGET);
samlSignedTarget.setClientId(SAML_SIGNED_TARGET);
samlSignedTarget.setEnabled(true);
samlSignedTarget.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
samlSignedTarget.setFullScopeAllowed(true);
samlSignedTarget.setAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT, "true");
samlSignedTarget.setAttribute(SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE,
SAML_SIGNED_TARGET + "endpoint");
samlSignedTarget.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username");
samlSignedTarget.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true");
samlSignedTarget.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true");
samlSignedTarget.setAttribute(SamlConfigAttributes.SAML_ENCRYPT, "false");
ClientModel samlEncryptedTarget = realm.addClient(SAML_ENCRYPTED_TARGET);
samlEncryptedTarget.setClientId(SAML_ENCRYPTED_TARGET);
samlEncryptedTarget.setEnabled(true);
samlEncryptedTarget.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
samlEncryptedTarget.setFullScopeAllowed(true);
samlEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT, "true");
samlEncryptedTarget.setAttribute(SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE,
SAML_ENCRYPTED_TARGET + "endpoint");
samlEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username");
samlEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "false");
samlEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true");
samlEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_ENCRYPT, "true");
samlEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, ENCRYPTION_CERTIFICATE);
samlEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_ASSERTION_LIFESPAN, "30");
ClientModel samlSignedAndEncryptedTarget = realm.addClient(SAML_SIGNED_AND_ENCRYPTED_TARGET);
samlSignedAndEncryptedTarget.setClientId(SAML_SIGNED_AND_ENCRYPTED_TARGET);
samlSignedAndEncryptedTarget.setEnabled(true);
samlSignedAndEncryptedTarget.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
samlSignedAndEncryptedTarget.setFullScopeAllowed(true);
samlSignedAndEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT, "true");
samlSignedAndEncryptedTarget.setAttribute(SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE,
SAML_SIGNED_AND_ENCRYPTED_TARGET + "endpoint");
samlSignedAndEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username");
samlSignedAndEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "true");
samlSignedAndEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true");
samlSignedAndEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_ENCRYPT, "true");
samlSignedAndEncryptedTarget.setAttribute(SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, ENCRYPTION_CERTIFICATE);
ClientModel samlUnsignedAndUnencryptedTarget = realm.addClient(SAML_UNSIGNED_AND_UNENCRYPTED_TARGET);
samlUnsignedAndUnencryptedTarget.setClientId(SAML_UNSIGNED_AND_UNENCRYPTED_TARGET);
samlUnsignedAndUnencryptedTarget.setEnabled(true);
samlUnsignedAndUnencryptedTarget.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
samlUnsignedAndUnencryptedTarget.setFullScopeAllowed(true);
samlUnsignedAndUnencryptedTarget.setAttribute(SamlConfigAttributes.SAML_AUTHNSTATEMENT, "true");
samlUnsignedAndUnencryptedTarget.setAttribute(SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE,
SAML_UNSIGNED_AND_UNENCRYPTED_TARGET + "endpoint");
samlUnsignedAndUnencryptedTarget.setAttribute(SamlConfigAttributes.SAML_NAME_ID_FORMAT_ATTRIBUTE, "username");
samlUnsignedAndUnencryptedTarget.setAttribute(SamlConfigAttributes.SAML_ASSERTION_SIGNATURE, "false");
samlUnsignedAndUnencryptedTarget.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true");
samlUnsignedAndUnencryptedTarget.setAttribute(SamlConfigAttributes.SAML_ENCRYPT, "false");
}
private static void addDirectExchanger(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(TEST);
RoleModel exampleRole = realm.addRole("example");
AdminPermissionManagement management = AdminPermissions.management(session, realm);
ClientModel directExchanger = realm.addClient("direct-exchanger");
directExchanger.setName("direct-exchanger");
directExchanger.setClientId("direct-exchanger");
directExchanger.setPublicClient(false);
directExchanger.setDirectAccessGrantsEnabled(true);
directExchanger.setEnabled(true);
directExchanger.setSecret("secret");
directExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
directExchanger.setFullScopeAllowed(false);
// permission for client to client exchange to "target" client
management.clients().setPermissionsEnabled(realm.getClientByClientId(SAML_SIGNED_TARGET), true);
management.clients().setPermissionsEnabled(realm.getClientByClientId(SAML_ENCRYPTED_TARGET), true);
management.clients().setPermissionsEnabled(realm.getClientByClientId(SAML_SIGNED_AND_ENCRYPTED_TARGET), true);
management.clients().setPermissionsEnabled(realm.getClientByClientId(SAML_UNSIGNED_AND_UNENCRYPTED_TARGET), true);
ClientPolicyRepresentation clientImpersonateRep = new ClientPolicyRepresentation();
clientImpersonateRep.setName("clientImpersonatorsDirect");
clientImpersonateRep.addClient(directExchanger.getId());
ResourceServer server = management.realmResourceServer();
Policy clientImpersonatePolicy = management.authz().getStoreFactory().getPolicyStore().create(clientImpersonateRep, server);
management.users().setPermissionsEnabled(true);
management.users().adminImpersonatingPermission().addAssociatedPolicy(clientImpersonatePolicy);
management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
UserModel impersonatedUser = session.users().addUser(realm, "impersonated-user");
impersonatedUser.setEnabled(true);
session.userCredentialManager().updateCredential(realm, impersonatedUser, UserCredentialModel.password("password"));
impersonatedUser.grantRole(exampleRole);
}
private PublicKey publicKeyFromString(String publicKey) {
return org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKey);
}
private PrivateKey privateKeyFromString(String privateKey) {
return org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKey);
}
}