KEYCLOAK-11029 Support modification of broker username / ID for identity provider linking
This commit is contained in:
parent
0362d3a430
commit
583fa07bc4
14 changed files with 571 additions and 117 deletions
|
@ -44,9 +44,7 @@ public class StackUtil {
|
||||||
* level, then returns stack trace, else returns empty {@link StringBuilder}
|
* level, then returns stack trace, else returns empty {@link StringBuilder}
|
||||||
*/
|
*/
|
||||||
public static StringBuilder getShortStackTrace(String prefix) {
|
public static StringBuilder getShortStackTrace(String prefix) {
|
||||||
if (! LOG.isTraceEnabled()) {
|
if (! isShortStackTraceEnabled()) return EMPTY;
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
StackTraceElement[] stackTrace = (new Throwable()).getStackTrace();
|
StackTraceElement[] stackTrace = (new Throwable()).getStackTrace();
|
||||||
|
@ -62,4 +60,8 @@ public class StackUtil {
|
||||||
}
|
}
|
||||||
return sb;
|
return sb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isShortStackTraceEnabled() {
|
||||||
|
return LOG.isTraceEnabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.broker.oidc.mappers;
|
||||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
|
import org.keycloak.broker.saml.mappers.UsernameTemplateMapper.Target;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderSyncMode;
|
import org.keycloak.models.IdentityProviderSyncMode;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -45,9 +46,15 @@ import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
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>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -83,10 +90,20 @@ public class UsernameTemplateMapper extends AbstractClaimMapper {
|
||||||
property = new ProviderConfigProperty();
|
property = new ProviderConfigProperty();
|
||||||
property.setName(TEMPLATE);
|
property.setName(TEMPLATE);
|
||||||
property.setLabel("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.setType(ProviderConfigProperty.STRING_TYPE);
|
||||||
property.setDefaultValue("${ALIAS}.${CLAIM.preferred_username}");
|
property.setDefaultValue("${ALIAS}.${CLAIM.preferred_username}");
|
||||||
configProperties.add(property);
|
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";
|
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) {
|
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.
|
// 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
|
// 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());
|
user.setUsername(context.getModelUsername());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
private static final Pattern SUBSTITUTION = Pattern.compile("\\$\\{([^}]+?)(?:\\s*\\|\\s*(\\S+)\\s*)?\\}");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
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) {
|
private void setUserNameFromTemplate(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||||
Matcher m = substitution.matcher(template);
|
Matcher m = SUBSTITUTION.matcher(template);
|
||||||
StringBuffer sb = new StringBuffer();
|
StringBuffer sb = new StringBuffer();
|
||||||
while (m.find()) {
|
while (m.find()) {
|
||||||
String variable = m.group(1);
|
String variable = m.group(1);
|
||||||
|
UnaryOperator<String> transformer = Optional.ofNullable(m.group(2)).map(TRANSFORMERS::get).orElse(UnaryOperator.identity());
|
||||||
|
|
||||||
if (variable.equals("ALIAS")) {
|
if (variable.equals("ALIAS")) {
|
||||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
m.appendReplacement(sb, transformer.apply(context.getIdpConfig().getAlias()));
|
||||||
} else if (variable.equals("UUID")) {
|
} else if (variable.equals("UUID")) {
|
||||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
m.appendReplacement(sb, transformer.apply(KeycloakModelUtils.generateId()));
|
||||||
} else if (variable.startsWith("CLAIM.")) {
|
} else if (variable.startsWith("CLAIM.")) {
|
||||||
String name = variable.substring("CLAIM.".length());
|
String name = variable.substring("CLAIM.".length());
|
||||||
Object value = AbstractClaimMapper.getClaimValue(context, name);
|
Object value = AbstractClaimMapper.getClaimValue(context, name);
|
||||||
if (value == null) value = "";
|
if (value == null) value = "";
|
||||||
m.appendReplacement(sb, value.toString());
|
m.appendReplacement(sb, transformer.apply(value.toString()));
|
||||||
} else {
|
} else {
|
||||||
m.appendReplacement(sb, m.group(1));
|
m.appendReplacement(sb, m.group(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
m.appendTail(sb);
|
m.appendTail(sb);
|
||||||
String username = sb.toString();
|
|
||||||
context.setModelUsername(username);
|
Target t = getTarget(mapperModel.getConfig().get(TARGET));
|
||||||
|
t.set(context, sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -36,9 +36,13 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -50,10 +54,20 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
|
||||||
|
|
||||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
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 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()));
|
private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
@ -61,10 +75,23 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
|
||||||
property = new ProviderConfigProperty();
|
property = new ProviderConfigProperty();
|
||||||
property.setName(TEMPLATE);
|
property.setName(TEMPLATE);
|
||||||
property.setLabel("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.setType(ProviderConfigProperty.STRING_TYPE);
|
||||||
property.setDefaultValue("${ALIAS}.${NAMEID}");
|
property.setDefaultValue("${ALIAS}.${NAMEID}");
|
||||||
configProperties.add(property);
|
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";
|
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) {
|
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.
|
// 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
|
// 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());
|
user.setUsername(context.getModelUsername());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
private static final Pattern SUBSTITUTION = Pattern.compile("\\$\\{([^}]+?)(?:\\s*\\|\\s*(\\S+)\\s*)?\\}");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
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) {
|
private void setUserNameFromTemplate(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||||
Matcher m = substitution.matcher(template);
|
Matcher m = SUBSTITUTION.matcher(template);
|
||||||
StringBuffer sb = new StringBuffer();
|
StringBuffer sb = new StringBuffer();
|
||||||
while (m.find()) {
|
while (m.find()) {
|
||||||
String variable = m.group(1);
|
String variable = m.group(1);
|
||||||
|
UnaryOperator<String> transformer = Optional.ofNullable(m.group(2)).map(TRANSFORMERS::get).orElse(UnaryOperator.identity());
|
||||||
|
|
||||||
if (variable.equals("ALIAS")) {
|
if (variable.equals("ALIAS")) {
|
||||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
m.appendReplacement(sb, transformer.apply(context.getIdpConfig().getAlias()));
|
||||||
} else if (variable.equals("UUID")) {
|
} else if (variable.equals("UUID")) {
|
||||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
m.appendReplacement(sb, transformer.apply(KeycloakModelUtils.generateId()));
|
||||||
} else if (variable.equals("NAMEID")) {
|
} else if (variable.equals("NAMEID")) {
|
||||||
SubjectType subject = assertion.getSubject();
|
SubjectType subject = assertion.getSubject();
|
||||||
SubjectType.STSubType subType = subject.getSubType();
|
SubjectType.STSubType subType = subject.getSubType();
|
||||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||||
m.appendReplacement(sb, subjectNameID.getValue());
|
m.appendReplacement(sb, transformer.apply(subjectNameID.getValue()));
|
||||||
} else if (variable.startsWith("ATTRIBUTE.")) {
|
} else if (variable.startsWith("ATTRIBUTE.")) {
|
||||||
String name = variable.substring("ATTRIBUTE.".length());
|
String name = variable.substring("ATTRIBUTE.".length());
|
||||||
String value = "";
|
String value = "";
|
||||||
|
@ -150,14 +179,16 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.appendReplacement(sb, value);
|
m.appendReplacement(sb, transformer.apply(value));
|
||||||
} else {
|
} else {
|
||||||
m.appendReplacement(sb, m.group(1));
|
m.appendReplacement(sb, m.group(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
m.appendTail(sb);
|
m.appendTail(sb);
|
||||||
context.setModelUsername(sb.toString());
|
|
||||||
|
Target t = getTarget(mapperModel.getConfig().get(TARGET));
|
||||||
|
t.set(context, sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -165,4 +196,12 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
|
||||||
return "Format the username to import.";
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.events.log;
|
package org.keycloak.events.log;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.StackUtil;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.events.Event;
|
import org.keycloak.events.Event;
|
||||||
import org.keycloak.events.EventListenerProvider;
|
import org.keycloak.events.EventListenerProvider;
|
||||||
|
@ -95,6 +96,10 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider
|
||||||
|
|
||||||
if(logger.isTraceEnabled()) {
|
if(logger.isTraceEnabled()) {
|
||||||
setKeycloakContext(sb);
|
setKeycloakContext(sb);
|
||||||
|
|
||||||
|
if (StackUtil.isShortStackTraceEnabled()) {
|
||||||
|
sb.append(", stackTrace=").append(StackUtil.getShortStackTrace());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(logger.isTraceEnabled() ? Logger.Level.TRACE : level, sb.toString());
|
logger.log(logger.isTraceEnabled() ? Logger.Level.TRACE : level, sb.toString());
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
<module name="org.bouncycastle"/>
|
<module name="org.bouncycastle"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
<module name="org.keycloak.keycloak-core"/>
|
<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"/>
|
||||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||||
<module name="org.keycloak.keycloak-services"/>
|
<module name="org.keycloak.keycloak-services"/>
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
package org.keycloak.testsuite.broker;
|
package org.keycloak.testsuite.broker;
|
||||||
|
|
||||||
class BrokerTestConstants {
|
public class BrokerTestConstants {
|
||||||
|
|
||||||
final static String REALM_PROV_NAME = "provider";
|
public final static String REALM_PROV_NAME = "provider";
|
||||||
final static String REALM_CONS_NAME = "consumer";
|
public final static String REALM_CONS_NAME = "consumer";
|
||||||
|
|
||||||
final static String IDP_OIDC_ALIAS = "kc-oidc-idp";
|
public final static String IDP_OIDC_ALIAS = "kc-oidc-idp";
|
||||||
final static String IDP_OIDC_PROVIDER_ID = "keycloak-oidc";
|
public final static String IDP_OIDC_PROVIDER_ID = "keycloak-oidc";
|
||||||
|
|
||||||
final static String IDP_SAML_ALIAS = "kc-saml-idp";
|
public final static String IDP_SAML_ALIAS = "kc-saml-idp";
|
||||||
final static String IDP_SAML_PROVIDER_ID = "saml";
|
public final static String IDP_SAML_PROVIDER_ID = "saml";
|
||||||
|
|
||||||
final static String CLIENT_ID = "brokerapp";
|
public final static String CLIENT_ID = "brokerapp";
|
||||||
final static String CLIENT_SECRET = "secret";
|
public final static String CLIENT_SECRET = "secret";
|
||||||
final static String VAULT_CLIENT_SECRET = "${vault.oidc_idp}";
|
public final static String VAULT_CLIENT_SECRET = "${vault.oidc_idp}";
|
||||||
|
|
||||||
final static String USER_LOGIN = "testuser";
|
public final static String USER_LOGIN = "testuser";
|
||||||
final static String USER_EMAIL = "user@localhost.com";
|
public final static String USER_EMAIL = "user@localhost.com";
|
||||||
final static String USER_PASSWORD = "password";
|
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" +
|
"DfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb4" +
|
||||||
"0tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SV" +
|
"0tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SV" +
|
||||||
"Er55KJoyQJQIDAQABAoGADaTtoG/+foOZUiLjRWKL/OmyavK9vjgyFtThNkZY4qHOh0h3" +
|
"Er55KJoyQJQIDAQABAoGADaTtoG/+foOZUiLjRWKL/OmyavK9vjgyFtThNkZY4qHOh0h3" +
|
||||||
|
@ -33,7 +33,7 @@ class BrokerTestConstants {
|
||||||
"kAC5loUGwU5dLaugsGH/a2Q8Ac8bmPglwfCstYDpl8Gp/eimb1eKyvDEELOhyImAv4/uZ" +
|
"kAC5loUGwU5dLaugsGH/a2Q8Ac8bmPglwfCstYDpl8Gp/eimb1eKyvDEELOhyImAv4/uZ" +
|
||||||
"V9wN85V0xZXWsw==";
|
"V9wN85V0xZXWsw==";
|
||||||
|
|
||||||
final static String IDP_SAML_SIGN_CERT = "MIIDdzCCAl+gAwIBAgIEbySuqTANBgkqhkiG" +
|
public final static String IDP_SAML_SIGN_CERT = "MIIDdzCCAl+gAwIBAgIEbySuqTANBgkqhkiG" +
|
||||||
"9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDV" +
|
"9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDV" +
|
||||||
"QQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDg" +
|
"QQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDg" +
|
||||||
"YDVQQDEwdVbmtub3duMB4XDTE1MDEyODIyMTYyMFoXDTE3MTAyNDIyMTYyMFowbDEQMA4" +
|
"YDVQQDEwdVbmtub3duMB4XDTE1MDEyODIyMTYyMFoXDTE3MTAyNDIyMTYyMFowbDEQMA4" +
|
||||||
|
@ -52,7 +52,7 @@ class BrokerTestConstants {
|
||||||
"KAXjMeHfzbiBr+cWz8NYZEtxUEDYDjTpKrYCSMJBXpmgVJCZ00BswbksxJwaGqGMPpUKm" +
|
"KAXjMeHfzbiBr+cWz8NYZEtxUEDYDjTpKrYCSMJBXpmgVJCZ00BswbksxJwaGqGMPpUKm" +
|
||||||
"CV671pf3m8nq3xyiHMDGuGwtbU+GE8kVx85menmp8+964nin";
|
"CV671pf3m8nq3xyiHMDGuGwtbU+GE8kVx85menmp8+964nin";
|
||||||
|
|
||||||
final static String REALM_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwg" +
|
public final static String REALM_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwg" +
|
||||||
"gSkAgEAAoIBAQCCPyvTTb14vSMkpe/pds2P5Cqxk7bkeFnQiNMS1vyZ+HS2O79fxzp1eA" +
|
"gSkAgEAAoIBAQCCPyvTTb14vSMkpe/pds2P5Cqxk7bkeFnQiNMS1vyZ+HS2O79fxzp1eA" +
|
||||||
"guHnBTs4XTRT7SZJhIT/6utgqZjmDigKV5N7X5ptq8BM/W1qa1cYBRip261pc+tWf3Iyw" +
|
"guHnBTs4XTRT7SZJhIT/6utgqZjmDigKV5N7X5ptq8BM/W1qa1cYBRip261pc+tWf3Iyw" +
|
||||||
"JYQ9yFI9mUQarmIEl0D7GH16NSZklheaWfbodRVarvX+ML0amNtGYVDft/RftYmgbKKrK" +
|
"JYQ9yFI9mUQarmIEl0D7GH16NSZklheaWfbodRVarvX+ML0amNtGYVDft/RftYmgbKKrK" +
|
||||||
|
@ -77,7 +77,7 @@ class BrokerTestConstants {
|
||||||
"spPC1kySiy+Ndr9jNohRZkR7pEjgqA5E8rdzc88LirUN7bY5HFHRWN9KXrs5/o3O1K3GF" +
|
"spPC1kySiy+Ndr9jNohRZkR7pEjgqA5E8rdzc88LirUN7bY5HFHRWN9KXrs5/o3O1K3GF" +
|
||||||
"Cp64N6nvnPEYZ2zSJalcMC2fjSsJg26z8Dg1H+gfTIDUMoGiEAAnJXuqk+WayPU+fZMLn";
|
"Cp64N6nvnPEYZ2zSJalcMC2fjSsJg26z8Dg1H+gfTIDUMoGiEAAnJXuqk+WayPU+fZMLn";
|
||||||
|
|
||||||
final static String REALM_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgK" +
|
public final static String REALM_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgK" +
|
||||||
"CAQEAgj8r0029eL0jJKXv6XbNj+QqsZO25HhZ0IjTEtb8mfh0tju/X8c6dXgILh5wU7OF0" +
|
"CAQEAgj8r0029eL0jJKXv6XbNj+QqsZO25HhZ0IjTEtb8mfh0tju/X8c6dXgILh5wU7OF0" +
|
||||||
"0U+0mSYSE/+rrYKmY5g4oCleTe1+abavATP1tamtXGAUYqdutaXPrVn9yMsCWEPchSPZlE" +
|
"0U+0mSYSE/+rrYKmY5g4oCleTe1+abavATP1tamtXGAUYqdutaXPrVn9yMsCWEPchSPZlE" +
|
||||||
"Gq5iBJdA+xh9ejUmZJYXmln26HUVWq71/jC9GpjbRmFQ37f0X7WJoGyiqyttfKkKfUeBmR" +
|
"Gq5iBJdA+xh9ejUmZJYXmln26HUVWq71/jC9GpjbRmFQ37f0X7WJoGyiqyttfKkKfUeBmR" +
|
||||||
|
|
|
@ -24,6 +24,8 @@ import org.keycloak.admin.client.resource.ComponentResource;
|
||||||
import org.keycloak.admin.client.resource.ComponentsResource;
|
import org.keycloak.admin.client.resource.ComponentsResource;
|
||||||
import org.keycloak.admin.client.resource.GroupResource;
|
import org.keycloak.admin.client.resource.GroupResource;
|
||||||
import org.keycloak.admin.client.resource.GroupsResource;
|
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.RealmResource;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.admin.client.resource.UsersResource;
|
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.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -39,7 +42,9 @@ import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.junit.Assert;
|
||||||
import static org.keycloak.testsuite.admin.ApiUtil.getCreatedId;
|
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 String id;
|
||||||
private final T resource;
|
private final T resource;
|
||||||
private final Runnable closer;
|
private final Runnable closer;
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.util;
|
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.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
import org.keycloak.testsuite.page.AbstractPage;
|
import org.keycloak.testsuite.page.AbstractPage;
|
||||||
import org.keycloak.testsuite.util.SamlClient.Binding;
|
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.UpdateProfileBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
|
import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.RequiredConsentBuilder;
|
import org.keycloak.testsuite.util.saml.RequiredConsentBuilder;
|
||||||
|
import java.util.function.Function;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
import org.apache.http.HttpResponse;
|
|
||||||
import org.apache.http.client.methods.HttpUriRequest;
|
import org.apache.http.client.methods.HttpUriRequest;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import static org.hamcrest.Matchers.notNullValue;
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
import static org.keycloak.testsuite.util.saml.SamlDocumentStepBuilder.saml2Object2String;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -114,6 +116,10 @@ public class SamlClientBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <T> T andThen(Function<SamlClientBuilder, T> next) {
|
||||||
|
return next.apply(this);
|
||||||
|
}
|
||||||
|
|
||||||
public SamlClientBuilder assertResponse(Matcher<? super CloseableHttpResponse> matcher) {
|
public SamlClientBuilder assertResponse(Matcher<? super CloseableHttpResponse> matcher) {
|
||||||
steps.add((client, currentURI, currentResponse, context) -> {
|
steps.add((client, currentURI, currentResponse, context) -> {
|
||||||
Assert.assertThat(currentResponse, matcher);
|
Assert.assertThat(currentResponse, matcher);
|
||||||
|
@ -164,6 +170,22 @@ public class SamlClientBuilder {
|
||||||
return addStepBuilder(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
|
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 */
|
/** Handles login page */
|
||||||
public LoginBuilder login() {
|
public LoginBuilder login() {
|
||||||
return addStepBuilder(new LoginBuilder(this));
|
return addStepBuilder(new LoginBuilder(this));
|
||||||
|
|
|
@ -29,6 +29,8 @@ import java.net.URISyntaxException;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.http.NameValuePair;
|
import org.apache.http.NameValuePair;
|
||||||
|
@ -62,6 +64,7 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
|
||||||
private URI targetUri;
|
private URI targetUri;
|
||||||
private String targetAttribute;
|
private String targetAttribute;
|
||||||
private Binding targetBinding;
|
private Binding targetBinding;
|
||||||
|
private Supplier<String> documentSupplier;
|
||||||
|
|
||||||
public ModifySamlResponseStepBuilder(Binding binding, SamlClientBuilder clientBuilder) {
|
public ModifySamlResponseStepBuilder(Binding binding, SamlClientBuilder clientBuilder) {
|
||||||
super(clientBuilder);
|
super(clientBuilder);
|
||||||
|
@ -83,6 +86,15 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
|
||||||
throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
|
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() {
|
public Binding targetBinding() {
|
||||||
return targetBinding;
|
return targetBinding;
|
||||||
}
|
}
|
||||||
|
@ -119,86 +131,108 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
|
||||||
}
|
}
|
||||||
|
|
||||||
protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException {
|
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));
|
if (documentSupplier != null) {
|
||||||
String location = currentResponse.getFirstHeader("Location").getValue();
|
Objects.requireNonNull(this.targetUri, "Set targetUri");
|
||||||
URI locationUri = URI.create(location);
|
Objects.requireNonNull(this.targetAttribute, "Set targetAttribute");
|
||||||
|
|
||||||
List<NameValuePair> params = URLEncodedUtils.parse(locationUri, "UTF-8");
|
samlDoc = documentSupplier.get();
|
||||||
for (Iterator<NameValuePair> it = params.iterator(); it.hasNext();) {
|
uri = this.targetUri;
|
||||||
NameValuePair param = it.next();
|
attrName = this.targetAttribute;
|
||||||
if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
|
params = new LinkedList<>();
|
||||||
assertThat("Only one SAMLRequest/SAMLResponse check", samlParam, nullValue());
|
} else {
|
||||||
samlParam = param;
|
NameValuePair samlParam = null;
|
||||||
it.remove();
|
|
||||||
|
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());
|
return createRequest(uri, attrName, samlDoc, params);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpUriRequest handlePostBinding(CloseableHttpResponse currentResponse) throws Exception {
|
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());
|
if (documentSupplier != null) {
|
||||||
assertThat(htmlBody, Matchers.containsString("SAML"));
|
Objects.requireNonNull(this.targetUri, "Set targetUri");
|
||||||
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(htmlBody);
|
Objects.requireNonNull(this.targetAttribute, "Set targetAttribute");
|
||||||
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();
|
samlDoc = documentSupplier.get();
|
||||||
Element form = forms.first();
|
uri = this.targetUri;
|
||||||
|
attrName = this.targetAttribute;
|
||||||
|
} else {
|
||||||
|
assertThat(currentResponse, statusCodeIsHC(Status.OK));
|
||||||
|
|
||||||
String base64EncodedSamlDoc = respElement.val();
|
final String htmlBody = EntityUtils.toString(currentResponse.getEntity());
|
||||||
InputStream decoded = PostBindingUtil.base64DecodeAsStream(base64EncodedSamlDoc);
|
assertThat(htmlBody, Matchers.containsString("SAML"));
|
||||||
String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
|
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(htmlBody);
|
||||||
IOUtils.closeQuietly(decoded);
|
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);
|
String transformed = getTransformer().transform(samlDoc);
|
||||||
if (transformed == null) {
|
if (transformed == null) {
|
||||||
return 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) {
|
switch (this.targetBinding) {
|
||||||
case POST:
|
case POST:
|
||||||
return createPostRequest(locationUri, attributeName, transformed, parameters);
|
return createPostRequest(locationUri, attributeName, transformed, parameters);
|
||||||
|
|
|
@ -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.ResponseType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
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.DocumentUtil;
|
||||||
import org.keycloak.saml.common.util.StaxUtil;
|
import org.keycloak.saml.common.util.StaxUtil;
|
||||||
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
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.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.logging.Level;
|
||||||
import javax.xml.stream.XMLStreamWriter;
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -97,9 +99,18 @@ public abstract class SamlDocumentStepBuilder<T extends SAML2Object, This extend
|
||||||
return null;
|
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();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
|
XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
|
||||||
|
|
||||||
if (transformed instanceof AuthnRequestType) {
|
if (transformed instanceof AuthnRequestType) {
|
||||||
new SAMLRequestWriter(xmlStreamWriter).write((AuthnRequestType) transformed);
|
new SAMLRequestWriter(xmlStreamWriter).write((AuthnRequestType) transformed);
|
||||||
} else if (transformed instanceof LogoutRequestType) {
|
} 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.assertNotNull("Unknown type: <null>", transformed);
|
||||||
Assert.fail("Unknown type: " + transformed.getClass().getName());
|
Assert.fail("Unknown type: " + transformed.getClass().getName());
|
||||||
}
|
}
|
||||||
String res = new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET);
|
return new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET);
|
||||||
LOG.debugf(" ---> %s", res);
|
} catch (ProcessingException ex) {
|
||||||
return res;
|
throw new RuntimeException(ex);
|
||||||
};
|
}
|
||||||
return (This) this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public This transformDocument(Consumer<Document> tr) {
|
public This transformDocument(Consumer<Document> tr) {
|
||||||
|
|
|
@ -106,12 +106,8 @@ public abstract class AbstractLDAPTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
|
||||||
protected ComponentRepresentation findMapperRepByName(String name) {
|
protected ComponentRepresentation findMapperRepByName(String name) {
|
||||||
List<ComponentRepresentation> mappers = testRealm().components().query(ldapModelId, LDAPStorageMapper.class.getName());
|
return testRealm().components().query(ldapModelId, LDAPStorageMapper.class.getName()).stream()
|
||||||
for (ComponentRepresentation mapper : mappers) {
|
.filter(mapper -> mapper.getName().equals(name))
|
||||||
if (mapper.getName().equals(name)) {
|
.findAny().orElse(null);
|
||||||
return mapper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -93,6 +93,13 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
||||||
.build(realm, SamlProtocol.LOGIN_PROTOCOL);
|
.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 {
|
protected URI getAuthServerRealmBase(String realm) throws IllegalArgumentException, UriBuilderException {
|
||||||
return RealmsResource
|
return RealmsResource
|
||||||
.realmBaseUrl(UriBuilder.fromUri(getAuthServerRoot()))
|
.realmBaseUrl(UriBuilder.fromUri(getAuthServerRoot()))
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
<module name="io.undertow.servlet"/>
|
<module name="io.undertow.servlet"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
<module name="org.keycloak.keycloak-core"/>
|
<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"/>
|
||||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||||
<module name="org.keycloak.keycloak-services" services="import"/>
|
<module name="org.keycloak.keycloak-services" services="import"/>
|
||||||
|
|
Loading…
Reference in a new issue