KEYCLOAK-14113 Support for exchanging to SAML 2.0 token
This commit is contained in:
parent
08dca9e89f
commit
f73b51818b
5 changed files with 857 additions and 3 deletions
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -1237,5 +1318,36 @@ public class TokenEndpoint {
|
||||||
String codeVerifierEncoded = Base64Url.encode(digestBytes);
|
String codeVerifierEncoded = Base64Url.encode(digestBytes);
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue