KEYCLOAK-11029 Support modification of broker username / ID for identity provider linking

This commit is contained in:
Hynek Mlnarik 2020-08-31 16:42:35 +02:00 committed by Hynek Mlnařík
parent 0362d3a430
commit 583fa07bc4
14 changed files with 571 additions and 117 deletions

View file

@ -44,9 +44,7 @@ public class StackUtil {
* level, then returns stack trace, else returns empty {@link StringBuilder}
*/
public static StringBuilder getShortStackTrace(String prefix) {
if (! LOG.isTraceEnabled()) {
return EMPTY;
}
if (! isShortStackTraceEnabled()) return EMPTY;
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = (new Throwable()).getStackTrace();
@ -62,4 +60,8 @@ public class StackUtil {
}
return sb;
}
public static boolean isShortStackTraceEnabled() {
return LOG.isTraceEnabled();
}
}

View file

@ -20,6 +20,7 @@ package org.keycloak.broker.oidc.mappers;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.saml.mappers.UsernameTemplateMapper.Target;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.KeycloakSession;
@ -45,9 +46,15 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.keycloak.broker.saml.mappers.UsernameTemplateMapper.TARGET;
import static org.keycloak.broker.saml.mappers.UsernameTemplateMapper.TARGETS;
import static org.keycloak.broker.saml.mappers.UsernameTemplateMapper.TRANSFORMERS;
import static org.keycloak.broker.saml.mappers.UsernameTemplateMapper.getTarget;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -83,10 +90,20 @@ public class UsernameTemplateMapper extends AbstractClaimMapper {
property = new ProviderConfigProperty();
property.setName(TEMPLATE);
property.setLabel("Template");
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${CLAIM.sub}'. ALIAS is the provider alias. CLAIM.<NAME> references an ID or Access token claim.");
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${CLAIM.sub}'. ALIAS is the provider alias. CLAIM.<NAME> references an ID or Access token claim. \n"
+ "The substitution can be converted to upper or lower case by appending |uppercase or |lowercase to the substituted value, e.g. '${CLAIM.sub | lowercase}");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setDefaultValue("${ALIAS}.${CLAIM.preferred_username}");
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(TARGET);
property.setLabel("Target");
property.setHelpText("Destination field for the mapper. LOCAL (default) means that the changes are applied to the username stored in local database upon user import. BROKER_ID and BROKER_USERNAME means that the changes are stored into the ID or username used for federation user lookup, respectively.");
property.setType(ProviderConfigProperty.LIST_TYPE);
property.setOptions(TARGETS);
property.setDefaultValue(Target.LOCAL.toString());
configProperties.add(property);
}
public static final String PROVIDER_ID = "oidc-username-idp-mapper";
@ -129,12 +146,12 @@ public class UsernameTemplateMapper extends AbstractClaimMapper {
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
// preprocessFederatedIdentity gets called anyways, so we only need to set the username if necessary.
// However, we don't want to set the username when the email is used as username
if (!realm.isRegistrationEmailAsUsername()) {
if (getTarget(mapperModel.getConfig().get(TARGET)) == Target.LOCAL && !realm.isRegistrationEmailAsUsername()) {
user.setUsername(context.getModelUsername());
}
}
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
private static final Pattern SUBSTITUTION = Pattern.compile("\\$\\{([^}]+?)(?:\\s*\\|\\s*(\\S+)\\s*)?\\}");
@Override
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
@ -143,27 +160,30 @@ public class UsernameTemplateMapper extends AbstractClaimMapper {
private void setUserNameFromTemplate(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String template = mapperModel.getConfig().get(TEMPLATE);
Matcher m = substitution.matcher(template);
Matcher m = SUBSTITUTION.matcher(template);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String variable = m.group(1);
UnaryOperator<String> transformer = Optional.ofNullable(m.group(2)).map(TRANSFORMERS::get).orElse(UnaryOperator.identity());
if (variable.equals("ALIAS")) {
m.appendReplacement(sb, context.getIdpConfig().getAlias());
m.appendReplacement(sb, transformer.apply(context.getIdpConfig().getAlias()));
} else if (variable.equals("UUID")) {
m.appendReplacement(sb, KeycloakModelUtils.generateId());
m.appendReplacement(sb, transformer.apply(KeycloakModelUtils.generateId()));
} else if (variable.startsWith("CLAIM.")) {
String name = variable.substring("CLAIM.".length());
Object value = AbstractClaimMapper.getClaimValue(context, name);
if (value == null) value = "";
m.appendReplacement(sb, value.toString());
m.appendReplacement(sb, transformer.apply(value.toString()));
} else {
m.appendReplacement(sb, m.group(1));
}
}
m.appendTail(sb);
String username = sb.toString();
context.setModelUsername(username);
Target t = getTarget(mapperModel.getConfig().get(TARGET));
t.set(context, sb.toString());
}
@Override

View file

@ -36,9 +36,13 @@ import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -50,10 +54,20 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
public static final String TEMPLATE = "template";
public static final String TARGET = "target";
public enum Target {
LOCAL { public void set(BrokeredIdentityContext context, String value) { context.setModelUsername(value); } },
BROKER_ID { public void set(BrokeredIdentityContext context, String value) { context.setId(value); } },
BROKER_USERNAME { public void set(BrokeredIdentityContext context, String value) { context.setUsername(value); } };
public abstract void set(BrokeredIdentityContext context, String value);
}
public static final List<String> TARGETS = Arrays.asList(Target.LOCAL.toString(), Target.BROKER_ID.toString(), Target.BROKER_USERNAME.toString());
public static final Map<String, UnaryOperator<String>> TRANSFORMERS = new HashMap<>();
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));
static {
@ -61,10 +75,23 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
property = new ProviderConfigProperty();
property.setName(TEMPLATE);
property.setLabel("Template");
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${NAMEID}'. ALIAS is the provider alias. NAMEID is that SAML name id assertion. ATTRIBUTE.<NAME> references a SAML attribute where name is the attribute name or friendly name.");
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${NAMEID}'. ALIAS is the provider alias. NAMEID is that SAML name id assertion. ATTRIBUTE.<NAME> references a SAML attribute where name is the attribute name or friendly name. \n"
+ "The substitution can be converted to upper or lower case by appending |uppercase or |lowercase to the substituted value, e.g. '${NAMEID | lowercase}");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setDefaultValue("${ALIAS}.${NAMEID}");
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(TARGET);
property.setLabel("Target");
property.setHelpText("Destination field for the mapper. LOCAL (default) means that the changes are applied to the username stored in local database upon user import. BROKER_ID and BROKER_USERNAME means that the changes are stored into the ID or username used for federation user lookup, respectively.");
property.setType(ProviderConfigProperty.LIST_TYPE);
property.setOptions(TARGETS);
property.setDefaultValue(Target.LOCAL.toString());
configProperties.add(property);
TRANSFORMERS.put("uppercase", String::toUpperCase);
TRANSFORMERS.put("lowercase", String::toLowerCase);
}
public static final String PROVIDER_ID = "saml-username-idp-mapper";
@ -107,12 +134,12 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
// preprocessFederatedIdentity gets called anyways, so we only need to set the username if necessary.
// However, we don't want to set the username when the email is used as username
if (!realm.isRegistrationEmailAsUsername()) {
if (getTarget(mapperModel.getConfig().get(TARGET)) == Target.LOCAL && !realm.isRegistrationEmailAsUsername()) {
user.setUsername(context.getModelUsername());
}
}
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
private static final Pattern SUBSTITUTION = Pattern.compile("\\$\\{([^}]+?)(?:\\s*\\|\\s*(\\S+)\\s*)?\\}");
@Override
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
@ -122,19 +149,21 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
private void setUserNameFromTemplate(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
String template = mapperModel.getConfig().get(TEMPLATE);
Matcher m = substitution.matcher(template);
Matcher m = SUBSTITUTION.matcher(template);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String variable = m.group(1);
UnaryOperator<String> transformer = Optional.ofNullable(m.group(2)).map(TRANSFORMERS::get).orElse(UnaryOperator.identity());
if (variable.equals("ALIAS")) {
m.appendReplacement(sb, context.getIdpConfig().getAlias());
m.appendReplacement(sb, transformer.apply(context.getIdpConfig().getAlias()));
} else if (variable.equals("UUID")) {
m.appendReplacement(sb, KeycloakModelUtils.generateId());
m.appendReplacement(sb, transformer.apply(KeycloakModelUtils.generateId()));
} else if (variable.equals("NAMEID")) {
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
m.appendReplacement(sb, subjectNameID.getValue());
m.appendReplacement(sb, transformer.apply(subjectNameID.getValue()));
} else if (variable.startsWith("ATTRIBUTE.")) {
String name = variable.substring("ATTRIBUTE.".length());
String value = "";
@ -150,14 +179,16 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
}
}
}
m.appendReplacement(sb, value);
m.appendReplacement(sb, transformer.apply(value));
} else {
m.appendReplacement(sb, m.group(1));
}
}
m.appendTail(sb);
context.setModelUsername(sb.toString());
Target t = getTarget(mapperModel.getConfig().get(TARGET));
t.set(context, sb.toString());
}
@Override
@ -165,4 +196,12 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
return "Format the username to import.";
}
public static Target getTarget(String value) {
try {
return value == null ? Target.LOCAL : Target.valueOf(value);
} catch (IllegalArgumentException ex) {
return Target.LOCAL;
}
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.events.log;
import org.keycloak.common.util.StackUtil;
import org.jboss.logging.Logger;
import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
@ -95,6 +96,10 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider
if(logger.isTraceEnabled()) {
setKeycloakContext(sb);
if (StackUtil.isShortStackTraceEnabled()) {
sb.append(", stackTrace=").append(StackUtil.getShortStackTrace());
}
}
logger.log(logger.isTraceEnabled() ? Logger.Level.TRACE : level, sb.toString());

View file

@ -30,6 +30,8 @@
<module name="org.bouncycastle"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-saml-core"/>
<module name="org.keycloak.keycloak-saml-core-public"/>
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.keycloak.keycloak-services"/>

View file

@ -1,25 +1,25 @@
package org.keycloak.testsuite.broker;
class BrokerTestConstants {
public class BrokerTestConstants {
final static String REALM_PROV_NAME = "provider";
final static String REALM_CONS_NAME = "consumer";
public final static String REALM_PROV_NAME = "provider";
public final static String REALM_CONS_NAME = "consumer";
final static String IDP_OIDC_ALIAS = "kc-oidc-idp";
final static String IDP_OIDC_PROVIDER_ID = "keycloak-oidc";
public final static String IDP_OIDC_ALIAS = "kc-oidc-idp";
public final static String IDP_OIDC_PROVIDER_ID = "keycloak-oidc";
final static String IDP_SAML_ALIAS = "kc-saml-idp";
final static String IDP_SAML_PROVIDER_ID = "saml";
public final static String IDP_SAML_ALIAS = "kc-saml-idp";
public final static String IDP_SAML_PROVIDER_ID = "saml";
final static String CLIENT_ID = "brokerapp";
final static String CLIENT_SECRET = "secret";
final static String VAULT_CLIENT_SECRET = "${vault.oidc_idp}";
public final static String CLIENT_ID = "brokerapp";
public final static String CLIENT_SECRET = "secret";
public final static String VAULT_CLIENT_SECRET = "${vault.oidc_idp}";
final static String USER_LOGIN = "testuser";
final static String USER_EMAIL = "user@localhost.com";
final static String USER_PASSWORD = "password";
public final static String USER_LOGIN = "testuser";
public final static String USER_EMAIL = "user@localhost.com";
public final static String USER_PASSWORD = "password";
final static String IDP_SAML_SIGN_KEY = "MIICWwIBAAKBgQDVG8a7xGN6ZIkDbeecySygc" +
public final static String IDP_SAML_SIGN_KEY = "MIICWwIBAAKBgQDVG8a7xGN6ZIkDbeecySygc" +
"DfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb4" +
"0tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SV" +
"Er55KJoyQJQIDAQABAoGADaTtoG/+foOZUiLjRWKL/OmyavK9vjgyFtThNkZY4qHOh0h3" +
@ -33,7 +33,7 @@ class BrokerTestConstants {
"kAC5loUGwU5dLaugsGH/a2Q8Ac8bmPglwfCstYDpl8Gp/eimb1eKyvDEELOhyImAv4/uZ" +
"V9wN85V0xZXWsw==";
final static String IDP_SAML_SIGN_CERT = "MIIDdzCCAl+gAwIBAgIEbySuqTANBgkqhkiG" +
public final static String IDP_SAML_SIGN_CERT = "MIIDdzCCAl+gAwIBAgIEbySuqTANBgkqhkiG" +
"9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDV" +
"QQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDg" +
"YDVQQDEwdVbmtub3duMB4XDTE1MDEyODIyMTYyMFoXDTE3MTAyNDIyMTYyMFowbDEQMA4" +
@ -52,7 +52,7 @@ class BrokerTestConstants {
"KAXjMeHfzbiBr+cWz8NYZEtxUEDYDjTpKrYCSMJBXpmgVJCZ00BswbksxJwaGqGMPpUKm" +
"CV671pf3m8nq3xyiHMDGuGwtbU+GE8kVx85menmp8+964nin";
final static String REALM_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwg" +
public final static String REALM_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwg" +
"gSkAgEAAoIBAQCCPyvTTb14vSMkpe/pds2P5Cqxk7bkeFnQiNMS1vyZ+HS2O79fxzp1eA" +
"guHnBTs4XTRT7SZJhIT/6utgqZjmDigKV5N7X5ptq8BM/W1qa1cYBRip261pc+tWf3Iyw" +
"JYQ9yFI9mUQarmIEl0D7GH16NSZklheaWfbodRVarvX+ML0amNtGYVDft/RftYmgbKKrK" +
@ -77,7 +77,7 @@ class BrokerTestConstants {
"spPC1kySiy+Ndr9jNohRZkR7pEjgqA5E8rdzc88LirUN7bY5HFHRWN9KXrs5/o3O1K3GF" +
"Cp64N6nvnPEYZ2zSJalcMC2fjSsJg26z8Dg1H+gfTIDUMoGiEAAnJXuqk+WayPU+fZMLn";
final static String REALM_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgK" +
public final static String REALM_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgK" +
"CAQEAgj8r0029eL0jJKXv6XbNj+QqsZO25HhZ0IjTEtb8mfh0tju/X8c6dXgILh5wU7OF0" +
"0U+0mSYSE/+rrYKmY5g4oCleTe1+abavATP1tamtXGAUYqdutaXPrVn9yMsCWEPchSPZlE" +
"Gq5iBJdA+xh9ejUmZJYXmln26HUVWq71/jC9GpjbRmFQ37f0X7WJoGyiqyttfKkKfUeBmR" +

View file

@ -24,6 +24,8 @@ import org.keycloak.admin.client.resource.ComponentResource;
import org.keycloak.admin.client.resource.ComponentsResource;
import org.keycloak.admin.client.resource.GroupResource;
import org.keycloak.admin.client.resource.GroupsResource;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.IdentityProvidersResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
@ -32,6 +34,7 @@ import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.HashMap;
@ -39,7 +42,9 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.ws.rs.core.Response;
import org.hamcrest.Matchers;
import org.jboss.logging.Logger;
import org.junit.Assert;
import static org.keycloak.testsuite.admin.ApiUtil.getCreatedId;
/**
@ -105,6 +110,17 @@ public class Creator<T> implements AutoCloseable {
}
}
public static Creator<IdentityProviderResource> create(RealmResource realmResource, IdentityProviderRepresentation rep) {
final IdentityProvidersResource res = realmResource.identityProviders();
Assert.assertThat("Identity provider alias must be specified", rep.getAlias(), Matchers.notNullValue());
try (Response response = res.create(rep)) {
String createdId = getCreatedId(response);
final IdentityProviderResource r = res.get(rep.getAlias());
LOG.debugf("Created identity provider ID %s", createdId);
return new Creator(createdId, r, r::remove);
}
}
private final String id;
private final T resource;
private final Runnable closer;

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.util;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.util.SamlClient.Binding;
@ -35,13 +36,14 @@ import org.keycloak.testsuite.util.saml.LoginBuilder;
import org.keycloak.testsuite.util.saml.UpdateProfileBuilder;
import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
import org.keycloak.testsuite.util.saml.RequiredConsentBuilder;
import java.util.function.Function;
import javax.ws.rs.core.Response.Status;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.hamcrest.Matcher;
import org.junit.Assert;
import org.w3c.dom.Document;
import static org.hamcrest.Matchers.notNullValue;
import static org.keycloak.testsuite.util.saml.SamlDocumentStepBuilder.saml2Object2String;
/**
*
@ -114,6 +116,10 @@ public class SamlClientBuilder {
return this;
}
public <T> T andThen(Function<SamlClientBuilder, T> next) {
return next.apply(this);
}
public SamlClientBuilder assertResponse(Matcher<? super CloseableHttpResponse> matcher) {
steps.add((client, currentURI, currentResponse, context) -> {
Assert.assertThat(currentResponse, matcher);
@ -164,6 +170,22 @@ public class SamlClientBuilder {
return addStepBuilder(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
}
/** Issues the given SAML document to the SAML endpoint */
public ModifySamlResponseStepBuilder submitSamlDocument(URI authServerSamlUrl, String samlDocument, Binding binding) {
return addStepBuilder(new ModifySamlResponseStepBuilder(binding, this)
.targetUri(authServerSamlUrl)
.documentSupplier(() -> samlDocument)
);
}
/** Issues the given SAML document to the SAML endpoint */
public ModifySamlResponseStepBuilder submitSamlDocument(URI authServerSamlUrl, SAML2Object samlObject, Binding binding) {
return addStepBuilder(new ModifySamlResponseStepBuilder(binding, this)
.targetUri(authServerSamlUrl)
.documentSupplier(() -> saml2Object2String(samlObject))
);
}
/** Handles login page */
public LoginBuilder login() {
return addStepBuilder(new LoginBuilder(this));

View file

@ -29,6 +29,8 @@ import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.io.IOUtils;
import org.apache.http.NameValuePair;
@ -62,6 +64,7 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
private URI targetUri;
private String targetAttribute;
private Binding targetBinding;
private Supplier<String> documentSupplier;
public ModifySamlResponseStepBuilder(Binding binding, SamlClientBuilder clientBuilder) {
super(clientBuilder);
@ -83,6 +86,15 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
}
public Supplier<String> documentSupplier() {
return documentSupplier;
}
public ModifySamlResponseStepBuilder documentSupplier(Supplier<String> documentSupplier) {
this.documentSupplier = documentSupplier;
return this;
}
public Binding targetBinding() {
return targetBinding;
}
@ -119,86 +131,108 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
}
protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException {
NameValuePair samlParam = null;
String samlDoc;
final String attrName;
final URI uri;
final List<NameValuePair> params;
assertThat(currentResponse, statusCodeIsHC(Status.FOUND));
String location = currentResponse.getFirstHeader("Location").getValue();
URI locationUri = URI.create(location);
if (documentSupplier != null) {
Objects.requireNonNull(this.targetUri, "Set targetUri");
Objects.requireNonNull(this.targetAttribute, "Set targetAttribute");
List<NameValuePair> params = URLEncodedUtils.parse(locationUri, "UTF-8");
for (Iterator<NameValuePair> it = params.iterator(); it.hasNext();) {
NameValuePair param = it.next();
if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
assertThat("Only one SAMLRequest/SAMLResponse check", samlParam, nullValue());
samlParam = param;
it.remove();
samlDoc = documentSupplier.get();
uri = this.targetUri;
attrName = this.targetAttribute;
params = new LinkedList<>();
} else {
NameValuePair samlParam = null;
assertThat(currentResponse, statusCodeIsHC(Status.FOUND));
String location = currentResponse.getFirstHeader("Location").getValue();
URI locationUri = URI.create(location);
params = URLEncodedUtils.parse(locationUri, "UTF-8");
for (Iterator<NameValuePair> it = params.iterator(); it.hasNext();) {
NameValuePair param = it.next();
if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
assertThat("Only one SAMLRequest/SAMLResponse check", samlParam, nullValue());
samlParam = param;
it.remove();
}
}
assertThat(samlParam, notNullValue());
String base64EncodedSamlDoc = samlParam.getValue();
InputStream decoded = RedirectBindingUtil.base64DeflateDecode(base64EncodedSamlDoc);
samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
IOUtils.closeQuietly(decoded);
uri = this.targetUri != null
? this.targetUri
: locationUri;
attrName = this.targetAttribute != null ? this.targetAttribute : samlParam.getName();
}
assertThat(samlParam, notNullValue());
String base64EncodedSamlDoc = samlParam.getValue();
InputStream decoded = RedirectBindingUtil.base64DeflateDecode(base64EncodedSamlDoc);
String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
IOUtils.closeQuietly(decoded);
String transformed = getTransformer().transform(samlDoc);
if (transformed == null) {
return null;
}
final String attrName = this.targetAttribute != null ? this.targetAttribute : samlParam.getName();
final URI uri = this.targetUri != null
? this.targetUri
: locationUri;
return createRequest(uri, attrName, transformed, params);
return createRequest(uri, attrName, samlDoc, params);
}
private HttpUriRequest handlePostBinding(CloseableHttpResponse currentResponse) throws Exception {
assertThat(currentResponse, statusCodeIsHC(Status.OK));
String samlDoc;
final String attrName;
final URI uri;
final List<NameValuePair> params = new LinkedList<>();
final String htmlBody = EntityUtils.toString(currentResponse.getEntity());
assertThat(htmlBody, Matchers.containsString("SAML"));
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(htmlBody);
Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]");
Elements forms = theResponsePage.select("form");
Elements relayStates = theResponsePage.select("input[name=RelayState]");
int size = samlResponses.size() + samlRequests.size();
assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1));
assertThat("Checking uniqueness of forms in the page", forms, hasSize(1));
if (documentSupplier != null) {
Objects.requireNonNull(this.targetUri, "Set targetUri");
Objects.requireNonNull(this.targetAttribute, "Set targetAttribute");
Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
Element form = forms.first();
samlDoc = documentSupplier.get();
uri = this.targetUri;
attrName = this.targetAttribute;
} else {
assertThat(currentResponse, statusCodeIsHC(Status.OK));
String base64EncodedSamlDoc = respElement.val();
InputStream decoded = PostBindingUtil.base64DecodeAsStream(base64EncodedSamlDoc);
String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
IOUtils.closeQuietly(decoded);
final String htmlBody = EntityUtils.toString(currentResponse.getEntity());
assertThat(htmlBody, Matchers.containsString("SAML"));
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(htmlBody);
Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]");
Elements forms = theResponsePage.select("form");
Elements relayStates = theResponsePage.select("input[name=RelayState]");
int size = samlResponses.size() + samlRequests.size();
assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1));
assertThat("Checking uniqueness of forms in the page", forms, hasSize(1));
Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
Element form = forms.first();
String base64EncodedSamlDoc = respElement.val();
InputStream decoded = PostBindingUtil.base64DecodeAsStream(base64EncodedSamlDoc);
samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
IOUtils.closeQuietly(decoded);
attrName = this.targetAttribute != null
? this.targetAttribute
: respElement.attr("name");
if (! relayStates.isEmpty()) {
params.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayStates.first().val()));
}
uri = this.targetUri != null
? this.targetUri
: URI.create(form.attr("action"));
}
return createRequest(uri, attrName, samlDoc, params);
}
protected HttpUriRequest createRequest(URI locationUri, String attributeName, String samlDoc, List<NameValuePair> parameters) throws Exception {
String transformed = getTransformer().transform(samlDoc);
if (transformed == null) {
return null;
}
final String attributeName = this.targetAttribute != null
? this.targetAttribute
: respElement.attr("name");
List<NameValuePair> parameters = new LinkedList<>();
if (! relayStates.isEmpty()) {
parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayStates.first().val()));
}
URI locationUri = this.targetUri != null
? this.targetUri
: URI.create(form.attr("action"));
return createRequest(locationUri, attributeName, transformed, parameters);
}
protected HttpUriRequest createRequest(URI locationUri, String attributeName, String transformed, List<NameValuePair> parameters) throws IOException, URISyntaxException {
switch (this.targetBinding) {
case POST:
return createPostRequest(locationUri, attributeName, transformed, parameters);

View file

@ -26,6 +26,7 @@ import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
@ -36,6 +37,7 @@ import org.keycloak.testsuite.util.SamlClient.Step;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.function.Consumer;
import java.util.logging.Level;
import javax.xml.stream.XMLStreamWriter;
import org.jboss.logging.Logger;
import org.junit.Assert;
@ -97,9 +99,18 @@ public abstract class SamlDocumentStepBuilder<T extends SAML2Object, This extend
return null;
}
String res = saml2Object2String(transformed);
LOG.debugf(" ---> %s", res);
return res;
};
return (This) this;
}
public static String saml2Object2String(final SAML2Object transformed) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
if (transformed instanceof AuthnRequestType) {
new SAMLRequestWriter(xmlStreamWriter).write((AuthnRequestType) transformed);
} else if (transformed instanceof LogoutRequestType) {
@ -118,11 +129,10 @@ public abstract class SamlDocumentStepBuilder<T extends SAML2Object, This extend
Assert.assertNotNull("Unknown type: <null>", transformed);
Assert.fail("Unknown type: " + transformed.getClass().getName());
}
String res = new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET);
LOG.debugf(" ---> %s", res);
return res;
};
return (This) this;
return new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET);
} catch (ProcessingException ex) {
throw new RuntimeException(ex);
}
}
public This transformDocument(Consumer<Document> tr) {

View file

@ -106,12 +106,8 @@ public abstract class AbstractLDAPTest extends AbstractTestRealmKeycloakTest {
protected ComponentRepresentation findMapperRepByName(String name) {
List<ComponentRepresentation> mappers = testRealm().components().query(ldapModelId, LDAPStorageMapper.class.getName());
for (ComponentRepresentation mapper : mappers) {
if (mapper.getName().equals(name)) {
return mapper;
}
}
return null;
return testRealm().components().query(ldapModelId, LDAPStorageMapper.class.getName()).stream()
.filter(mapper -> mapper.getName().equals(name))
.findAny().orElse(null);
}
}

View file

@ -0,0 +1,299 @@
/*
* 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.federation.ldap;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticatorFactory;
import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory;
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
import org.keycloak.broker.saml.mappers.UsernameTemplateMapper;
import org.keycloak.broker.saml.mappers.UsernameTemplateMapper.Target;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.saml.SAML2LoginResponseBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.broker.KcSamlBrokerConfiguration;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.updaters.Creator;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.LDAPRule;
import org.keycloak.testsuite.util.LDAPTestUtils;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClientBuilder;
import com.google.common.collect.ImmutableMap;
import java.net.URI;
import java.util.UUID;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_SAML_ALIAS;
import static org.keycloak.testsuite.federation.ldap.AbstractLDAPTest.TEST_REALM_NAME;
import static org.keycloak.testsuite.federation.ldap.AbstractLDAPTest.ldapModelId;
/**
*
* @author hmlnarik
*/
public class LDAPSamlIdPInitiatedVaryingLetterCaseTest extends AbstractLDAPTest {
@ClassRule
public static LDAPRule ldapRule = new LDAPRule();
private static final String USER_NAME_LDAP = "JdOe";
private static final String USER_NAME_LOWERCASE = USER_NAME_LDAP.toLowerCase();
private static final String USER_NAME_UPPERCASE = USER_NAME_LDAP.toUpperCase();
private static final String USER_FIRST_NAME = "Joe";
private static final String USER_LAST_NAME = "Doe";
private static final String USER_PASSWORD = "P@ssw0rd!";
private static final String USER_EMAIL = "jdoe@keycloak.org";
private static final String USER_STREET = "Street";
private static final String USER_POSTAL_CODE = "Post code";
private static final String MY_APP = "myapp";
private static final String EXT_SSO = "sso";
private static final String EXT_SSO_URL = "http://localhost-" + EXT_SSO + ".127.0.0.1.nip.io";
private static final String DUMMY_URL = "http://localhost-" + EXT_SSO + "-dummy.127.0.0.1.nip.io";
private static final String FLOW_AUTO_LINK = "AutoLink";
private String idpAlias;
@Override
protected LDAPRule getLDAPRule() {
return ldapRule;
}
@Override
protected void afterImportTestRealm() {
getTestingClient().server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
// Delete all LDAP users
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);
// Add some new LDAP users for testing
LDAPObject user = LDAPTestUtils.addLDAPUser
(
ctx.getLdapProvider(),
appRealm,
USER_NAME_LDAP,
USER_FIRST_NAME,
USER_LAST_NAME,
USER_EMAIL,
USER_STREET,
USER_POSTAL_CODE
);
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), user, USER_PASSWORD);
});
ComponentRepresentation ldap = testRealm().components().query(null, "org.keycloak.storage.UserStorageProvider").get(0);
ComponentRepresentation ldapMapper = new ComponentRepresentation();
ldapMapper.setName("uid-to-user-attr-mapper");
ldapMapper.setProviderId(UserAttributeLDAPStorageMapperFactory.PROVIDER_ID);
ldapMapper.setProviderType("org.keycloak.storage.ldap.mappers.LDAPStorageMapper");
ldapMapper.setParentId(ldap.getId());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
config.add(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "ldapUid");
config.add(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "uid");
config.add(UserAttributeLDAPStorageMapper.READ_ONLY, "true");
config.add(UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
ldapMapper.setConfig(config);
testRealm().components().add(ldapMapper);
}
@Before
public void setupIdentityProvider() {
// Configure autolink flow
AuthenticationFlowRepresentation newFlow = new AuthenticationFlowRepresentation();
newFlow.setAlias(FLOW_AUTO_LINK);
newFlow.setDescription("Auto-link flow");
newFlow.setProviderId("basic-flow");
newFlow.setBuiltIn(false);
newFlow.setTopLevel(true);
Creator.Flow amr = Creator.create(testRealm(), newFlow);
AuthenticationExecutionInfoRepresentation exCreateUser = amr.addExecution(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID);
exCreateUser.setRequirement(Requirement.ALTERNATIVE.name());
testRealm().flows().updateExecutions(FLOW_AUTO_LINK, exCreateUser);
AuthenticationExecutionInfoRepresentation exAutoLink = amr.addExecution(IdpAutoLinkAuthenticatorFactory.PROVIDER_ID);
exAutoLink.setRequirement(Requirement.ALTERNATIVE.name());
testRealm().flows().updateExecutions(FLOW_AUTO_LINK, exAutoLink);
getCleanup().addCleanup(amr);
// Configure identity provider
IdentityProviderRepresentation idp = KcSamlBrokerConfiguration.INSTANCE.setUpIdentityProvider();
idp.getConfig().put(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get());
idp.setFirstBrokerLoginFlowAlias(FLOW_AUTO_LINK);
final Creator<IdentityProviderResource> idpCreator = Creator.create(testRealm(), idp);
IdentityProviderMapperRepresentation samlNameIdMapper = new IdentityProviderMapperRepresentation();
samlNameIdMapper.setName("username-nameid-mapper");
idpAlias = idp.getAlias();
samlNameIdMapper.setIdentityProviderAlias(idpAlias);
samlNameIdMapper.setIdentityProviderMapper(UsernameTemplateMapper.PROVIDER_ID);
samlNameIdMapper.setConfig(ImmutableMap.<String,String>builder()
.put(IdentityProviderMapperModel.SYNC_MODE, "IMPORT")
.put(UsernameTemplateMapper.TEMPLATE, "${NAMEID | lowercase}")
.put(UsernameTemplateMapper.TARGET, Target.BROKER_ID.name())
.build());
idpCreator.resource().addMapper(samlNameIdMapper);
getCleanup().addCleanup(idpCreator);
}
@Before
public void setupClients() {
getCleanup().addCleanup(Creator.create(testRealm(), ClientBuilder.create()
.protocol(SamlProtocol.LOGIN_PROTOCOL)
.clientId(EXT_SSO_URL)
.baseUrl(EXT_SSO_URL)
.attribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME, EXT_SSO)
.attribute(SamlProtocol.SAML_NAME_ID_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())
.attribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, DUMMY_URL)
.build())
);
getCleanup().addCleanup(Creator.create(testRealm(), ClientBuilder.create()
.clientId(MY_APP)
.protocol(OIDCLoginProtocol.LOGIN_PROTOCOL)
.baseUrl(oauth.APP_AUTH_ROOT)
.build())
);
}
@After
public void cleanupUsers() {
testRealm().userStorage().removeImportedUsers(ldapModelId);
}
@Test
public void loginLDAPTest() {
loginPage.open();
loginPage.login(USER_NAME_LDAP, USER_PASSWORD);
appPage.assertCurrent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
appPage.logout();
}
protected URI getAuthServerBrokerSamlEndpoint(String realm, String identityProviderAlias, String samlClientId) throws IllegalArgumentException, UriBuilderException {
return RealmsResource
.realmBaseUrl(UriBuilder.fromUri(getAuthServerRoot()))
.path("broker/{idp-name}/endpoint/clients/{client-id}")
.build(realm, identityProviderAlias, samlClientId);
}
@Test
public void idpInitiatedMatchCaseLDAPTest() throws Exception {
testIdpInitiated(USER_NAME_LDAP, true);
}
@Test
public void idpInitiatedUpperCaseLDAPTest() throws Exception {
testIdpInitiated(USER_NAME_UPPERCASE, true);
}
@Test
public void idpInitiatedLowerCaseLDAPTest() throws Exception {
testIdpInitiated(USER_NAME_LOWERCASE, true);
}
@Test
public void idpInitiatedVaryingLetterCasesLDAPTest() throws Exception {
testIdpInitiated(USER_NAME_LDAP, true);
testIdpInitiated(USER_NAME_UPPERCASE, false);
testIdpInitiated(USER_NAME_LOWERCASE, false);
}
private void testIdpInitiated(String userName, boolean isFirstBrokerLogin) throws Exception {
final URI destination = getAuthServerBrokerSamlEndpoint(TEST_REALM_NAME, IDP_SAML_ALIAS, EXT_SSO);
ResponseType response = prepareResponseForIdPInitiatedFlow(destination, userName);
final SamlClientBuilder builder = new SamlClientBuilder()
// Create user session via IdP-initiated login
.submitSamlDocument(destination, response, Binding.POST)
.targetAttributeSamlResponse()
.build();
if (isFirstBrokerLogin) {
builder
// First-broker login
.followOneRedirect()
// After first-broker login
.followOneRedirect();
}
builder
// Do not truly process SAML POST response for a virtual IdP-initiated client, just check that no error was reported
.processSamlResponse(Binding.POST)
.transformObject(so -> {
assertThat(so, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
return null;
})
.build()
// Now navigate to the application where the session should already be created
.navigateTo(oauth.getLoginFormUrl())
.assertResponse(Matchers.bodyHC(containsString("AUTH_RESPONSE")))
.execute();
assertThat(testRealm().users().search(USER_NAME_LDAP, Boolean.TRUE), hasSize(1));
}
private ResponseType prepareResponseForIdPInitiatedFlow(final URI destination, String userName) throws ConfigurationException, ProcessingException {
// Prepare Response for IdP-initiated flow
return new SAML2LoginResponseBuilder()
.requestID(UUID.randomUUID().toString())
.destination(destination.toString())
.issuer(EXT_SSO_URL)
.requestIssuer(destination.toString())
.assertionExpiration(1000000)
.subjectExpiration(1000000)
.sessionIndex("idp:" + UUID.randomUUID())
.nameIdentifier(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(), userName)
.buildModel();
}
}

View file

@ -93,6 +93,13 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
.build(realm, SamlProtocol.LOGIN_PROTOCOL);
}
protected URI getAuthServerBrokerSamlEndpoint(String realm, String identityProviderAlias) throws IllegalArgumentException, UriBuilderException {
return RealmsResource
.realmBaseUrl(UriBuilder.fromUri(getAuthServerRoot()))
.path("broker/{idp-name}/endpoint")
.build(realm, identityProviderAlias);
}
protected URI getAuthServerRealmBase(String realm) throws IllegalArgumentException, UriBuilderException {
return RealmsResource
.realmBaseUrl(UriBuilder.fromUri(getAuthServerRoot()))

View file

@ -24,6 +24,8 @@
<module name="io.undertow.servlet"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-saml-core"/>
<module name="org.keycloak.keycloak-saml-core-public"/>
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.keycloak.keycloak-services" services="import"/>