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} * 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();
}
} }

View file

@ -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

View file

@ -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;
}
}
} }

View file

@ -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());

View file

@ -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"/>

View file

@ -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" +

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.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;

View file

@ -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));

View file

@ -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);

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.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) {

View file

@ -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;
} }
} }

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); .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()))

View file

@ -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"/>