From 48b115a4bbce1f8da5526e0f73a964e1500e64e1 Mon Sep 17 00:00:00 2001 From: girirajsharma Date: Sat, 14 Feb 2015 15:37:25 +0530 Subject: [PATCH 1/7] Minor changes : Fixed spell/grammatical errors in docbook. --- .../reference/en/en-US/modules/adapter-config.xml | 4 ++-- .../reference/en/en-US/modules/admin-rest-api.xml | 2 +- docbook/reference/en/en-US/modules/events.xml | 2 +- .../en/en-US/modules/javascript-adapter.xml | 4 ++-- docbook/reference/en/en-US/modules/multi-tenancy.xml | 6 +++--- docbook/reference/en/en-US/modules/providers.xml | 12 +++++------- .../en/en-US/modules/server-installation.xml | 6 +++--- docbook/reference/en/en-US/modules/timeouts.xml | 2 +- .../reference/en/en-US/modules/user-federation.xml | 2 +- 9 files changed, 19 insertions(+), 21 deletions(-) diff --git a/docbook/reference/en/en-US/modules/adapter-config.xml b/docbook/reference/en/en-US/modules/adapter-config.xml index a9d4e08a16..d4ef6423c3 100755 --- a/docbook/reference/en/en-US/modules/adapter-config.xml +++ b/docbook/reference/en/en-US/modules/adapter-config.xml @@ -38,7 +38,7 @@ Some of these configuration switches may be adapter specific and some are common across all adapters. For Java adapters you can use ${...} enclosure as System property replacement. For example ${jboss.server.config.dir}. Also, you can obtain a template - for this config file from the admin console. Go to the realm and application you want a template for. + for this config file from the admin console. Go to the realm and select the application you want a template for. Go to the Installation tab and this will provide you with a template that includes the public key of the realm. @@ -348,7 +348,7 @@ If true, then adapter will send registration request to Keycloak. It's false - by default as useful just in cluster (See Registration of application nodes to Keycloak) + by default and useful just in cluster (See Registration of application nodes to Keycloak) diff --git a/docbook/reference/en/en-US/modules/admin-rest-api.xml b/docbook/reference/en/en-US/modules/admin-rest-api.xml index 74533cd4b5..56d5ccd63e 100755 --- a/docbook/reference/en/en-US/modules/admin-rest-api.xml +++ b/docbook/reference/en/en-US/modules/admin-rest-api.xml @@ -3,7 +3,7 @@ The Keycloak Admin Console is implemented entirely with a fully functional REST admin API. You can invoke this REST API from your Java applications by obtaining an access token. You must have the appropriate - permissions set up as describe in and + permissions set up as described in and The documentation for this REST API is auto-generated and is contained in the distribution of keycloak under diff --git a/docbook/reference/en/en-US/modules/events.xml b/docbook/reference/en/en-US/modules/events.xml index e580448d62..57cac03ded 100755 --- a/docbook/reference/en/en-US/modules/events.xml +++ b/docbook/reference/en/en-US/modules/events.xml @@ -40,7 +40,7 @@
Event Listener - Keycloak comes with an Email Event Listener and a JBogg Logging Event Listener. The Email Event Listener + Keycloak comes with an Email Event Listener and a JBoss Logging Event Listener. The Email Event Listener sends an email to the users account when an event occurs. The JBoss Logging Event Listener writes to a log file when an events occurs. diff --git a/docbook/reference/en/en-US/modules/javascript-adapter.xml b/docbook/reference/en/en-US/modules/javascript-adapter.xml index c20856210c..80f04ede87 100755 --- a/docbook/reference/en/en-US/modules/javascript-adapter.xml +++ b/docbook/reference/en/en-US/modules/javascript-adapter.xml @@ -206,7 +206,7 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp' Options is an Object, where: redirectUri - specifies the uri to redirect to after login - prompt - can be set to 'none' to check if the user is logged in already (if not logged in a login form is not displayed) + prompt - can be set to 'none' to check if the user is logged in already (if not logged in, a login form is not displayed) loginHint - used to pre-fill the username/email field on the login form @@ -218,7 +218,7 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp' Options is an Object, where: redirectUri - specifies the uri to redirect to after login - prompt - can be set to 'none' to check if the user is logged in already (if not logged in a login form is not displayed) + prompt - can be set to 'none' to check if the user is logged in already (if not logged in, a login form is not displayed) diff --git a/docbook/reference/en/en-US/modules/multi-tenancy.xml b/docbook/reference/en/en-US/modules/multi-tenancy.xml index 410621f049..e7b385ee8c 100644 --- a/docbook/reference/en/en-US/modules/multi-tenancy.xml +++ b/docbook/reference/en/en-US/modules/multi-tenancy.xml @@ -9,7 +9,7 @@ The same WAR file deployed under two different names, each with its own Keycloak configuration (probably via the Keycloak Subsystem). This scenario is suitable when the number of realms is known in advance or when there's a dynamic provision of application instances. - One example would be a service provider that dinamically creates servers/deployments for their clients, like a PaaS. + One example would be a service provider that dynamically creates servers/deployments for their clients, like a PaaS. @@ -37,7 +37,7 @@ Add a context parameter to the web.xml, named keycloak.config.resolver. - The value of this property should be the fully qualified name of the a class extending + The value of this property should be the fully qualified name of the class extending org.keycloak.adapters.KeycloakConfigResolver. @@ -51,6 +51,6 @@ - An implementation of this feature can be found on the examples. + An implementation of this feature can be found in the examples.
diff --git a/docbook/reference/en/en-US/modules/providers.xml b/docbook/reference/en/en-US/modules/providers.xml index 62b5b8e3d8..4e2239dcbd 100755 --- a/docbook/reference/en/en-US/modules/providers.xml +++ b/docbook/reference/en/en-US/modules/providers.xml @@ -80,9 +80,7 @@ public class MyEventListenerProvider implements EventListenerProvider { The file META-INF/services/org.keycloak.events.EventListenerProviderFactory should contain the full name of your ProviderFactory implementation: - + @@ -142,7 +140,7 @@ org.acme.provider.MyEventListenerProviderFactory Connections Jpa - Loads and configures Infinispan connections. The default implementation can load datasources + Loads and configures Jpa connections. The default implementation can load datasources from WildFly/EAP, or alternatively can be manually configured in keycloak-server.json. @@ -188,15 +186,15 @@ org.acme.provider.MyEventListenerProviderFactory Exports the Keycloak database. Keycloak provides implementations that export to JSON files either - as a single file, multiple file in a directory or a encrypted ZIP archive. + as a single file, multiple files in a directory or a encrypted ZIP archive. Import - Imports and exported Keycloak database. Keycloak provides implementations that import from JSON + Imports an exported Keycloak database. Keycloak provides implementations that import from JSON files either - as a single file, multiple file in a directory or a encrypted ZIP archive. + as a single file, multiple files in a directory or a encrypted ZIP archive. diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml index a4b5455fe3..35d231db30 100755 --- a/docbook/reference/en/en-US/modules/server-installation.xml +++ b/docbook/reference/en/en-US/modules/server-installation.xml @@ -94,8 +94,8 @@ keycloak-war-dist-all-&project.version;/ $ cd keycloak-war-dist-all-&project.version; - $ cp -r deployments $JBOSS_HOME/standalone - $ cp -r configuration $JBOSS_HOME/standalone + $ cp -r deployments $JBOSS_HOME/standalone/deployments + $ cp -r configuration $JBOSS_HOME/standalone/configuration @@ -515,7 +515,7 @@ keycloak-war-dist-all-&project.version;/ - You should answer the What is your first and last name? question with + You should answer What is your first and last name ? question with the DNS name of the machine you're installing the server on. For testing purposes, localhost should be used. After executing this command, the keycloak.jks file will be generated in the same directory as you executed diff --git a/docbook/reference/en/en-US/modules/timeouts.xml b/docbook/reference/en/en-US/modules/timeouts.xml index 62d72a6ade..ef410199a6 100755 --- a/docbook/reference/en/en-US/modules/timeouts.xml +++ b/docbook/reference/en/en-US/modules/timeouts.xml @@ -11,7 +11,7 @@ If you go to the admin console page of Settings->General, you should see a Remember Me on/off switch. Your realm sets a SSO cookie so that you only have to enter in your login credentials once. This Remember Me admin config option, when turned on, will show a "Remember Me" checkbox on the user's login page. - If the user clicks this, the realm's SSO. cookie will be persistent. This means that if the user closes their browser + If the user clicks this, the realm's SSO cookie will be persistent. This means that if the user closes their browser they will still be logged in the next time they start up their browser. diff --git a/docbook/reference/en/en-US/modules/user-federation.xml b/docbook/reference/en/en-US/modules/user-federation.xml index f1ba3babf3..777f4b71fe 100755 --- a/docbook/reference/en/en-US/modules/user-federation.xml +++ b/docbook/reference/en/en-US/modules/user-federation.xml @@ -155,7 +155,7 @@ The keycloak examples directory contains an example of a simple User Federation Provider backed by a simple properties file. See examples/providers/federation-provider. Most of how - to create a federation provider is explain directly within the example code, but some information is here too. + to create a federation provider is explained directly within the example code, but some information is here too. Writing a User Federation Provider starts by implementing the UserFederationProvider From 5da05aa62ac088d61bfb78b1c651e2251907746f Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 13 Feb 2015 21:05:17 +0100 Subject: [PATCH 2/7] LDAP testing improvements. Support for embedded Kerberos server in testsuite --- pom.xml | 36 ++-- .../managers/LDAPConnectionTestManager.java | 3 +- testsuite/integration/README.md | 30 +++ testsuite/integration/pom.xml | 64 +++++- .../keycloak/testutils/KeycloakServer.java | 19 -- .../ldap/EmbeddedServersFactory.java | 82 ++++++++ .../ldap/InMemoryDirectoryServiceFactory.java | 144 ++++++++++++++ .../ldap/InMemorySchemaPartition.java | 75 +++++++ .../ldap/KerberosEmbeddedServer.java | 183 +++++++++++++++++ .../testutils/ldap/KerberosKeytabCreator.java | 70 +++++++ .../LDAPConfiguration.java} | 117 +---------- .../testutils/ldap/LDAPEmbeddedServer.java | 186 ++++++++++++++++++ .../src/main/resources/kerberos/http.keytab | Bin 0 -> 266 bytes .../resources/kerberos/users-kerberos.ldif | 88 +++++++++ .../src/main/resources/log4j.properties | 7 +- .../FederationProvidersIntegrationTest.java | 4 +- .../testsuite/forms/SyncProvidersTest.java | 4 +- .../org/keycloak/testsuite/rule/LDAPRule.java | 33 ++-- 18 files changed, 976 insertions(+), 169 deletions(-) create mode 100644 testsuite/integration/src/main/java/org/keycloak/testutils/ldap/EmbeddedServersFactory.java create mode 100644 testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemoryDirectoryServiceFactory.java create mode 100644 testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemorySchemaPartition.java create mode 100644 testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java create mode 100644 testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosKeytabCreator.java rename testsuite/integration/src/main/java/org/keycloak/testutils/{LDAPEmbeddedServer.java => ldap/LDAPConfiguration.java} (66%) mode change 100755 => 100644 create mode 100644 testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java create mode 100644 testsuite/integration/src/main/resources/kerberos/http.keytab create mode 100644 testsuite/integration/src/main/resources/kerberos/users-kerberos.ldif diff --git a/pom.xml b/pom.xml index 4aada1a49c..d43ce03c7d 100755 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,8 @@ 0.33.12 + 2.0.0-M17 + 1.0.0-M23 2.3.8 1.50 1.50 @@ -23,7 +25,6 @@ 1.1.1.Final 2.7.0.CR3 - 1.0.2.Final 2.11.3 3.1.4.GA 0.9.30 @@ -307,17 +308,6 @@ picketlink-impl ${picketlink.version} - - org.picketbox - picketbox-ldap - ${picketbox.ldap.version} - - - org.picketbox - picketbox-ldap - ${picketbox.ldap.version} - test-jar - org.jboss.logging jboss-logging @@ -422,6 +412,28 @@ ${winzipaes.version} + + + org.apache.directory.server + apacheds-core-annotations + ${apacheds.version} + + + org.apache.directory.server + apacheds-interceptor-kerberos + ${apacheds.version} + + + org.apache.directory.server + apacheds-server-annotations + ${apacheds.version} + + + org.apache.directory.api + api-ldap-codec-standalone + ${apacheds.codec.version} + + org.seleniumhq.selenium diff --git a/services/src/main/java/org/keycloak/services/managers/LDAPConnectionTestManager.java b/services/src/main/java/org/keycloak/services/managers/LDAPConnectionTestManager.java index 2b78406e8c..fff0bfb3b1 100755 --- a/services/src/main/java/org/keycloak/services/managers/LDAPConnectionTestManager.java +++ b/services/src/main/java/org/keycloak/services/managers/LDAPConnectionTestManager.java @@ -27,11 +27,10 @@ public class LDAPConnectionTestManager { try { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(Context.SECURITY_AUTHENTICATION, "simple"); - env.put(Context.PROVIDER_URL, connectionUrl); if (TEST_AUTHENTICATION.equals(action)) { + env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, bindDn); char[] bindCredentialChar = null; diff --git a/testsuite/integration/README.md b/testsuite/integration/README.md index 9de055c154..a8d06711ce 100644 --- a/testsuite/integration/README.md +++ b/testsuite/integration/README.md @@ -90,4 +90,34 @@ To configure Keycloak to use the above server add the following system propertie For example if using the test utils Keycloak server start it with: mvn exec:java -Pkeycloak-server -Dkeycloak.mail.smtp.from=auto@keycloak.org -Dkeycloak.mail.smtp.host=localhost -Dkeycloak.mail.smtp.port=3025 + +LDAP server +----------- + +To start a ApacheDS based LDAP server for testing LDAP sending run: + + mvn exec:java -Pldap + +There are additional system properties you can use to configure (See EmbeddedServersFactory class for details). Once done, you can create LDAP Federation provider +in Keycloak admin console with the settings like: +Vendor: Other +Connection URL: ldap://localhost:10389 +Base DN: dc=keycloak,dc=org +User DN Suffix: ou=People,dc=keycloak,dc=org +Bind DN: uid=admin,ou=system +Bind credential: secret + +Kerberos server +--------------- + +To start a ApacheDS based Kerberos server for testing Kerberos + LDAP sending run: + + mvn exec:java -Pkerberos + +There are additional system properties you can use to configure (See EmbeddedServersFactory class for details). Once done, you can create LDAP Federation provider +in Keycloak admin console with same settings like mentioned in previous LDAP section. And you can enable Kerberos with the settings like: + +Server Principal: HTTP/localhost@KEYCLOAK.ORG +KeyTab: $KEYCLOAK_SOURCES/testsuite/integration/src/main/resources/kerberos/http.keytab + diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 5800d98df6..98697e5497 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -113,10 +113,6 @@ com.google.zxing javase - - org.bouncycastle - bcprov-jdk15on - org.apache.httpcomponents httpclient @@ -212,15 +208,31 @@ org.seleniumhq.selenium selenium-chrome-driver + + - org.picketbox - picketbox-ldap - test-jar + org.apache.directory.server + apacheds-core-annotations + + + org.slf4j + slf4j-log4j12 + + - org.picketbox - picketbox-ldap + org.apache.directory.server + apacheds-interceptor-kerberos + + org.apache.directory.server + apacheds-server-annotations + + + org.apache.directory.api + api-ldap-codec-standalone + + org.picketlink picketlink-wildfly-common @@ -270,6 +282,12 @@ ${project.basedir} + + org.apache.felix + maven-bundle-plugin + true + true + @@ -316,6 +334,34 @@ + + ldap + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.ldap.LDAPEmbeddedServer + + + + + + + kerberos + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.ldap.KerberosEmbeddedServer + + + + + jpa diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java index 9b0c200e20..d5e1c375a6 100755 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java @@ -104,27 +104,8 @@ public class KeycloakServer { } public static void main(String[] args) throws Throwable { - //bootstrapLdap(); Can't seem to get this to work. bootstrapKeycloakServer(args); } - /*private static LDAPEmbeddedServer embeddedServer; - public static void bootstrapLdap() throws Exception { - embeddedServer = new LDAPEmbeddedServer(); - embeddedServer.setup(); - embeddedServer.importLDIF("ldap/users.ldif"); - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - try { - embeddedServer.tearDown(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }); - - - } */ public static KeycloakServer bootstrapKeycloakServer(String[] args) throws Throwable { KeycloakServerConfig config = new KeycloakServerConfig(); diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/EmbeddedServersFactory.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/EmbeddedServersFactory.java new file mode 100644 index 0000000000..a377d9b954 --- /dev/null +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/EmbeddedServersFactory.java @@ -0,0 +1,82 @@ +package org.keycloak.testutils.ldap; + +/** + * Factory for ApacheDS based LDAP and Kerberos servers + * + * @author Marek Posolda + */ +public class EmbeddedServersFactory { + + private static final String DEFAULT_BASE_DN = "dc=keycloak,dc=org"; + private static final String DEFAULT_BIND_HOST = "localhost"; + private static final int DEFAULT_BIND_PORT = 10389; + private static final String DEFAULT_LDIF_FILE = "ldap/users.ldif"; + + private static final String DEFAULT_KERBEROS_LDIF_FILE = "kerberos/users-kerberos.ldif"; + + private static final String DEFAULT_KERBEROS_REALM = "KEYCLOAK.ORG"; + private static final int DEFAULT_KDC_PORT = 6088; + + private String baseDN; + private String bindHost; + private int bindPort; + private String ldifFile; + private String kerberosRealm; + private int kdcPort; + + + public static EmbeddedServersFactory readConfiguration() { + EmbeddedServersFactory factory = new EmbeddedServersFactory(); + factory.readProperties(); + return factory; + } + + + protected void readProperties() { + this.baseDN = System.getProperty("ldap.baseDN"); + this.bindHost = System.getProperty("ldap.host"); + String bindPort = System.getProperty("ldap.port"); + this.ldifFile = System.getProperty("ldap.ldif"); + + this.kerberosRealm = System.getProperty("kerberos.realm"); + String kdcPort = System.getProperty("kerberos.port"); + + if (baseDN == null || baseDN.isEmpty()) { + baseDN = DEFAULT_BASE_DN; + } + if (bindHost == null || bindHost.isEmpty()) { + bindHost = DEFAULT_BIND_HOST; + } + this.bindPort = (bindPort == null || bindPort.isEmpty()) ? DEFAULT_BIND_PORT : Integer.parseInt(bindPort); + if (ldifFile == null || ldifFile.isEmpty()) { + ldifFile = DEFAULT_LDIF_FILE; + } + + if (kerberosRealm == null || kerberosRealm.isEmpty()) { + kerberosRealm = DEFAULT_KERBEROS_REALM; + } + this.kdcPort = (kdcPort == null || kdcPort.isEmpty()) ? DEFAULT_KDC_PORT : Integer.parseInt(kdcPort); + } + + + public LDAPEmbeddedServer createLdapServer() { + + // Override LDIF file with default for embedded LDAP + if (ldifFile.equals(DEFAULT_KERBEROS_LDIF_FILE)) { + ldifFile = DEFAULT_LDIF_FILE; + } + + return new LDAPEmbeddedServer(baseDN, bindHost, bindPort, ldifFile); + } + + + public KerberosEmbeddedServer createKerberosServer() { + + // Override LDIF file with default for embedded Kerberos + if (ldifFile.equals(DEFAULT_LDIF_FILE)) { + ldifFile = DEFAULT_KERBEROS_LDIF_FILE; + } + + return new KerberosEmbeddedServer(baseDN, bindHost, bindPort, ldifFile, kerberosRealm, kdcPort); + } +} diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemoryDirectoryServiceFactory.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemoryDirectoryServiceFactory.java new file mode 100644 index 0000000000..bba4954a3f --- /dev/null +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemoryDirectoryServiceFactory.java @@ -0,0 +1,144 @@ +package org.keycloak.testutils.ldap; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.Configuration; +import org.apache.commons.io.FileUtils; +import org.apache.directory.api.ldap.model.constants.SchemaConstants; +import org.apache.directory.api.ldap.model.schema.LdapComparator; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.api.ldap.model.schema.comparators.NormalizingComparator; +import org.apache.directory.api.ldap.model.schema.registries.ComparatorRegistry; +import org.apache.directory.api.ldap.model.schema.registries.SchemaLoader; +import org.apache.directory.api.ldap.schemaloader.JarLdifSchemaLoader; +import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager; +import org.apache.directory.api.util.exception.Exceptions; +import org.apache.directory.server.constants.ServerDNConstants; +import org.apache.directory.server.core.DefaultDirectoryService; +import org.apache.directory.server.core.api.CacheService; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.InstanceLayout; +import org.apache.directory.server.core.api.partition.Partition; +import org.apache.directory.server.core.api.schema.SchemaPartition; +import org.apache.directory.server.core.factory.AvlPartitionFactory; +import org.apache.directory.server.core.factory.DirectoryServiceFactory; +import org.apache.directory.server.core.factory.PartitionFactory; +import org.apache.directory.server.i18n.I18n; +import org.jboss.logging.Logger; + +/** + * Factory for a fast (mostly in-memory-only) ApacheDS DirectoryService. Use only for tests!! + * + * @author Josef Cacek + */ +class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory { + + private static final Logger log = Logger.getLogger(InMemoryDirectoryServiceFactory.class); + + private final DirectoryService directoryService; + private final PartitionFactory partitionFactory; + + /** + * Default constructor which creates {@link DefaultDirectoryService} instance and configures {@link AvlPartitionFactory} as + * the {@link PartitionFactory} implementation. + */ + public InMemoryDirectoryServiceFactory() { + try { + directoryService = new DefaultDirectoryService(); + } catch (Exception e) { + throw new RuntimeException(e); + } + directoryService.setShutdownHookEnabled(false); + partitionFactory = new AvlPartitionFactory(); + } + + /** + * Constructor which uses provided {@link DirectoryService} and {@link PartitionFactory} implementations. + * + * @param directoryService must be not-null + * @param partitionFactory must be not-null + */ + public InMemoryDirectoryServiceFactory(DirectoryService directoryService, PartitionFactory partitionFactory) { + this.directoryService = directoryService; + this.partitionFactory = partitionFactory; + } + + /** + * {@inheritDoc} + */ + public void init(String name) throws Exception { + if ((directoryService != null) && directoryService.isStarted()) { + return; + } + directoryService.setInstanceId(name); + + // instance layout + InstanceLayout instanceLayout = new InstanceLayout(System.getProperty("java.io.tmpdir") + "/server-work-" + name); + if (instanceLayout.getInstanceDirectory().exists()) { + try { + FileUtils.deleteDirectory(instanceLayout.getInstanceDirectory()); + } catch (IOException e) { + log.warn("couldn't delete the instance directory before initializing the DirectoryService", e); + } + } + directoryService.setInstanceLayout(instanceLayout); + + // EhCache in disabled-like-mode + Configuration ehCacheConfig = new Configuration(); + CacheConfiguration defaultCache = new CacheConfiguration("default", 1).eternal(false).timeToIdleSeconds(30) + .timeToLiveSeconds(30).overflowToDisk(false); + ehCacheConfig.addDefaultCache(defaultCache); + CacheService cacheService = new CacheService(new CacheManager(ehCacheConfig)); + directoryService.setCacheService(cacheService); + + // Init the schema + // SchemaLoader loader = new SingleLdifSchemaLoader(); + SchemaLoader loader = new JarLdifSchemaLoader(); + SchemaManager schemaManager = new DefaultSchemaManager(loader); + schemaManager.loadAllEnabled(); + ComparatorRegistry comparatorRegistry = schemaManager.getComparatorRegistry(); + for (LdapComparator comparator : comparatorRegistry) { + if (comparator instanceof NormalizingComparator) { + ((NormalizingComparator) comparator).setOnServer(); + } + } + directoryService.setSchemaManager(schemaManager); + InMemorySchemaPartition inMemorySchemaPartition = new InMemorySchemaPartition(schemaManager); + SchemaPartition schemaPartition = new SchemaPartition(schemaManager); + schemaPartition.setWrappedPartition(inMemorySchemaPartition); + directoryService.setSchemaPartition(schemaPartition); + List errors = schemaManager.getErrors(); + if (errors.size() != 0) { + throw new Exception(I18n.err(I18n.ERR_317, Exceptions.printErrors(errors))); + } + + // Init system partition + Partition systemPartition = partitionFactory.createPartition(directoryService.getSchemaManager(), + directoryService.getDnFactory(), "system", + ServerDNConstants.SYSTEM_DN, 500, + new File(directoryService.getInstanceLayout().getPartitionsDirectory(), + "system")); + + systemPartition.setSchemaManager(directoryService.getSchemaManager()); + partitionFactory.addIndex(systemPartition, SchemaConstants.OBJECT_CLASS_AT, 100); + directoryService.setSystemPartition(systemPartition); + directoryService.startup(); + } + + /** + * {@inheritDoc} + */ + public DirectoryService getDirectoryService() throws Exception { + return directoryService; + } + + /** + * {@inheritDoc} + */ + public PartitionFactory getPartitionFactory() throws Exception { + return partitionFactory; + } +} \ No newline at end of file diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemorySchemaPartition.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemorySchemaPartition.java new file mode 100644 index 0000000000..a5fed44ec7 --- /dev/null +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/InMemorySchemaPartition.java @@ -0,0 +1,75 @@ +package org.keycloak.testutils.ldap; + +import java.net.URL; +import java.util.Map; +import java.util.TreeSet; +import java.util.UUID; +import java.util.regex.Pattern; +import javax.naming.InvalidNameException; +import org.apache.directory.api.ldap.model.constants.SchemaConstants; +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.ldif.LdifEntry; +import org.apache.directory.api.ldap.model.ldif.LdifReader; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor; +import org.apache.directory.api.ldap.schemaextractor.impl.ResourceMap; +import org.apache.directory.server.core.api.interceptor.context.AddOperationContext; +import org.apache.directory.server.core.partition.ldif.AbstractLdifPartition; +import org.jboss.logging.Logger; + +/** + * In-memory schema-only partition which loads the data in the similar way as the + * {@link org.apache.directory.api.ldap.schemaloader.JarLdifSchemaLoader}. + * + * @author Josef Cacek + */ +class InMemorySchemaPartition extends AbstractLdifPartition { + + private static final Logger log = Logger.getLogger(InMemorySchemaPartition.class); + + /** + * Filesystem path separator pattern, either forward slash or backslash. java.util.regex.Pattern is immutable so only one + * instance is needed for all uses. + */ + public InMemorySchemaPartition(SchemaManager schemaManager) { + super(schemaManager); + } + + /** + * Partition initialization - loads schema entries from the files on classpath. + * + * @see org.apache.directory.server.core.partition.impl.avl.AvlPartition#doInit() + */ + @Override + protected void doInit() throws InvalidNameException, Exception { + if (initialized) + return; + log.debug("Initializing schema partition " + getId()); + suffixDn.apply(schemaManager); + super.doInit(); + + // load schema + final Map resMap = ResourceMap.getResources(Pattern.compile("schema[/\\Q\\\\E]ou=schema.*")); + for (String resourcePath : new TreeSet(resMap.keySet())) { + if (resourcePath.endsWith(".ldif")) { + URL resource = DefaultSchemaLdifExtractor.getUniqueResource(resourcePath, "Schema LDIF file"); + LdifReader reader = new LdifReader(resource.openStream()); + LdifEntry ldifEntry = reader.next(); + reader.close(); + Entry entry = new DefaultEntry(schemaManager, ldifEntry.getEntry()); + + // add mandatory attributes + if (entry.get(SchemaConstants.ENTRY_CSN_AT) == null) { + entry.add(SchemaConstants.ENTRY_CSN_AT, defaultCSNFactory.newInstance().toString()); + } + if (entry.get(SchemaConstants.ENTRY_UUID_AT) == null) { + entry.add(SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString()); + } + AddOperationContext addContext = new AddOperationContext(null, entry); + super.add(addContext); + } + } + } + +} diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java new file mode 100644 index 0000000000..635c81764c --- /dev/null +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosEmbeddedServer.java @@ -0,0 +1,183 @@ +package org.keycloak.testutils.ldap; + +import java.io.IOException; +import java.lang.reflect.Field; + +import javax.security.auth.kerberos.KerberosPrincipal; + +import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms; +import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor; +import org.apache.directory.server.kerberos.KerberosConfig; +import org.apache.directory.server.kerberos.kdc.KdcServer; +import org.apache.directory.server.kerberos.shared.replay.ReplayCache; +import org.apache.directory.server.ldap.LdapServer; +import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler; +import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler; +import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler; +import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler; +import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler; +import org.apache.directory.server.protocol.shared.transport.UdpTransport; +import org.apache.directory.shared.kerberos.KerberosTime; +import org.jboss.logging.Logger; + +/** + * @author Marek Posolda + */ +public class KerberosEmbeddedServer extends LDAPEmbeddedServer { + + private static final Logger log = Logger.getLogger(LDAPEmbeddedServer.class); + + private final String kerberosRealm; + private final int kdcPort; + + private KdcServer kdcServer; + + + public static void main(String[] args) throws Exception { + EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration(); + KerberosEmbeddedServer kerberosEmbeddedServer = factory.createKerberosServer(); + kerberosEmbeddedServer.init(); + kerberosEmbeddedServer.start(); + } + + + protected KerberosEmbeddedServer(String baseDN, String bindHost, int bindPort, String ldifFile, String kerberosRealm, int kdcPort) { + super(baseDN, bindHost, bindPort, ldifFile); + this.kerberosRealm = kerberosRealm; + this.kdcPort = kdcPort; + } + + + @Override + public void init() throws Exception { + super.init(); + + log.info("Creating KDC server"); + createAndStartKdcServer(); + } + + + @Override + protected DirectoryService createDirectoryService() throws Exception { + DirectoryService directoryService = super.createDirectoryService(); + + directoryService.addLast(new KeyDerivationInterceptor()); + return directoryService; + } + + + @Override + protected LdapServer createLdapServer() { + LdapServer ldapServer = super.createLdapServer(); + + ldapServer.setSaslHost( this.bindHost ); + ldapServer.setSaslPrincipal( "ldap/" + this.bindHost + "@" + this.kerberosRealm); + + ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler()); + ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.CRAM_MD5, new CramMd5MechanismHandler()); + ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.DIGEST_MD5, new DigestMd5MechanismHandler()); + ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.GSSAPI, new GssapiMechanismHandler()); + ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.NTLM, new NtlmMechanismHandler()); + ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.GSS_SPNEGO, new NtlmMechanismHandler()); + + return ldapServer; + } + + + protected KdcServer createAndStartKdcServer() throws Exception { + KerberosConfig kdcConfig = new KerberosConfig(); + kdcConfig.setServicePrincipal("krbtgt/" + this.kerberosRealm + "@" + this.kerberosRealm); + kdcConfig.setPrimaryRealm(this.kerberosRealm); + kdcConfig.setMaximumTicketLifetime(60000 * 1440); + kdcConfig.setMaximumRenewableLifetime(60000 * 10080); + kdcConfig.setPaEncTimestampRequired(false); + + kdcServer = new NoReplayKdcServer(kdcConfig); + kdcServer.setSearchBaseDn(this.baseDN); + + UdpTransport udp = new UdpTransport(this.bindHost, this.kdcPort); + kdcServer.addTransports(udp); + + kdcServer.setDirectoryService(directoryService); + + // Launch the server + kdcServer.start(); + + return kdcServer; + } + + + public void stop() throws Exception { + stopLdapServer(); + stopKerberosServer(); + shutdownDirectoryService(); + } + + + protected void stopKerberosServer() { + log.info("Stoping Kerberos server."); + kdcServer.stop(); + } + + + /** + * Replacement of apacheDS KdcServer class with disabled ticket replay cache. + * + * @author Dominik Pospisil + */ + class NoReplayKdcServer extends KdcServer { + + NoReplayKdcServer(KerberosConfig kdcConfig) { + super(kdcConfig); + } + + /** + * + * Dummy implementation of the ApacheDS kerberos replay cache. Essentially disables kerbores ticket replay checks. + * https://issues.jboss.org/browse/JBPAPP-10974 + * + * @author Dominik Pospisil + */ + private class DummyReplayCache implements ReplayCache { + + @Override + public boolean isReplay(KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal, KerberosTime clientTime, + int clientMicroSeconds) { + return false; + } + + @Override + public void save(KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal, KerberosTime clientTime, + int clientMicroSeconds) { + return; + } + + @Override + public void clear() { + return; + } + + } + + /** + * @throws java.io.IOException if we cannot bind to the sockets + */ + @Override + public void start() throws IOException, LdapInvalidDnException { + super.start(); + + try { + + // override initialized replay cache with a dummy implementation + Field replayCacheField = KdcServer.class.getDeclaredField("replayCache"); + replayCacheField.setAccessible(true); + replayCacheField.set(this, new DummyReplayCache()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + } +} diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosKeytabCreator.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosKeytabCreator.java new file mode 100644 index 0000000000..aa26153fb1 --- /dev/null +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/KerberosKeytabCreator.java @@ -0,0 +1,70 @@ +package org.keycloak.testutils.ldap; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory; +import org.apache.directory.server.kerberos.shared.keytab.Keytab; +import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry; +import org.apache.directory.shared.kerberos.KerberosTime; +import org.apache.directory.shared.kerberos.codec.types.EncryptionType; +import org.apache.directory.shared.kerberos.components.EncryptionKey; + +/** + * Helper utility for creating Keytab files. + * + * @author Josef Cacek + */ +public class KerberosKeytabCreator { + + // Public methods -------------------------------------------------------- + + /** + * The main. + * + * @param args + * @throws java.io.IOException + */ + public static void main(String[] args) throws IOException { + if (args == null || args.length != 3) { + System.out.println("Kerberos keytab generator"); + System.out.println("-------------------------"); + System.out.println("Arguments missing or invalid. Required arguments are: "); + System.out.println("Example of usage:"); + System.out.println("mvn exec:java -Dexec.mainClass=\"org.keycloak.testutils.ldap.KerberosKeytabCreator\" -Dexec.args=\"HTTP/localhost@KEYCLOAK.ORG httppwd src/main/resources/kerberos/http.keytab\""); + } else { + final File keytabFile = new File(args[2]); + createKeytab(args[0], args[1], keytabFile); + System.out.println("Keytab file was created: " + keytabFile.getAbsolutePath() + ", principal: " + args[0] + ", passphrase: " + args[1]); + } + } + + /** + * Creates a keytab file for given principal. + * + * @param principalName + * @param passPhrase + * @param keytabFile + * @throws IOException + */ + public static void createKeytab(final String principalName, final String passPhrase, final File keytabFile) + throws IOException { + final KerberosTime timeStamp = new KerberosTime(); + final int principalType = 1; // KRB5_NT_PRINCIPAL + + final Keytab keytab = Keytab.getInstance(); + final List entries = new ArrayList(); + for (Map.Entry keyEntry : KerberosKeyFactory.getKerberosKeys(principalName, passPhrase) + .entrySet()) { + System.out.println("Adding keytab entry of type: " + keyEntry.getKey().getName()); + final EncryptionKey key = keyEntry.getValue(); + final byte keyVersion = (byte) key.getKeyVersion(); + entries.add(new KeytabEntry(principalName, principalType, timeStamp, keyVersion, key)); + } + keytab.setEntries(entries); + keytab.write(keytabFile); + } +} diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/LDAPEmbeddedServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPConfiguration.java old mode 100755 new mode 100644 similarity index 66% rename from testsuite/integration/src/main/java/org/keycloak/testutils/LDAPEmbeddedServer.java rename to testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPConfiguration.java index bbf932c3da..1312143306 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/LDAPEmbeddedServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPConfiguration.java @@ -1,32 +1,16 @@ -package org.keycloak.testutils; +package org.keycloak.testutils.ldap; -import org.keycloak.models.LDAPConstants; -import org.picketbox.test.ldap.AbstractLDAPTest; - -import javax.naming.CompositeName; -import javax.naming.Context; -import javax.naming.ContextNotEmptyException; -import javax.naming.Name; -import javax.naming.NameClassPair; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; -import java.io.File; import java.io.InputStream; import java.util.HashMap; -import java.util.Hashtable; import java.util.Map; import java.util.Properties; +import org.keycloak.models.LDAPConstants; + /** - * Forked from Picketlink project - * - * Abstract base for all LDAP test suites. It handles - * @author Peter Skopek: pskopek at redhat dot com - * + * @author Marek Posolda */ -public class LDAPEmbeddedServer extends AbstractLDAPTest { +public class LDAPConfiguration { public static final String CONNECTION_PROPERTIES = "ldap/ldap-connection.properties"; @@ -64,10 +48,10 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { public static String IDM_TEST_LDAP_USER_OBJECT_CLASSES = "idm.test.ldap.user.object.classes"; public static String IDM_TEST_LDAP_USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE = "idm.test.ldap.user.account.controls.after.password.update"; - - public LDAPEmbeddedServer() { - super(); - loadConnectionProperties(); + public static LDAPConfiguration readConfiguration() { + LDAPConfiguration ldapConfiguration = new LDAPConfiguration(); + ldapConfiguration.loadConnectionProperties(); + return ldapConfiguration; } protected void loadConnectionProperties() { @@ -98,44 +82,6 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { userAccountControlsAfterPasswordUpdate = Boolean.parseBoolean(p.getProperty(IDM_TEST_LDAP_USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE)); } - @Override - public void setup() throws Exception { - // suppress emb. LDAP server start - if (isStartEmbeddedLdapLerver()) { - // On Windows, the directory may not be fully deleted from previous test - String tempDir = System.getProperty("java.io.tmpdir"); - File workDir = new File(tempDir + File.separator + "server-work"); - if (workDir.exists()) { - recursiveDeleteDir(workDir); - } - - super.setup(); - } - } - - @Override - public void tearDown() throws Exception { - // suppress emb. LDAP server stop - if (isStartEmbeddedLdapLerver()) { - - // clear data left in LDAP - DirContext ctx = getDirContext(); - clearSubContexts(ctx, new CompositeName(baseDn)); - - super.tearDown(); - } - } - - private DirContext getDirContext() throws NamingException { - Hashtable env = new Hashtable(); - env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(Context.PROVIDER_URL, connectionUrl); - env.put(Context.SECURITY_PRINCIPAL, bindDn); - env.put(Context.SECURITY_CREDENTIALS, bindCredential); - DirContext ctx = new InitialDirContext(env); - return ctx; - } - public Map getLDAPConfig() { Map ldapConfig = new HashMap(); ldapConfig.put(LDAPConstants.CONNECTION_URL, getConnectionUrl()); @@ -153,37 +99,6 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { return ldapConfig; } - - public static void clearSubContexts(DirContext ctx, Name name) throws NamingException { - - NamingEnumeration enumeration = null; - try { - enumeration = ctx.list(name); - while (enumeration.hasMore()) { - NameClassPair pair = enumeration.next(); - Name childName = ctx.composeName(new CompositeName(pair.getName()), name); - try { - ctx.destroySubcontext(childName); - } - catch (ContextNotEmptyException e) { - clearSubContexts(ctx, childName); - ctx.destroySubcontext(childName); - } - } - } - catch (NamingException e) { - e.printStackTrace(); - } - finally { - try { - enumeration.close(); - } - catch (Exception e) { - // Never mind this - } - } - } - public String getConnectionUrl() { return connectionUrl; } @@ -247,18 +162,4 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest { public boolean isUserAccountControlsAfterPasswordUpdate() { return userAccountControlsAfterPasswordUpdate; } - - @Override - public void importLDIF(String fileName) throws Exception { - // import LDIF only in case we are running against embedded LDAP server - if (isStartEmbeddedLdapLerver()) { - super.importLDIF(fileName); - } - } - - @Override - protected void createBaseDN() throws Exception { - ds.createBaseDN("keycloak", "dc=keycloak,dc=org"); - } - } diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java new file mode 100644 index 0000000000..9c18288ebf --- /dev/null +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java @@ -0,0 +1,186 @@ +package org.keycloak.testutils.ldap; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.text.StrSubstitutor; +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException; +import org.apache.directory.api.ldap.model.ldif.LdifEntry; +import org.apache.directory.api.ldap.model.ldif.LdifReader; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.partition.Partition; +import org.apache.directory.server.core.factory.DSAnnotationProcessor; +import org.apache.directory.server.core.factory.PartitionFactory; +import org.apache.directory.server.ldap.LdapServer; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.directory.server.protocol.shared.transport.Transport; +import org.jboss.logging.Logger; +import org.keycloak.util.StreamUtil; + +/** + * @author Marek Posolda + */ +public class LDAPEmbeddedServer { + + private static final Logger log = Logger.getLogger(LDAPEmbeddedServer.class); + + protected final String baseDN; + protected final String bindHost; + protected final int bindPort; + protected final String ldifFile; + + protected DirectoryService directoryService; + protected LdapServer ldapServer; + + + public static void main(String[] args) throws Exception { + EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration(); + LDAPEmbeddedServer ldapEmbeddedServer = factory.createLdapServer(); + ldapEmbeddedServer.init(); + ldapEmbeddedServer.start(); + } + + public LDAPEmbeddedServer(String baseDN, String bindHost, int bindPort, String ldifFile) { + this.baseDN = baseDN; + this.bindHost = bindHost; + this.bindPort = bindPort; + this.ldifFile = ldifFile; + } + + + public void init() throws Exception { + log.info("Creating LDAP Directory Service. Config: baseDN=" + baseDN + ", bindHost=" + bindHost + ", bindPort=" + bindPort); + this.directoryService = createDirectoryService(); + + log.info("Importing LDIF: " + ldifFile); + importLdif(); + + log.info("Creating LDAP Server"); + this.ldapServer = createLdapServer(); + } + + + public void start() throws Exception { + log.info("Starting LDAP Server"); + ldapServer.start(); + log.info("LDAP Server started"); + } + + + protected DirectoryService createDirectoryService() throws Exception { + // Parse "keycloak" from "dc=keycloak,dc=org" + String dcName = baseDN.split(",")[0].substring(3); + + InMemoryDirectoryServiceFactory dsf = new InMemoryDirectoryServiceFactory(); + + DirectoryService service = dsf.getDirectoryService(); + service.setAccessControlEnabled(false); + service.setAllowAnonymousAccess(false); + service.getChangeLog().setEnabled(false); + + dsf.init(dcName + "DS"); + + SchemaManager schemaManager = service.getSchemaManager(); + + PartitionFactory partitionFactory = dsf.getPartitionFactory(); + Partition partition = partitionFactory.createPartition( + schemaManager, + service.getDnFactory(), + dcName, + this.baseDN, + 1000, + new File(service.getInstanceLayout().getPartitionsDirectory(), dcName)); + partition.setCacheService( service.getCacheService() ); + partition.initialize(); + + partition.setSchemaManager( schemaManager ); + + // Inject the partition into the DirectoryService + service.addPartition( partition ); + + // Last, process the context entry + String entryLdif = + "dn: " + baseDN + "\n" + + "dc: " + dcName + "\n" + + "objectClass: top\n" + + "objectClass: domain\n\n"; + DSAnnotationProcessor.injectEntries(service, entryLdif); + + return service; + } + + + protected LdapServer createLdapServer() { + LdapServer ldapServer = new LdapServer(); + + ldapServer.setServiceName("DefaultLdapServer"); + + // Read the transports + Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50); + ldapServer.addTransports( ldap ); + + // Associate the DS to this LdapServer + ldapServer.setDirectoryService( directoryService ); + + // Propagate the anonymous flag to the DS + directoryService.setAllowAnonymousAccess(false); + + ldapServer.setSaslHost( this.bindHost ); + ldapServer.setSaslPrincipal( "ldap/" + this.bindHost + "@KEYCLOAK.ORG"); + ldapServer.setSaslRealms(new ArrayList()); + return ldapServer; + } + + + private void importLdif() throws Exception { + Map map = new HashMap(); + map.put("hostname", this.bindHost); + + // For now, assume that LDIF file is on classpath + InputStream is = getClass().getClassLoader().getResourceAsStream(ldifFile); + if (is == null) { + throw new IllegalStateException("LDIF file not found on classpath. Location was: " + ldifFile); + } + + final String ldifContent = StrSubstitutor.replace(StreamUtil.readString(is), map); + log.info("Importing LDIF: " + ldifContent); + final SchemaManager schemaManager = directoryService.getSchemaManager(); + + for (LdifEntry ldifEntry : new LdifReader(IOUtils.toInputStream(ldifContent))) { + try { + directoryService.getAdminSession().add(new DefaultEntry(schemaManager, ldifEntry.getEntry())); + } catch (LdapEntryAlreadyExistsException ignore) { + log.debug("Entry " + ldifEntry.getNewRdn() + " already exists. Ignoring"); + } + } + } + + + public void stop() throws Exception { + stopLdapServer(); + shutdownDirectoryService(); + } + + + protected void stopLdapServer() { + log.info("Stoping LDAP server."); + ldapServer.stop(); + } + + + protected void shutdownDirectoryService() throws Exception { + log.info("Stoping Directory service."); + directoryService.shutdown(); + + log.info("Removing Directory service workfiles."); + FileUtils.deleteDirectory(directoryService.getInstanceLayout().getInstanceDirectory()); + } + +} diff --git a/testsuite/integration/src/main/resources/kerberos/http.keytab b/testsuite/integration/src/main/resources/kerberos/http.keytab new file mode 100644 index 0000000000000000000000000000000000000000..c156500feef4b8d9ef7abaa0275263bba600244a GIT binary patch literal 266 zcmZQ&VqjpfV_;(7@pg@L_VIW0*7FZ?XJGLN2?=1}%*jtq%*n_vE&(cH40(7|mw`c$ zL4g1HK9N~r>sNh0AIWRKXG$iCCWtc#ut)GOJoM-K%#~$zxz3^YmHxKF99)4q-AK?Wz#w6?dAa`m%Vp`0Ir54>eZRlrtUT|_Xv6eM0Q%ERZ2$lO literal 0 HcmV?d00001 diff --git a/testsuite/integration/src/main/resources/kerberos/users-kerberos.ldif b/testsuite/integration/src/main/resources/kerberos/users-kerberos.ldif new file mode 100644 index 0000000000..fcde10e462 --- /dev/null +++ b/testsuite/integration/src/main/resources/kerberos/users-kerberos.ldif @@ -0,0 +1,88 @@ +dn: dc=keycloak,dc=org +objectclass: dcObject +objectclass: organization +o: Keycloak +dc: Keycloak + +dn: ou=People,dc=keycloak,dc=org +objectClass: organizationalUnit +objectClass: top +ou: People + +dn: uid=krbtgt,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: KDC Service +sn: Service +uid: krbtgt +userPassword: secret +krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=ldap,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: LDAP +sn: Service +uid: ldap +userPassword: randall +krb5PrincipalName: ldap/${hostname}@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=HTTP,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: HTTP +sn: Service +uid: HTTP +userPassword: httppwd +krb5PrincipalName: HTTP/${hostname}@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=hnelson,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: Horatio Nelson +sn: Nelson +uid: hnelson +userPassword: secret +krb5PrincipalName: hnelson@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=jduke,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: Java Duke +sn: duke +uid: jduke +userPassword: theduke +krb5PrincipalName: jduke@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + +dn: uid=gsstestserver,ou=People,dc=keycloak,dc=org +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: gsstestserver +sn: Service +uid: gsstestserver +userPassword: gsstestpwd +krb5PrincipalName: gsstestserver/xxx@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 diff --git a/testsuite/integration/src/main/resources/log4j.properties b/testsuite/integration/src/main/resources/log4j.properties index 573c238f3c..5e78b310ff 100755 --- a/testsuite/integration/src/main/resources/log4j.properties +++ b/testsuite/integration/src/main/resources/log4j.properties @@ -19,6 +19,11 @@ log4j.logger.org.keycloak=info # Enable to view kerberos/spnego logging # log4j.logger.org.keycloak.broker.kerberos=trace +# Enable to view detailed AS REQ and TGS REQ requests to embedded Kerberos server +log4j.logger.org.apache.directory.server.kerberos=debug + log4j.logger.org.xnio=off log4j.logger.org.hibernate=off -log4j.logger.org.jboss.resteasy=warn \ No newline at end of file +log4j.logger.org.jboss.resteasy=warn +log4j.logger.org.apache.directory.api=warn +log4j.logger.org.apache.directory.server.core=warn \ No newline at end of file diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java index 1146257e7b..833e56c664 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java @@ -33,7 +33,6 @@ import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.LDAPRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; -import org.keycloak.testutils.LDAPEmbeddedServer; import org.openqa.selenium.WebDriver; import org.picketlink.idm.PartitionManager; import org.picketlink.idm.model.basic.User; @@ -56,8 +55,7 @@ public class FederationProvidersIntegrationTest { public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { addUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app"); - LDAPEmbeddedServer ldapServer = ldapRule.getEmbeddedServer(); - Map ldapConfig = ldapServer.getLDAPConfig(); + Map ldapConfig = ldapRule.getLdapConfig(); ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "true"); ldapConfig.put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java index f256ee008c..917a544e8a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java @@ -22,7 +22,6 @@ import org.keycloak.services.managers.UsersSyncManager; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.LDAPRule; import org.keycloak.testutils.DummyUserFederationProviderFactory; -import org.keycloak.testutils.LDAPEmbeddedServer; import org.keycloak.timer.TimerProvider; import org.keycloak.util.Time; import org.picketlink.idm.PartitionManager; @@ -49,8 +48,7 @@ public class SyncProvidersTest { // Other tests may left Time offset uncleared, which could cause issues Time.setOffset(0); - LDAPEmbeddedServer ldapServer = ldapRule.getEmbeddedServer(); - Map ldapConfig = ldapServer.getLDAPConfig(); + Map ldapConfig = ldapRule.getLdapConfig(); ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "false"); ldapConfig.put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.UNSYNCED.toString()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java index 1c631bb9ad..5b120377cb 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java @@ -1,37 +1,46 @@ package org.keycloak.testsuite.rule; +import java.util.Map; + import org.junit.rules.ExternalResource; -import org.keycloak.testutils.LDAPEmbeddedServer; +import org.keycloak.testutils.ldap.EmbeddedServersFactory; +import org.keycloak.testutils.ldap.LDAPConfiguration; +import org.keycloak.testutils.ldap.LDAPEmbeddedServer; /** * @author Marek Posolda */ public class LDAPRule extends ExternalResource { - private LDAPEmbeddedServer embeddedServer; + private LDAPConfiguration ldapConfiguration; + private LDAPEmbeddedServer ldapEmbeddedServer; @Override protected void before() throws Throwable { - try { - embeddedServer = new LDAPEmbeddedServer(); - embeddedServer.setup(); - embeddedServer.importLDIF("ldap/users.ldif"); - } catch (Exception e) { - throw new RuntimeException("Error starting Embedded LDAP server.", e); + ldapConfiguration = LDAPConfiguration.readConfiguration(); + + if (ldapConfiguration.isStartEmbeddedLdapLerver()) { + EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration(); + ldapEmbeddedServer = factory.createLdapServer(); + ldapEmbeddedServer.init(); + ldapEmbeddedServer.start(); } } @Override protected void after() { try { - embeddedServer.tearDown(); - embeddedServer = null; + if (ldapEmbeddedServer != null) { + ldapEmbeddedServer.stop(); + ldapEmbeddedServer = null; + ldapConfiguration = null; + } } catch (Exception e) { throw new RuntimeException("Error tearDown Embedded LDAP server.", e); } } - public LDAPEmbeddedServer getEmbeddedServer() { - return embeddedServer; + public Map getLdapConfig() { + return ldapConfiguration.getLDAPConfig(); } } From 4795059e154ae90dfb60faac6bd71d586679baff Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 16 Feb 2015 11:50:42 +0100 Subject: [PATCH 3/7] Fix mongo model --- .../mongo/impl/MongoStoreImpl.java | 7 +++ .../mongo/impl/types/BasicDBListMapper.java | 1 + .../impl/types/BasicDBListToSetMapper.java | 44 +++++++++++++++++++ .../mongo/impl/types/ListMapper.java | 4 +- .../keycloak/adapters/MongoRealmProvider.java | 4 +- .../adapters/MongoRealmProviderFactory.java | 2 +- .../keycloak/adapters/MongoUserProvider.java | 5 +-- .../adapters/MongoUserProviderFactory.java | 2 +- 8 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java index 35a7e42535..aded77e6b0 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/MongoStoreImpl.java @@ -18,6 +18,7 @@ import org.keycloak.connections.mongo.api.types.Mapper; import org.keycloak.connections.mongo.api.types.MapperContext; import org.keycloak.connections.mongo.api.types.MapperRegistry; import org.keycloak.connections.mongo.impl.types.BasicDBListMapper; +import org.keycloak.connections.mongo.impl.types.BasicDBListToSetMapper; import org.keycloak.connections.mongo.impl.types.BasicDBObjectMapper; import org.keycloak.connections.mongo.impl.types.BasicDBObjectToMapMapper; import org.keycloak.connections.mongo.impl.types.EnumToStringMapper; @@ -35,8 +36,10 @@ import org.keycloak.models.utils.reflection.PropertyQueries; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -71,6 +74,10 @@ public class MongoStoreImpl implements MongoStore { mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, List.class)); mapperRegistry.addDBObjectMapper(new BasicDBListMapper(mapperRegistry)); + mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, HashSet.class)); + mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, Set.class)); + mapperRegistry.addDBObjectMapper(new BasicDBListToSetMapper(mapperRegistry)); + mapperRegistry.addAppObjectMapper(new MapMapper(HashMap.class)); mapperRegistry.addAppObjectMapper(new MapMapper(Map.class)); mapperRegistry.addDBObjectMapper(new BasicDBObjectToMapMapper()); diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java index f44f54506a..cc229c6969 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java @@ -6,6 +6,7 @@ import org.keycloak.connections.mongo.api.types.MapperContext; import org.keycloak.connections.mongo.api.types.MapperRegistry; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java new file mode 100644 index 0000000000..d43781a225 --- /dev/null +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java @@ -0,0 +1,44 @@ +package org.keycloak.connections.mongo.impl.types; + +import java.util.HashSet; +import java.util.Set; + +import com.mongodb.BasicDBList; +import org.keycloak.connections.mongo.api.types.Mapper; +import org.keycloak.connections.mongo.api.types.MapperContext; +import org.keycloak.connections.mongo.api.types.MapperRegistry; + +/** + * @author Marek Posolda + */ +public class BasicDBListToSetMapper implements Mapper { + + private final MapperRegistry mapperRegistry; + + public BasicDBListToSetMapper(MapperRegistry mapperRegistry) { + this.mapperRegistry = mapperRegistry; + } + + @Override + public Set convertObject(MapperContext context) { + BasicDBList dbList = context.getObjectToConvert(); + Set appObjects = new HashSet(); + Class expectedListElementType = context.getGenericTypes().get(0); + + for (Object dbObject : dbList) { + MapperContext newContext = new MapperContext(dbObject, expectedListElementType, null); + appObjects.add(mapperRegistry.convertDBObjectToApplicationObject(newContext)); + } + return appObjects; + } + + @Override + public Class getTypeOfObjectToConvert() { + return BasicDBList.class; + } + + @Override + public Class getExpectedReturnType() { + return Set.class; + } +} diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/ListMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/ListMapper.java index 3274fe36d5..dc649d67c5 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/ListMapper.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/ListMapper.java @@ -5,12 +5,12 @@ import org.keycloak.connections.mongo.api.types.Mapper; import org.keycloak.connections.mongo.api.types.MapperContext; import org.keycloak.connections.mongo.api.types.MapperRegistry; -import java.util.List; +import java.util.Collection; /** * @author Marek Posolda */ -public class ListMapper implements Mapper { +public class ListMapper implements Mapper { private final MapperRegistry mapperRegistry; private final Class listType; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java index 7ce4935bc2..691bce4925 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java @@ -27,11 +27,9 @@ public class MongoRealmProvider implements RealmProvider { private final MongoStoreInvocationContext invocationContext; private final KeycloakSession session; - private final MongoStore mongoStore; - public MongoRealmProvider(KeycloakSession session, MongoStore mongoStore, MongoStoreInvocationContext invocationContext) { + public MongoRealmProvider(KeycloakSession session, MongoStoreInvocationContext invocationContext) { this.session = session; - this.mongoStore = mongoStore; this.invocationContext = invocationContext; } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProviderFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProviderFactory.java index fceeb82ae9..96067533a4 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProviderFactory.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProviderFactory.java @@ -27,7 +27,7 @@ public class MongoRealmProviderFactory implements RealmProviderFactory { @Override public RealmProvider create(KeycloakSession session) { MongoConnectionProvider connection = session.getProvider(MongoConnectionProvider.class); - return new MongoRealmProvider(session, connection.getMongoStore(), connection.getInvocationContext()); + return new MongoRealmProvider(session, connection.getInvocationContext()); } @Override diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 52d4c98d2b..94227112d0 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -33,11 +33,9 @@ public class MongoUserProvider implements UserProvider { private final MongoStoreInvocationContext invocationContext; private final KeycloakSession session; - private final MongoStore mongoStore; - public MongoUserProvider(KeycloakSession session, MongoStore mongoStore, MongoStoreInvocationContext invocationContext) { + public MongoUserProvider(KeycloakSession session, MongoStoreInvocationContext invocationContext) { this.session = session; - this.mongoStore = mongoStore; this.invocationContext = invocationContext; } @@ -311,6 +309,7 @@ public class MongoUserProvider implements UserProvider { @Override public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) { + federatedUser = getUserById(federatedUser.getId(), realm); MongoUserEntity userEntity = ((UserAdapter) federatedUser).getUser(); FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, federatedIdentityModel.getIdentityProvider()); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProviderFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProviderFactory.java index 083d0c03bf..a11704827b 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProviderFactory.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProviderFactory.java @@ -27,7 +27,7 @@ public class MongoUserProviderFactory implements UserProviderFactory { @Override public UserProvider create(KeycloakSession session) { MongoConnectionProvider connection = session.getProvider(MongoConnectionProvider.class); - return new MongoUserProvider(session, connection.getMongoStore(), connection.getInvocationContext()); + return new MongoUserProvider(session, connection.getInvocationContext()); } @Override From bb027a31234b3eb346cbb99506b5f591efb17b11 Mon Sep 17 00:00:00 2001 From: girirajsharma Date: Wed, 18 Feb 2015 19:01:48 +0530 Subject: [PATCH 4/7] Fixed a very minor breadcrumb label in user details(attributes) UI. --- .../theme/admin/base/resources/partials/user-detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html index c678571dc7..5e770a882c 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html @@ -16,7 +16,7 @@ -

{{application.name}} Identity Provider Settings

+

{{application.name}} Identity Provider Settings

{{identityProvider.name}}
- +
From d8024b0c6ce0f036c6e864311a4f051e2c6f2bf3 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 16 Feb 2015 17:27:49 +0100 Subject: [PATCH 6/7] Kerberos refactored to be federation provider. Support for Kerberos and LDAP. Username/password authentication support with kerberos credentials. First step - KerberosFederationProvider Finished KerberosFederationProvider. Added support for username/password authentication with kerberos credentials Added Kerberos UI to LDAPFederationProvider Fixes --- .../broker/kerberos/KerberosConstants.java | 30 --- .../kerberos/KerberosIdentityProvider.java | 147 ----------- .../KerberosIdentityProviderConfig.java | 26 -- .../KerberosIdentityProviderFactory.java | 27 -- ...ak.broker.provider.IdentityProviderFactory | 1 - broker/pom.xml | 1 - .../idm/CredentialRepresentation.java | 1 + dependencies/server-all/pom.xml | 2 +- .../BasePropertiesFederationProvider.java | 11 + {broker => federation}/kerberos/pom.xml | 29 ++- .../kerberos/CommonKerberosConfig.java | 46 ++++ .../federation/kerberos/KerberosConfig.java | 34 +++ .../kerberos/KerberosFederationProvider.java | 241 ++++++++++++++++++ .../KerberosFederationProviderFactory.java | 80 ++++++ .../ReadOnlyKerberosUserModelDelegate.java | 28 ++ .../KerberosServerSubjectAuthenticator.java | 8 +- ...KerberosUsernamePasswordAuthenticator.java | 126 +++++++++ .../kerberos/impl/SPNEGOAuthenticator.java | 30 ++- ...cloak.models.UserFederationProviderFactory | 1 + federation/ldap/pom.xml | 6 + .../ldap/LDAPFederationProvider.java | 110 ++++++-- .../ldap/LDAPFederationProviderFactory.java | 20 +- .../kerberos/LDAPProviderKerberosConfig.java | 20 ++ federation/pom.xml | 1 + .../theme/admin/base/resources/js/app.js | 30 +++ .../base/resources/js/controllers/realm.js | 5 +- .../base/resources/js/controllers/users.js | 56 ++-- .../partials/federated-kerberos.html | 118 +++++++++ .../resources/partials/federated-ldap.html | 54 +++- .../realm-identity-provider-kerberos.html | 78 ------ .../login/base/messages/messages.properties | 3 +- .../keycloak/login/LoginFormsProvider.java | 2 + .../FreeMarkerLoginFormsProvider.java | 10 + .../models/CredentialValidationOutput.java | 46 ++++ .../models/RequiredCredentialModel.java | 7 + .../keycloak/models/UserCredentialModel.java | 8 + .../models/UserFederationManager.java | 54 ++-- .../models/UserFederationProvider.java | 17 ++ .../org/keycloak/models/UserProvider.java | 2 + .../models/utils/KerberosConstants.java | 38 +++ .../cache/DefaultCacheUserProvider.java | 6 + .../models/cache/NoCacheUserProvider.java | 6 + .../keycloak/models/jpa/JpaUserProvider.java | 7 + .../keycloak/adapters/MongoUserProvider.java | 7 + .../protocol/oidc/OpenIDConnectService.java | 11 + .../managers/HttpAuthenticationChallenge.java | 11 + .../managers/HttpAuthenticationManager.java | 167 ++++++++++++ .../admin/UserFederationResource.java | 23 ++ .../DummyUserFederationProvider.java | 11 + .../AbstractIdentityProviderModelTest.java | 2 - .../broker/ImportIdentityProviderTest.java | 20 -- 51 files changed, 1396 insertions(+), 429 deletions(-) delete mode 100644 broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosConstants.java delete mode 100644 broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProvider.java delete mode 100644 broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderConfig.java delete mode 100644 broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderFactory.java delete mode 100644 broker/kerberos/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory rename {broker => federation}/kerberos/pom.xml (58%) create mode 100644 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java create mode 100644 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java create mode 100644 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java create mode 100644 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java create mode 100644 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java rename {broker/kerberos/src/main/java/org/keycloak/broker => federation/kerberos/src/main/java/org/keycloak/federation}/kerberos/impl/KerberosServerSubjectAuthenticator.java (89%) create mode 100644 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java rename {broker/kerberos/src/main/java/org/keycloak/broker => federation/kerberos/src/main/java/org/keycloak/federation}/kerberos/impl/SPNEGOAuthenticator.java (80%) create mode 100644 federation/kerberos/src/main/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java create mode 100644 forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-kerberos.html delete mode 100644 forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-kerberos.html create mode 100644 model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java create mode 100644 model/api/src/main/java/org/keycloak/models/utils/KerberosConstants.java create mode 100644 services/src/main/java/org/keycloak/services/managers/HttpAuthenticationChallenge.java create mode 100644 services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java diff --git a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosConstants.java b/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosConstants.java deleted file mode 100644 index 80a24587ff..0000000000 --- a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosConstants.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.keycloak.broker.kerberos; - -/** - * @author Marek Posolda - */ -public class KerberosConstants { - - /** - * Value of HTTP Headers "WWW-Authenticate" or "Authorization" used for SPNEGO/Kerberos - **/ - public static final String NEGOTIATE = "Negotiate"; - - - /** - * Helper parameter for relay state - */ - public static final String RELAY_STATE_PARAM = "RelayState"; - - - /** - * OID of SPNEGO mechanism. See http://www.oid-info.com/get/1.3.6.1.5.5.2 - */ - public static final String SPNEGO_OID = "1.3.6.1.5.5.2"; - - /** - * OID of Kerberos v5 mechanism. See http://www.oid-info.com/get/1.2.840.113554.1.2.2 - */ - public static final String KRB5_OID = "1.2.840.113554.1.2.2"; - -} diff --git a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProvider.java b/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProvider.java deleted file mode 100644 index 84ceb22f2e..0000000000 --- a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProvider.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.keycloak.broker.kerberos; - -import java.net.URI; - -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; - -import org.jboss.logging.Logger; -import org.keycloak.broker.kerberos.impl.KerberosServerSubjectAuthenticator; -import org.keycloak.broker.kerberos.impl.SPNEGOAuthenticator; -import org.keycloak.broker.provider.AbstractIdentityProvider; -import org.keycloak.broker.provider.AuthenticationRequest; -import org.keycloak.broker.provider.AuthenticationResponse; -import org.keycloak.broker.provider.FederatedIdentity; -import org.keycloak.login.LoginFormsProvider; -import org.keycloak.models.FederatedIdentityModel; - -/** - * @author Marek Posolda - */ -public class KerberosIdentityProvider extends AbstractIdentityProvider { - - private static final Logger logger = Logger.getLogger(KerberosIdentityProvider.class); - - public KerberosIdentityProvider(KerberosIdentityProviderConfig config) { - super(config); - } - - - @Override - public AuthenticationResponse handleRequest(AuthenticationRequest request) { - - // Just redirect to handleResponse for now - URI redirectUri = UriBuilder.fromUri(request.getRedirectUri()).queryParam(KerberosConstants.RELAY_STATE_PARAM, request.getState()).build(); - Response response = Response.status(302) - .location(redirectUri) - .build(); - - return AuthenticationResponse.fromResponse(response); - } - - - @Override - public String getRelayState(AuthenticationRequest request) { - UriInfo uriInfo = request.getUriInfo(); - return uriInfo.getQueryParameters().getFirst(KerberosConstants.RELAY_STATE_PARAM); - } - - - @Override - public AuthenticationResponse handleResponse(AuthenticationRequest request) { - String authHeader = request.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); - - // Case when we don't yet have any Negotiate header - if (authHeader == null) { - return sendNegotiateResponse(request, null); - } - - String[] tokens = authHeader.split(" "); - if (tokens.length != 2) { - logger.warn("Invalid length of tokens: " + tokens.length); - return sendNegotiateResponse(request, null); - } else if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) { - logger.warn("Unknown scheme " + tokens[0]); - return sendNegotiateResponse(request, null); - } else { - String spnegoToken = tokens[1]; - SPNEGOAuthenticator spnegoAuthenticator = createSPNEGOAuthenticator(spnegoToken); - spnegoAuthenticator.authenticate(); - - if (spnegoAuthenticator.isAuthenticated()) { - FederatedIdentity federatedIdentity = getFederatedIdentity(spnegoAuthenticator); - return AuthenticationResponse.end(federatedIdentity); - } else { - return sendNegotiateResponse(request, spnegoAuthenticator.getResponseToken()); - } - } - } - - protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken) { - KerberosServerSubjectAuthenticator kerberosAuth = createKerberosSubjectAuthenticator(); - return new SPNEGOAuthenticator(kerberosAuth, spnegoToken); - } - - protected KerberosServerSubjectAuthenticator createKerberosSubjectAuthenticator() { - return new KerberosServerSubjectAuthenticator(getConfig()); - } - - - /** - * Send response with header "WWW-Authenticate: Negotiate {negotiateToken}" - * - * @param negotiateToken token to be send back in response or null if just "WWW-Authenticate: Negotiate" should be sent - * @return AuthenticationResponse - */ - protected AuthenticationResponse sendNegotiateResponse(AuthenticationRequest request, String negotiateToken) { - String negotiateHeader = negotiateToken == null ? KerberosConstants.NEGOTIATE : KerberosConstants.NEGOTIATE + " " + negotiateToken; - - if (logger.isTraceEnabled()) { - logger.trace("Sending back " + HttpHeaders.WWW_AUTHENTICATE + ": " + negotiateHeader); - } - - Response response; - LoginFormsProvider loginFormsProvider = request.getSession().getProvider(LoginFormsProvider.class) - .setRealm(request.getRealm()) - .setUriInfo(request.getUriInfo()) - .setStatus(Response.Status.UNAUTHORIZED); - - if (request.getClientSession().getUserSession() == null) { - // User not logged. Display HTML with login form as fallback if SPNEGO token not found - response = loginFormsProvider.setClient(request.getClientSession().getClient()) - .setClientSessionCode(getRelayState(request)) - .setWarning("errorKerberosLogin") - .createLogin(); - } else { - // User logged and linking account. Display HTML with error if SPNEGO token not found - response = loginFormsProvider.setError("errorKerberosLinkAccount") - .createErrorPage(); - } - - response.getMetadata().putSingle(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader); - return AuthenticationResponse.fromResponse(response); - } - - - protected FederatedIdentity getFederatedIdentity(SPNEGOAuthenticator spnegoAuthenticator) { - String kerberosUsername = spnegoAuthenticator.getPrincipal(); - FederatedIdentity user = new FederatedIdentity(kerberosUsername); - user.setUsername(kerberosUsername); - - // Just guessing email - String[] tokens = kerberosUsername.split("@"); - String email = tokens[0] + "@" + tokens[1].toLowerCase(); - user.setEmail(email); - return user; - } - - - @Override - public Response retrieveToken(FederatedIdentityModel identity) { - logger.warn("retrieveToken unsupported for Kerberos right now"); - return null; - } -} diff --git a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderConfig.java b/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderConfig.java deleted file mode 100644 index 50dd85a704..0000000000 --- a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.keycloak.broker.kerberos; - -import org.keycloak.models.IdentityProviderModel; - -/** - * @author Marek Posolda - */ -public class KerberosIdentityProviderConfig extends IdentityProviderModel { - - public KerberosIdentityProviderConfig(IdentityProviderModel identityProviderModel) { - super(identityProviderModel); - } - - public String getServerPrincipal() { - return getConfig().get("serverPrincipal"); - } - - public String getKeyTab() { - return getConfig().get("keyTab"); - } - - public boolean getDebug() { - return Boolean.valueOf(getConfig().get("debug")); - } - -} diff --git a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderFactory.java b/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderFactory.java deleted file mode 100644 index 42b7428757..0000000000 --- a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/KerberosIdentityProviderFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.keycloak.broker.kerberos; - -import org.keycloak.broker.provider.AbstractIdentityProviderFactory; -import org.keycloak.models.IdentityProviderModel; - -/** - * @author Marek Posolda - */ -public class KerberosIdentityProviderFactory extends AbstractIdentityProviderFactory { - - public static final String PROVIDER_ID = "kerberos"; - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public String getName() { - return "Kerberos"; - } - - @Override - public KerberosIdentityProvider create(IdentityProviderModel model) { - return new KerberosIdentityProvider(new KerberosIdentityProviderConfig(model)); - } -} diff --git a/broker/kerberos/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory b/broker/kerberos/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory deleted file mode 100644 index 4c72cb8377..0000000000 --- a/broker/kerberos/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.broker.kerberos.KerberosIdentityProviderFactory \ No newline at end of file diff --git a/broker/pom.xml b/broker/pom.xml index e2ae6ffa12..7121b2b031 100755 --- a/broker/pom.xml +++ b/broker/pom.xml @@ -18,7 +18,6 @@ core oidc saml - kerberos diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java index f1ea7153c2..b7dad57706 100755 --- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java @@ -10,6 +10,7 @@ public class CredentialRepresentation { public static final String PASSWORD_TOKEN = "password-token"; public static final String TOTP = "totp"; public static final String CLIENT_CERT = "cert"; + public static final String KERBEROS = "kerberos"; protected String type; protected String device; diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index c2546e8682..f697926c71 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -95,7 +95,7 @@ org.keycloak - keycloak-broker-kerberos + keycloak-kerberos-federation ${project.version} diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java index 0d63474e14..964560c21e 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java @@ -1,5 +1,6 @@ package org.keycloak.examples.federation.properties; +import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -127,6 +128,11 @@ public abstract class BasePropertiesFederationProvider implements UserFederation return supportedCredentialTypes; } + @Override + public Set getSupportedCredentialTypes() { + return supportedCredentialTypes; + } + @Override public boolean validCredentials(RealmModel realm, UserModel user, List input) { for (UserCredentialModel cred : input) { @@ -155,6 +161,11 @@ public abstract class BasePropertiesFederationProvider implements UserFederation return true; } + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) { + return CredentialValidationOutput.failed(); + } + @Override public void close() { diff --git a/broker/kerberos/pom.xml b/federation/kerberos/pom.xml similarity index 58% rename from broker/kerberos/pom.xml rename to federation/kerberos/pom.xml index 2a7d75e2e3..6f7b6baefe 100644 --- a/broker/kerberos/pom.xml +++ b/federation/kerberos/pom.xml @@ -1,6 +1,5 @@ - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> keycloak-parent org.keycloak @@ -9,21 +8,22 @@ 4.0.0 - keycloak-broker-kerberos - Keycloak Broker Kerberos - - jar + keycloak-kerberos-federation + Keycloak Kerberos Federation + org.keycloak - keycloak-broker-core + keycloak-core ${project.version} + provided org.keycloak - keycloak-login-api + keycloak-model-api ${project.version} + provided org.jboss.logging @@ -36,4 +36,17 @@ provided + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java new file mode 100644 index 0000000000..6fe528c351 --- /dev/null +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java @@ -0,0 +1,46 @@ +package org.keycloak.federation.kerberos; + +import java.util.Map; + +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.utils.KerberosConstants; + +/** + * Common configuration useful for all providers + * + * @author Marek Posolda + */ +public abstract class CommonKerberosConfig { + + private final UserFederationProviderModel providerModel; + + public CommonKerberosConfig(UserFederationProviderModel userFederationProvider) { + this.providerModel = userFederationProvider; + } + + // Should be always true for KerberosFederationProvider + public boolean isAllowKerberosAuthentication() { + return Boolean.valueOf(getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION)); + } + + public String getKerberosRealm() { + return getConfig().get("kerberosRealm"); + } + + public String getServerPrincipal() { + return getConfig().get("serverPrincipal"); + } + + public String getKeyTab() { + return getConfig().get("keyTab"); + } + + public boolean getDebug() { + return Boolean.valueOf(getConfig().get("debug")); + } + + protected Map getConfig() { + return providerModel.getConfig(); + } + +} diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java new file mode 100644 index 0000000000..950f9cd38a --- /dev/null +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java @@ -0,0 +1,34 @@ +package org.keycloak.federation.kerberos; + +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; + +/** + * Configuration specific to {@link KerberosFederationProvider} + * + * @author Marek Posolda + */ +public class KerberosConfig extends CommonKerberosConfig { + + public KerberosConfig(UserFederationProviderModel userFederationProvider) { + super(userFederationProvider); + } + + public UserFederationProvider.EditMode getEditMode() { + String editModeString = getConfig().get("editMode"); + if (editModeString == null) { + return UserFederationProvider.EditMode.UNSYNCED; + } else { + return UserFederationProvider.EditMode.valueOf(editModeString); + } + } + + public boolean isAllowPasswordAuthentication() { + return Boolean.valueOf(getConfig().get("allowPasswordAuthentication")); + } + + public boolean isUpdateProfileFirstLogin() { + return Boolean.valueOf(getConfig().get("updateProfileFirstLogin")); + } + +} diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java new file mode 100644 index 0000000000..35f58859d6 --- /dev/null +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -0,0 +1,241 @@ +package org.keycloak.federation.kerberos; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; +import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; +import org.keycloak.models.CredentialValidationOutput; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KerberosConstants; + +/** + * @author Marek Posolda + */ +public class KerberosFederationProvider implements UserFederationProvider { + + private static final Logger logger = Logger.getLogger(KerberosFederationProvider.class); + public static final String KERBEROS_PRINCIPAL = "KERBEROS_PRINCIPAL"; + + protected KeycloakSession session; + protected UserFederationProviderModel model; + protected KerberosConfig kerberosConfig; + protected KerberosFederationProviderFactory factory; + + public KerberosFederationProvider(KeycloakSession session,UserFederationProviderModel model, KerberosFederationProviderFactory factory) { + this.session = session; + this.model = model; + this.kerberosConfig = new KerberosConfig(model); + this.factory = factory; + } + + @Override + public UserModel proxy(UserModel local) { + if (kerberosConfig.getEditMode() == EditMode.READ_ONLY) { + return new ReadOnlyKerberosUserModelDelegate(local, this); + } else { + return local; + } + } + + @Override + public boolean synchronizeRegistrations() { + return false; + } + + @Override + public UserModel register(RealmModel realm, UserModel user) { + return null; + } + + @Override + public boolean removeUser(RealmModel realm, UserModel user) { + // TODO: Not sure if federation provider is expected to delete user in localStorage. Looks rather like a bug in UserFederationManager.removeUser . + return session.userStorage().removeUser(realm, user); + } + + @Override + public UserModel getUserByUsername(RealmModel realm, String username) { + if (username.contains("@")) { + username = username.split("@")[0]; + } + + KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); + if (authenticator.isUserAvailable(username)) { + return findOrCreateAuthenticatedUser(realm, username); + } else { + return null; + } + } + + @Override + public UserModel getUserByEmail(RealmModel realm, String email) { + return null; + } + + @Override + public List searchByAttributes(Map attributes, RealmModel realm, int maxResults) { + return Collections.emptyList(); + } + + @Override + public void preRemove(RealmModel realm) { + + } + + @Override + public void preRemove(RealmModel realm, RoleModel role) { + + } + + @Override + public boolean isValid(UserModel local) { + // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now + + String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm(); + return model.getId().equals(local.getFederationLink()) && kerberosPrincipal.equals(local.getAttribute(KERBEROS_PRINCIPAL)); + } + + @Override + public Set getSupportedCredentialTypes(UserModel local) { + Set supportedCredTypes = new HashSet(); + supportedCredTypes.add(UserCredentialModel.KERBEROS); + + if (kerberosConfig.isAllowPasswordAuthentication()) { + boolean passwordSupported = true; + if (kerberosConfig.getEditMode() == EditMode.UNSYNCED ) { + + // Password from KC database has preference over kerberos password + for (UserCredentialValueModel cred : local.getCredentialsDirectly()) { + if (cred.getType().equals(UserCredentialModel.PASSWORD)) { + passwordSupported = false; + } + } + } + + if (passwordSupported) { + supportedCredTypes.add(UserCredentialModel.PASSWORD); + } + } + + return supportedCredTypes; + } + + @Override + public Set getSupportedCredentialTypes() { + Set supportedCredTypes = new HashSet(); + supportedCredTypes.add(UserCredentialModel.KERBEROS); + return supportedCredTypes; + } + + @Override + public boolean validCredentials(RealmModel realm, UserModel user, List input) { + for (UserCredentialModel cred : input) { + if (cred.getType().equals(UserCredentialModel.PASSWORD)) { + return validPassword(user.getUsername(), cred.getValue()); + } else { + return false; // invalid cred type + } + } + return true; + } + + protected boolean validPassword(String username, String password) { + if (kerberosConfig.isAllowPasswordAuthentication()) { + KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); + return authenticator.validUser(username, password); + } else { + return false; + } + } + + @Override + public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { + return validCredentials(realm, user, Arrays.asList(input)); + } + + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) { + if (credential.getType().equals(UserCredentialModel.KERBEROS)) { + String spnegoToken = credential.getValue(); + SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); + + spnegoAuthenticator.authenticate(); + + if (spnegoAuthenticator.isAuthenticated()) { + Map state = new HashMap(); + state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, spnegoAuthenticator.getDelegationCredential()); + + String username = spnegoAuthenticator.getAuthenticatedUsername(); + UserModel user = findOrCreateAuthenticatedUser(realm, username); + + return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state); + } else { + Map state = new HashMap(); + state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken()); + return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state); + } + + } else { + return CredentialValidationOutput.failed(); + } + } + + @Override + public void close() { + + } + + /** + * Called after successful authentication + * + * @param realm + * @param username username without realm prefix + * @return + */ + protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) { + UserModel user = session.userStorage().getUserByUsername(username, realm); + if (user != null) { + logger.debug("Kerberos authenticated user " + username + " found in Keycloak storage"); + if (!isValid(user)) { + throw new IllegalStateException("User with username " + username + " already exists, but is not linked to provider [" + model.getDisplayName() + + "] or kerberos principal is not correct. Kerberos principal on user is: " + user.getAttribute(KERBEROS_PRINCIPAL)); + } + + return proxy(user); + } else { + return importUserToKeycloak(realm, username); + } + } + + protected UserModel importUserToKeycloak(RealmModel realm, String username) { + // Just guessing email from kerberos realm + String email = username + "@" + kerberosConfig.getKerberosRealm().toLowerCase(); + + logger.info("Creating kerberos user: " + username + ", email: " + email + " to local Keycloak storage"); + UserModel user = session.userStorage().addUser(realm, username); + user.setEnabled(true); + user.setEmail(email); + user.setFederationLink(model.getId()); + user.setAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm()); + + if (kerberosConfig.isUpdateProfileFirstLogin()) { + user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); + } + + return proxy(user); + } +} diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java new file mode 100644 index 0000000000..871587ca0e --- /dev/null +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java @@ -0,0 +1,80 @@ +package org.keycloak.federation.kerberos; + +import java.util.Collections; +import java.util.Date; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.federation.kerberos.impl.KerberosServerSubjectAuthenticator; +import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; +import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderFactory; +import org.keycloak.models.UserFederationProviderModel; + +/** + * Factory for standalone Kerberos federation provider. Standalone means that it's not backed by LDAP. For Kerberos backed by LDAP (like MS AD or ApacheDS environment) + * you should rather use LDAP Federation Provider. + * + * @author Marek Posolda + */ +public class KerberosFederationProviderFactory implements UserFederationProviderFactory { + + private static final Logger logger = Logger.getLogger(KerberosFederationProviderFactory.class); + public static final String PROVIDER_NAME = "kerberos"; + @Override + public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) { + return new KerberosFederationProvider(session, model, this); + } + + @Override + public Set getConfigurationOptions() { + return Collections.emptySet(); + } + + @Override + public String getId() { + return PROVIDER_NAME; + } + + @Override + public void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) { + logger.warn("Sync users not supported for this provider"); + } + + @Override + public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) { + logger.warn("Sync users not supported for this provider"); + } + + @Override + public UserFederationProvider create(KeycloakSession session) { + throw new IllegalAccessError("Illegal to call this method"); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void close() { + + } + + protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) { + KerberosServerSubjectAuthenticator kerberosAuth = createKerberosSubjectAuthenticator(kerberosConfig); + return new SPNEGOAuthenticator(kerberosConfig, kerberosAuth, spnegoToken); + } + + protected KerberosServerSubjectAuthenticator createKerberosSubjectAuthenticator(CommonKerberosConfig kerberosConfig) { + return new KerberosServerSubjectAuthenticator(kerberosConfig); + } + + protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAuthenticator(CommonKerberosConfig kerberosConfig) { + return new KerberosUsernamePasswordAuthenticator(kerberosConfig); + } +} diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java new file mode 100644 index 0000000000..7ac3ca7953 --- /dev/null +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java @@ -0,0 +1,28 @@ +package org.keycloak.federation.kerberos; + +import org.keycloak.models.ModelReadOnlyException; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.UserModelDelegate; + +/** + * @author Marek Posolda + */ +public class ReadOnlyKerberosUserModelDelegate extends UserModelDelegate { + + protected KerberosFederationProvider provider; + + public ReadOnlyKerberosUserModelDelegate(UserModel delegate, KerberosFederationProvider provider) { + super(delegate); + this.provider = provider; + } + + @Override + public void updateCredential(UserCredentialModel cred) { + if (provider.getSupportedCredentialTypes(delegate).contains(cred.getType())) { + throw new ModelReadOnlyException("Can't change password in Keycloak database. Change password with your Kerberos server"); + } + + delegate.updateCredential(cred); + } +} diff --git a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/impl/KerberosServerSubjectAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosServerSubjectAuthenticator.java similarity index 89% rename from broker/kerberos/src/main/java/org/keycloak/broker/kerberos/impl/KerberosServerSubjectAuthenticator.java rename to federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosServerSubjectAuthenticator.java index 3384a4348c..08b3f1d722 100644 --- a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/impl/KerberosServerSubjectAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosServerSubjectAuthenticator.java @@ -1,4 +1,4 @@ -package org.keycloak.broker.kerberos.impl; +package org.keycloak.federation.kerberos.impl; import java.util.HashMap; import java.util.Map; @@ -10,7 +10,7 @@ import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.jboss.logging.Logger; -import org.keycloak.broker.kerberos.KerberosIdentityProviderConfig; +import org.keycloak.federation.kerberos.CommonKerberosConfig; /** * @author Marek Posolda @@ -19,10 +19,10 @@ public class KerberosServerSubjectAuthenticator { private static final Logger logger = Logger.getLogger(KerberosServerSubjectAuthenticator.class); - private final KerberosIdentityProviderConfig config; + private final CommonKerberosConfig config; private LoginContext loginContext; - public KerberosServerSubjectAuthenticator(KerberosIdentityProviderConfig config) { + public KerberosServerSubjectAuthenticator(CommonKerberosConfig config) { this.config = config; } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java new file mode 100644 index 0000000000..6515950d35 --- /dev/null +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java @@ -0,0 +1,126 @@ +package org.keycloak.federation.kerberos.impl; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.jboss.logging.Logger; +import org.keycloak.federation.kerberos.CommonKerberosConfig; + +/** + * @author Marek Posolda + */ +public class KerberosUsernamePasswordAuthenticator { + + private static final Logger logger = Logger.getLogger(KerberosUsernamePasswordAuthenticator.class); + + private final CommonKerberosConfig config; + + public KerberosUsernamePasswordAuthenticator(CommonKerberosConfig config) { + this.config = config; + } + + /** + * Returns true if user with given username exists in kerberos database + * + * @param username username without Kerberos realm attached + * @return true if user available + */ + public boolean isUserAvailable(String username) { + String principal = getKerberosPrincipal(username); + + logger.debug("Checking existence of principal: " + principal); + try { + LoginContext loginContext = new LoginContext("does-not-matter", null, + createJaasCallbackHandler(principal, "fake-password-which-nobody-has"), + createJaasConfiguration()); + + loginContext.login(); + + throw new IllegalStateException("Didn't expect to end here"); + } catch (LoginException le) { + String message = le.getMessage(); + logger.debug("Message from kerberos: " + message); + + // Bit cumbersome, but seems to work with tested kerberos servers + boolean exists = (!message.contains("Client not found")); + return exists; + } + } + + /** + * Returns true if user was successfully authenticated against Kerberos + * + * @param username username without Kerberos realm attached + * @param password kerberos password + * @return true if user was successfully authenticated + */ + public boolean validUser(String username, String password) { + String principal = getKerberosPrincipal(username); + + logger.debug("Validating password of principal: " + principal); + try { + LoginContext loginContext = new LoginContext("does-not-matter", null, + createJaasCallbackHandler(principal, password), + createJaasConfiguration()); + + loginContext.login(); + logger.debug("Principal " + principal + " authenticated succesfully"); + + loginContext.logout(); + return true; + } catch (LoginException le) { + logger.debug("Failed to authenticate user " + username, le); + return false; + } + } + + + protected String getKerberosPrincipal(String username) { + return username + "@" + config.getKerberosRealm(); + } + + protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) { + return new CallbackHandler() { + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + NameCallback nameCallback = (NameCallback) callback; + nameCallback.setName(principal); + } else if (callback instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback) callback; + passwordCallback.setPassword(password.toCharArray()); + } else { + throw new UnsupportedCallbackException(callback, "Unsupported callback: " + callback.getClass().getCanonicalName()); + } + } + } + }; + } + + protected Configuration createJaasConfiguration() { + return new Configuration() { + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map options = new HashMap(); + options.put("storeKey", "true"); + options.put("debug", String.valueOf(config.getDebug())); + AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); + return new AppConfigurationEntry[] { kerberosLMConfiguration }; + } + }; + } +} diff --git a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/impl/SPNEGOAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java similarity index 80% rename from broker/kerberos/src/main/java/org/keycloak/broker/kerberos/impl/SPNEGOAuthenticator.java rename to federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java index 212587a8c2..59983637ab 100644 --- a/broker/kerberos/src/main/java/org/keycloak/broker/kerberos/impl/SPNEGOAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java @@ -1,4 +1,4 @@ -package org.keycloak.broker.kerberos.impl; +package org.keycloak.federation.kerberos.impl; import java.io.IOException; import java.security.PrivilegedActionException; @@ -12,6 +12,7 @@ import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.jboss.logging.Logger; +import org.keycloak.federation.kerberos.CommonKerberosConfig; /** * @author Marek Posolda @@ -24,13 +25,15 @@ public class SPNEGOAuthenticator { private final KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator; private final String spnegoToken; + private final CommonKerberosConfig kerberosConfig; private boolean authenticated = false; - private String principal = null; + private String authenticatedKerberosPrincipal = null; private GSSCredential delegationCredential; private String responseToken = null; - public SPNEGOAuthenticator(KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) { + public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) { + this.kerberosConfig = kerberosConfig; this.kerberosSubjectAuthenticator = kerberosSubjectAuthenticator; this.spnegoToken = spnegoToken; } @@ -61,10 +64,6 @@ public class SPNEGOAuthenticator { return authenticated; } - public String getPrincipal() { - return principal; - } - public String getResponseToken() { return responseToken; } @@ -73,6 +72,19 @@ public class SPNEGOAuthenticator { return delegationCredential; } + /** + * @return username to be used in Keycloak. Username is authenticated kerberos principal without realm name + */ + public String getAuthenticatedUsername() { + String[] tokens = authenticatedKerberosPrincipal.split("@"); + String username = tokens[0]; + if (!tokens[1].equalsIgnoreCase(kerberosConfig.getKerberosRealm())) { + throw new IllegalStateException("Invalid kerberos realm. Realm from the ticket: " + tokens[1] + ", configured realm: " + kerberosConfig.getKerberosRealm()); + } + return username; + } + + private class AcceptSecContext implements PrivilegedExceptionAction { @Override @@ -87,7 +99,7 @@ public class SPNEGOAuthenticator { logAuthDetails(gssContext); if (gssContext.isEstablished()) { - principal = gssContext.getSrcName().toString(); + authenticatedKerberosPrincipal = gssContext.getSrcName().toString(); // What should be done with delegation credential? Figure out if there are use-cases for storing it as claims in FederatedIdentity if (gssContext.getCredDelegState()) { @@ -107,6 +119,7 @@ public class SPNEGOAuthenticator { } + protected GSSContext establishContext() throws GSSException, IOException { GSSContext gssContext = GSS_MANAGER.createContext((GSSCredential) null); @@ -117,6 +130,7 @@ public class SPNEGOAuthenticator { return gssContext; } + protected void logAuthDetails(GSSContext gssContext) throws GSSException { if (log.isDebugEnabled()) { String message = new StringBuilder("SPNEGO Security context accepted with token: " + responseToken) diff --git a/federation/kerberos/src/main/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory b/federation/kerberos/src/main/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory new file mode 100644 index 0000000000..040f5c972e --- /dev/null +++ b/federation/kerberos/src/main/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory @@ -0,0 +1 @@ +org.keycloak.federation.kerberos.KerberosFederationProviderFactory \ No newline at end of file diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml index 7149e43462..a449335c8b 100755 --- a/federation/ldap/pom.xml +++ b/federation/ldap/pom.xml @@ -25,6 +25,12 @@ ${project.version} provided + + org.keycloak + keycloak-kerberos-federation + ${project.version} + provided + org.keycloak keycloak-picketlink-api diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index a49a989b24..9a926041ca 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -1,6 +1,10 @@ package org.keycloak.federation.ldap; import org.jboss.logging.Logger; +import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; +import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; +import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; +import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; @@ -10,6 +14,7 @@ import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KerberosConstants; import org.picketlink.idm.IdentityManagementException; import org.picketlink.idm.IdentityManager; import org.picketlink.idm.PartitionManager; @@ -17,7 +22,7 @@ import org.picketlink.idm.model.basic.BasicModel; import org.picketlink.idm.model.basic.User; import org.picketlink.idm.query.IdentityQuery; -import java.util.Collections; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -36,28 +41,32 @@ public class LDAPFederationProvider implements UserFederationProvider { public static final String SYNC_REGISTRATIONS = "syncRegistrations"; public static final String EDIT_MODE = "editMode"; + protected LDAPFederationProviderFactory factory; protected KeycloakSession session; protected UserFederationProviderModel model; protected PartitionManager partitionManager; protected EditMode editMode; + protected LDAPProviderKerberosConfig kerberosConfig; - protected static final Set supportedCredentialTypes = new HashSet(); + protected final Set supportedCredentialTypes = new HashSet(); - static - { - supportedCredentialTypes.add(UserCredentialModel.PASSWORD); - } - - public LDAPFederationProvider(KeycloakSession session, UserFederationProviderModel model, PartitionManager partitionManager) { + public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, PartitionManager partitionManager) { + this.factory = factory; this.session = session; this.model = model; this.partitionManager = partitionManager; + this.kerberosConfig = new LDAPProviderKerberosConfig(model); String editModeString = model.getConfig().get(EDIT_MODE); if (editModeString == null) { editMode = EditMode.READ_ONLY; } else { editMode = EditMode.valueOf(editModeString); } + + supportedCredentialTypes.add(UserCredentialModel.PASSWORD); + if (kerberosConfig.isAllowKerberosAuthentication()) { + supportedCredentialTypes.add(UserCredentialModel.KERBEROS); + } } private ModelException convertIDMException(IdentityManagementException ie) { @@ -97,16 +106,23 @@ public class LDAPFederationProvider implements UserFederationProvider { @Override public Set getSupportedCredentialTypes(UserModel local) { + Set supportedCredentialTypes = new HashSet(this.supportedCredentialTypes); if (editMode == EditMode.UNSYNCED ) { for (UserCredentialValueModel cred : local.getCredentialsDirectly()) { if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - return Collections.emptySet(); + // User has changed password in KC local database. Use KC password instead of LDAP password + supportedCredentialTypes.remove(UserCredentialModel.PASSWORD); } } } return supportedCredentialTypes; } + @Override + public Set getSupportedCredentialTypes() { + return new HashSet(this.supportedCredentialTypes); + } + @Override public boolean synchronizeRegistrations() { return "true".equalsIgnoreCase(model.getConfig().get(SYNC_REGISTRATIONS)) && editMode == EditMode.WRITABLE; @@ -244,6 +260,8 @@ public class LDAPFederationProvider implements UserFederationProvider { imported.setLastName(picketlinkUser.getLastName()); imported.setFederationLink(model.getId()); imported.setAttribute(LDAP_ID, picketlinkUser.getId()); + + logger.debugf("Added new user from LDAP. Username: " + imported.getUsername() + ", Email: ", imported.getEmail() + ", LDAP_ID: " + picketlinkUser.getId()); return proxy(imported); } @@ -285,10 +303,17 @@ public class LDAPFederationProvider implements UserFederationProvider { } public boolean validPassword(String username, String password) { - try { - return LDAPUtils.validatePassword(partitionManager, username, password); - } catch (IdentityManagementException ie) { - throw convertIDMException(ie); + if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) { + // Use Kerberos JAAS (Krb5LoginModule) + KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); + return authenticator.validUser(username, password); + } else { + // Use Naming LDAP API + try { + return LDAPUtils.validatePassword(partitionManager, username, password); + } catch (IdentityManagementException ie) { + throw convertIDMException(ie); + } } } @@ -307,14 +332,37 @@ public class LDAPFederationProvider implements UserFederationProvider { @Override public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - for (UserCredentialModel cred : input) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - return validPassword(user.getUsername(), cred.getValue()); - } else { - return false; // invalid cred type + return validCredentials(realm, user, Arrays.asList(input)); + } + + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) { + if (credential.getType().equals(UserCredentialModel.KERBEROS)) { + if (kerberosConfig.isAllowKerberosAuthentication()) { + String spnegoToken = credential.getValue(); + SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); + + spnegoAuthenticator.authenticate(); + + if (spnegoAuthenticator.isAuthenticated()) { + Map state = new HashMap(); + state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, spnegoAuthenticator.getDelegationCredential()); + + // TODO: This assumes that LDAP "uid" is equal to kerberos principal name. Like uid "hnelson" and kerberos principal "hnelson@KEYCLOAK.ORG". + // Check if it's correct or if LDAP attribute for mapping kerberos principal should be available (For ApacheDS it seems to be attribute "krb5PrincipalName" but on MSAD it's likely different) + String username = spnegoAuthenticator.getAuthenticatedUsername(); + UserModel user = findOrCreateAuthenticatedUser(realm, username); + + return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state); + } else { + Map state = new HashMap(); + state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken()); + return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state); + } } } - return true; + + return CredentialValidationOutput.failed(); } @Override @@ -330,7 +378,6 @@ public class LDAPFederationProvider implements UserFederationProvider { if (currentUser == null) { // Add new user to Keycloak importUserFromPicketlink(realm, picketlinkUser); - logger.debugf("Added new user from LDAP: %s", username); } else { if ((fedModel.getId().equals(currentUser.getFederationLink())) && (picketlinkUser.getId().equals(currentUser.getAttribute(LDAPFederationProvider.LDAP_ID)))) { // Update keycloak user @@ -345,4 +392,27 @@ public class LDAPFederationProvider implements UserFederationProvider { } } } + + /** + * Called after successful kerberos authentication + * + * @param realm + * @param username username without realm prefix + * @return + */ + protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) { + UserModel user = session.userStorage().getUserByUsername(username, realm); + if (user != null) { + logger.debug("Kerberos authenticated user " + username + " found in Keycloak storage"); + if (!isValid(user)) { + throw new IllegalStateException("User with username " + username + " already exists, but is not linked to provider [" + model.getDisplayName() + + "] or LDAP_ID is not correct. LDAP_ID on user is: " + user.getAttribute(LDAP_ID)); + } + + return proxy(user); + } else { + // Creating user to local storage + return getUserByUsername(realm, username); + } + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index 44987e96f5..16a877f681 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -2,6 +2,11 @@ package org.keycloak.federation.ldap; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.federation.kerberos.KerberosConfig; +import org.keycloak.federation.kerberos.impl.KerberosServerSubjectAuthenticator; +import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; +import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; @@ -45,7 +50,7 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact public LDAPFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) { PartitionManagerProvider idmProvider = session.getProvider(PartitionManagerProvider.class); PartitionManager partition = idmProvider.getPartitionManager(model); - return new LDAPFederationProvider(session, model, partition); + return new LDAPFederationProvider(this, session, model, partition); } @Override @@ -140,4 +145,17 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel); ldapFedProvider.importPicketlinkUsers(realm, users, fedModel); } + + protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) { + KerberosServerSubjectAuthenticator kerberosAuth = createKerberosSubjectAuthenticator(kerberosConfig); + return new SPNEGOAuthenticator(kerberosConfig, kerberosAuth, spnegoToken); + } + + protected KerberosServerSubjectAuthenticator createKerberosSubjectAuthenticator(CommonKerberosConfig kerberosConfig) { + return new KerberosServerSubjectAuthenticator(kerberosConfig); + } + + protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAuthenticator(CommonKerberosConfig kerberosConfig) { + return new KerberosUsernamePasswordAuthenticator(kerberosConfig); + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java new file mode 100644 index 0000000000..b98c5cae71 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java @@ -0,0 +1,20 @@ +package org.keycloak.federation.ldap.kerberos; + +import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.models.UserFederationProviderModel; + +/** + * Configuration specific to {@link org.keycloak.federation.ldap.LDAPFederationProvider} + * + * @author Marek Posolda + */ +public class LDAPProviderKerberosConfig extends CommonKerberosConfig { + + public LDAPProviderKerberosConfig(UserFederationProviderModel userFederationProvider) { + super(userFederationProvider); + } + + public boolean isUseKerberosForPasswordAuthentication() { + return Boolean.valueOf(getConfig().get("useKerberosForPasswordAuthentication")); + } +} diff --git a/federation/pom.xml b/federation/pom.xml index b990d51f3b..7c1487a532 100755 --- a/federation/pom.xml +++ b/federation/pom.xml @@ -17,6 +17,7 @@ ldap + kerberos diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js index ff854afef9..054c218279 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js @@ -875,6 +875,36 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'LDAPCtrl' }) + .when('/realms/:realm/user-federation/providers/kerberos/:instance', { + templateUrl : 'partials/federated-kerberos.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + instance : function(UserFederationInstanceLoader) { + return UserFederationInstanceLoader(); + }, + providerFactory : function() { + return { id: "kerberos" }; + } + }, + controller : 'GenericUserFederationCtrl' + }) + .when('/create/user-federation/:realm/providers/kerberos', { + templateUrl : 'partials/federated-kerberos.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + instance : function() { + return {}; + }, + providerFactory : function() { + return { id: "kerberos" }; + } + }, + controller : 'GenericUserFederationCtrl' + }) .when('/create/user-federation/:realm/providers/:provider', { templateUrl : 'partials/federated-generic.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js index 5c25e1b01b..52b313a3ae 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js @@ -435,7 +435,7 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $scope.userCredentialOptions = { 'multiple' : true, 'simple_tags' : true, - 'tags' : ['password', 'totp', 'cert'] + 'tags' : ['password', 'totp', 'cert', 'kerberos'] }; $scope.changed = false; @@ -653,8 +653,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload $scope.identityProvider.name = providerFactory.name; $scope.identityProvider.enabled = true; $scope.identityProvider.updateProfileFirstLogin = true; - // Kerberos is suggested as default provider, others not - $scope.identityProvider.authenticateByDefault = (providerFactory.id === "kerberos"); + $scope.identityProvider.authenticateByDefault = false; $scope.newIdentityProvider = true; } diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js index 1f36f90661..fb0ce1bef1 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js @@ -378,9 +378,23 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif instance.priority = 0; $scope.fullSyncEnabled = false; $scope.changedSyncEnabled = false; + + if (providerFactory.id === 'kerberos') { + instance.config.debug = false; + instance.config.allowPasswordAuthentication = true; + instance.config.editMode = 'UNSYNCED'; + instance.config.updateProfileFirstLogin = true; + instance.config.allowKerberosAuthentication = true; + } } else { $scope.fullSyncEnabled = (instance.fullSyncPeriod && instance.fullSyncPeriod > 0); $scope.changedSyncEnabled = (instance.changedSyncPeriod && instance.changedSyncPeriod > 0); + + if (providerFactory.id === 'kerberos') { + instance.config.debug = (instance.config.debug === 'true' || instance.config.debug === true); + instance.config.allowPasswordAuthentication = (instance.config.allowPasswordAuthentication === 'true' || instance.config.allowPasswordAuthentication === true); + instance.config.updateProfileFirstLogin = (instance.config.updateProfileFirstLogin === 'true' || instance.config.updateProfileFirstLogin === true); + } } $scope.changed = false; @@ -488,25 +502,30 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog, instance.providerName = "ldap"; instance.config = {}; instance.priority = 0; - $scope.syncRegistrations = false; - $scope.userAccountControlsAfterPasswordUpdate = true; - instance.config.userAccountControlsAfterPasswordUpdate = "true"; + instance.config.syncRegistrations = false; + instance.config.userAccountControlsAfterPasswordUpdate = true; + instance.config.connectionPooling = true; + instance.config.pagination = true; - $scope.connectionPooling = true; - instance.config.connectionPooling = "true"; + instance.config.allowKerberosAuthentication = false; + instance.config.debug = false; + instance.config.useKerberosForPasswordAuthentication = false; - $scope.pagination = true; - instance.config.pagination = "true"; instance.config.batchSizeForSync = DEFAULT_BATCH_SIZE; $scope.fullSyncEnabled = false; $scope.changedSyncEnabled = false; } else { - $scope.syncRegistrations = instance.config.syncRegistrations && instance.config.syncRegistrations == "true"; - $scope.userAccountControlsAfterPasswordUpdate = instance.config.userAccountControlsAfterPasswordUpdate && instance.config.userAccountControlsAfterPasswordUpdate == "true"; - $scope.connectionPooling = instance.config.connectionPooling && instance.config.connectionPooling == "true"; - $scope.pagination = instance.config.pagination && instance.config.pagination == "true"; + instance.config.syncRegistrations = (instance.config.syncRegistrations === 'true' || instance.config.syncRegistrations === true); + instance.config.userAccountControlsAfterPasswordUpdate = (instance.config.userAccountControlsAfterPasswordUpdate === 'true' || instance.config.userAccountControlsAfterPasswordUpdate === true); + instance.config.connectionPooling = (instance.config.connectionPooling === 'true' || instance.config.connectionPooling === true); + instance.config.pagination = (instance.config.pagination === 'true' || instance.config.pagination === true); + + instance.config.allowKerberosAuthentication = (instance.config.allowKerberosAuthentication === 'true' || instance.config.allowKerberosAuthentication === true); + instance.config.debug = (instance.config.debug === 'true' || instance.config.debug === true); + instance.config.useKerberosForPasswordAuthentication = (instance.config.useKerberosForPasswordAuthentication === 'true' || instance.config.useKerberosForPasswordAuthentication === true); + if (!instance.config.batchSizeForSync) { instance.config.batchSizeForSync = DEFAULT_BATCH_SIZE; } @@ -534,21 +553,6 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog, $scope.realm = realm; - function watchBooleanProperty(propertyName) { - $scope.$watch(propertyName, function() { - if ($scope[propertyName]) { - $scope.instance.config[propertyName] = "true"; - } else { - $scope.instance.config[propertyName] = "false"; - } - }) - } - - watchBooleanProperty('syncRegistrations'); - watchBooleanProperty('userAccountControlsAfterPasswordUpdate'); - watchBooleanProperty('connectionPooling'); - watchBooleanProperty('pagination'); - $scope.$watch('fullSyncEnabled', function(newVal, oldVal) { if (oldVal == newVal) { return; diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-kerberos.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-kerberos.html new file mode 100644 index 0000000000..1b30221c36 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-kerberos.html @@ -0,0 +1,118 @@ +
+
+ +
+ + +

Kerberos Provider Settings

+

Add Standalone Kerberos Provider

+

* Required fields

+ + +
+ Required Settings +
+ +
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ + + +
+ +
+
\ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html index 69c53e2862..66ff5c2d07 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html @@ -58,7 +58,7 @@
- +
@@ -139,27 +139,73 @@
- +
- +
- +
+
+ Kerberos integration +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
Sync settings
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-kerberos.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-kerberos.html deleted file mode 100644 index b061234563..0000000000 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-kerberos.html +++ /dev/null @@ -1,78 +0,0 @@ -
-
- -

-
- -

{{identityProvider.name}} Provider Settings

-

* Required fields

-
-
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- - -
-
-
-
diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties index c7e1fff9f2..5c5d1f7e92 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties @@ -98,8 +98,7 @@ actionPasswordWarning=You need to change your password to activate your account. actionEmailWarning=You need to verify your email address to activate your account. actionFollow=Please fill in the fields below. -errorKerberosLogin=Kerberos ticket not available. Use different login mechanism -errorKerberosLinkAccount=Kerberos ticket not available. +errorKerberosLogin=Kerberos ticket not available. Authenticate with password. successHeader=Success! errorHeader=Error! diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index 0d1f931268..45f3adb1b8 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -54,6 +54,8 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setQueryParams(MultivaluedMap queryParams); + public LoginFormsProvider setResponseHeader(String headerName, String headerValue); + public LoginFormsProvider setFormData(MultivaluedMap formData); public LoginFormsProvider setStatus(Response.Status status); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 1360800ce9..44a7585004 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -57,6 +57,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private List realmRolesRequested; private MultivaluedMap resourceRolesRequested; private MultivaluedMap queryParams; + private Map httpResponseHeaders = new HashMap(); private String accessRequestMessage; private URI actionUri; @@ -226,6 +227,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme); Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result); BrowserSecurityHeaderSetup.headers(builder, realm); + for (Map.Entry entry : httpResponseHeaders.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); @@ -335,6 +339,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override + public LoginFormsProvider setResponseHeader(String headerName, String headerValue) { + this.httpResponseHeaders.put(headerName, headerValue); + return this; + } + @Override public void close() { } diff --git a/model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java b/model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java new file mode 100644 index 0000000000..70e4093ab5 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java @@ -0,0 +1,46 @@ +package org.keycloak.models; + +import java.util.HashMap; +import java.util.Map; + +/** + * Output of credential validation + * + * @author Marek Posolda + */ +public class CredentialValidationOutput { + + private final UserModel authenticatedUser; // authenticated user. + private final Status authStatus; // status whether user is authenticated or more steps needed + private final Map state; // Additional state related to authentication. It can contain data to be sent back to client or data about used credentials. + + public CredentialValidationOutput(UserModel authenticatedUser, Status authStatus, Map state) { + this.authenticatedUser = authenticatedUser; + this.authStatus = authStatus; + this.state = state; + } + + public static CredentialValidationOutput failed() { + return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FAILED, new HashMap()); + } + + public UserModel getAuthenticatedUser() { + return authenticatedUser; + } + + public Status getAuthStatus() { + return authStatus; + } + + public Map getState() { + return state; + } + + public CredentialValidationOutput merge(CredentialValidationOutput that) { + throw new IllegalStateException("Not supported yet"); + } + + public static enum Status { + AUTHENTICATED, FAILED, CONTINUE + } +} diff --git a/model/api/src/main/java/org/keycloak/models/RequiredCredentialModel.java b/model/api/src/main/java/org/keycloak/models/RequiredCredentialModel.java index 9d77a7a29f..0840b04f39 100755 --- a/model/api/src/main/java/org/keycloak/models/RequiredCredentialModel.java +++ b/model/api/src/main/java/org/keycloak/models/RequiredCredentialModel.java @@ -54,6 +54,7 @@ public class RequiredCredentialModel { public static final RequiredCredentialModel TOTP; public static final RequiredCredentialModel CLIENT_CERT; public static final RequiredCredentialModel SECRET; + public static final RequiredCredentialModel KERBEROS; static { Map map = new HashMap(); @@ -81,6 +82,12 @@ public class RequiredCredentialModel { CLIENT_CERT.setSecret(false); CLIENT_CERT.setFormLabel("clientCertificate"); map.put(CLIENT_CERT.getType(), CLIENT_CERT); + KERBEROS = new RequiredCredentialModel(); + KERBEROS.setType(UserCredentialModel.KERBEROS); + KERBEROS.setInput(false); + KERBEROS.setSecret(false); + KERBEROS.setFormLabel("kerberos"); + map.put(KERBEROS.getType(), KERBEROS); BUILT_IN = Collections.unmodifiableMap(map); } } diff --git a/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java b/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java index 2f1fbf7aa2..5fb60050eb 100755 --- a/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java @@ -14,6 +14,7 @@ public class UserCredentialModel { public static final String SECRET = "secret"; public static final String TOTP = "totp"; public static final String CLIENT_CERT = "cert"; + public static final String KERBEROS = "kerberos"; protected String type; protected String value; @@ -49,6 +50,13 @@ public class UserCredentialModel { return model; } + public static UserCredentialModel kerberos(String token) { + UserCredentialModel model = new UserCredentialModel(); + model.setType(KERBEROS); + model.setValue(token); + return model; + } + public static UserCredentialModel generateSecret() { UserCredentialModel model = new UserCredentialModel(); model.setType(SECRET); diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index 1b3bdd1ca5..7d3ecf470b 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -3,6 +3,7 @@ package org.keycloak.models; import org.jboss.logging.Logger; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -355,27 +356,40 @@ public class UserFederationManager implements UserProvider { @Override public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - UserFederationProvider link = getFederationLink(realm, user); - if (link != null) { - validateUser(realm, user); - Set supportedCredentialTypes = link.getSupportedCredentialTypes(user); - if (supportedCredentialTypes.size() > 0) { - List fedCreds = new ArrayList(); - List localCreds = new ArrayList(); - for (UserCredentialModel cred : input) { - if (supportedCredentialTypes.contains(cred.getType())) { - fedCreds.add(cred); - } else { - localCreds.add(cred); - } - } - if (!link.validCredentials(realm, user, fedCreds)) { - return false; - } - return session.userStorage().validCredentials(realm, user, localCreds); - } + return validCredentials(realm, user, Arrays.asList(input)); + } + + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input) { + List fedProviderModels = realm.getUserFederationProviders(); + List fedProviders = new ArrayList(); + for (UserFederationProviderModel fedProviderModel : fedProviderModels) { + fedProviders.add(getFederationProvider(fedProviderModel)); } - return session.userStorage().validCredentials(realm, user, input); + + CredentialValidationOutput result = null; + for (UserCredentialModel cred : input) { + UserFederationProvider providerSupportingCreds = null; + + // Find provider, which supports required credential type + for (UserFederationProvider fedProvider : fedProviders) { + if (fedProvider.getSupportedCredentialTypes().contains(cred.getType())) { + providerSupportingCreds = fedProvider; + break; + } + } + + if (providerSupportingCreds == null) { + logger.warn("Don't have provider supporting credentials of type " + cred.getType()); + return CredentialValidationOutput.failed(); + } + + CredentialValidationOutput currentResult = providerSupportingCreds.validCredentials(realm, cred); + result = (result == null) ? currentResult : result.merge(currentResult); + } + + // For now, validCredentials(realm, input) is not supported for local userProviders + return (result != null) ? result : CredentialValidationOutput.failed(); } @Override diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java index 16e81b5412..948e8c9185 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java @@ -130,6 +130,14 @@ public interface UserFederationProvider extends Provider { */ Set getSupportedCredentialTypes(UserModel user); + /** + * What UserCredentialModel types should be handled by this provider? This is called in scenarios when we don't know user, + * who is going to authenticate (For example Kerberos authentication). + * + * @return + */ + Set getSupportedCredentialTypes(); + /** * Validate credentials for this user. This method will only be called with credential parameters supported * by this provider @@ -141,6 +149,15 @@ public interface UserFederationProvider extends Provider { */ boolean validCredentials(RealmModel realm, UserModel user, List input); boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input); + + /** + * Validate credentials of unknown user. The authenticated user is recognized based on provided credentials and returned back in CredentialValidationOutput + * @param realm + * @param input + * @return + */ + CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential); + void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index 0d0a25b4a6..b508638ea4 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -43,5 +43,7 @@ public interface UserProvider extends Provider { boolean validCredentials(RealmModel realm, UserModel user, List input); boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input); + CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input); + void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KerberosConstants.java b/model/api/src/main/java/org/keycloak/models/utils/KerberosConstants.java new file mode 100644 index 0000000000..6f8a830a1f --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/KerberosConstants.java @@ -0,0 +1,38 @@ +package org.keycloak.models.utils; + +/** + * @author Marek Posolda + */ +public class KerberosConstants { + + /** + * Value of HTTP Headers "WWW-Authenticate" or "Authorization" used for SPNEGO/Kerberos + **/ + public static final String NEGOTIATE = "Negotiate"; + + /** + * OID of SPNEGO mechanism. See http://www.oid-info.com/get/1.3.6.1.5.5.2 + */ + public static final String SPNEGO_OID = "1.3.6.1.5.5.2"; + + /** + * OID of Kerberos v5 mechanism. See http://www.oid-info.com/get/1.2.840.113554.1.2.2 + */ + public static final String KRB5_OID = "1.2.840.113554.1.2.2"; + + /** + * Configuration federation provider model attribute. It's always true for KerberosFederationProvider and configurable for LDAPFederationProvider + */ + public static final String ALLOW_KERBEROS_AUTHENTICATION = "allowKerberosAuthentication"; + + /** + * Internal attribute used in "state" map . Contains token to be passed in HTTP Response back to browser to continue handshake + */ + public static final String RESPONSE_TOKEN = "SpnegoResponseToken"; + + /** + * Internal attribute used in "state" map . Contains credential from SPNEGO/Kerberos successful authentication + */ + public static final String GSS_DELEGATION_CREDENTIAL = "GssDelegationCredential"; + +} diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java index 017a26e2bd..8dbb0449ce 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java @@ -1,5 +1,6 @@ package org.keycloak.models.cache; +import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.RealmModel; @@ -282,6 +283,11 @@ public class DefaultCacheUserProvider implements CacheUserProvider { return getDelegate().validCredentials(realm, user, input); } + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input) { + return getDelegate().validCredentials(realm, input); + } + @Override public void preRemove(RealmModel realm) { realmInvalidations.add(realm.getId()); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java index e468b16ca0..857b3e81a6 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java @@ -1,5 +1,6 @@ package org.keycloak.models.cache; +import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -155,6 +156,11 @@ public class NoCacheUserProvider implements CacheUserProvider { return getDelegate().validCredentials(realm, user, input); } + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input) { + return getDelegate().validCredentials(realm, input); + } + @Override public void preRemove(RealmModel realm) { getDelegate().preRemove(realm); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 438c700b49..c94c068fb9 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -1,6 +1,7 @@ package org.keycloak.models.jpa; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -375,4 +376,10 @@ public class JpaUserProvider implements UserProvider { public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { return CredentialValidation.validCredentials(realm, user, input); } + + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input) { + // Not supported yet + return null; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 94227112d0..5babac03c9 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -6,6 +6,7 @@ import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.MongoStore; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -378,4 +379,10 @@ public class MongoUserProvider implements UserProvider { public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { return CredentialValidation.validCredentials(realm, user, input); } + + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input) { + // Not supported yet + return null; + } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java index fd20b16e8e..acf39cf473 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java @@ -41,6 +41,7 @@ import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.managers.HttpAuthenticationManager; import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.flows.Flows; @@ -883,6 +884,11 @@ public class OpenIDConnectService { response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event); if (response != null) return response; + // SPNEGO/Kerberos authentication TODO: This should be somehow pluggable instead of hardcoded this way (Authentication interceptors?) + HttpAuthenticationManager httpAuthManager = new HttpAuthenticationManager(session, clientSession, realm, uriInfo, request, clientConnection, event); + HttpAuthenticationManager.HttpAuthOutput httpAuthOutput = httpAuthManager.spnegoAuthenticate(); + if (httpAuthOutput.getResponse() != null) return httpAuthOutput.getResponse(); + if (prompt != null && prompt.equals("none")) { OpenIDConnect oauth = new OpenIDConnect(session, realm, uriInfo); return oauth.cancelLogin(clientSession); @@ -911,6 +917,11 @@ public class OpenIDConnectService { LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) .setClientSessionCode(accessCode); + // Attach state from SPNEGO authentication + if (httpAuthOutput.getChallenge() != null) { + httpAuthOutput.getChallenge().sendChallenge(forms); + } + String rememberMeUsername = AuthenticationManager.getRememberMeUsername(realm, headers); if (loginHint != null || rememberMeUsername != null) { diff --git a/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationChallenge.java b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationChallenge.java new file mode 100644 index 0000000000..0be8bcc27d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationChallenge.java @@ -0,0 +1,11 @@ +package org.keycloak.services.managers; + +import org.keycloak.login.LoginFormsProvider; + +/** + * @author Marek Posolda + */ +public interface HttpAuthenticationChallenge { + + void addChallenge(LoginFormsProvider loginFormsProvider); +} diff --git a/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java new file mode 100644 index 0000000000..ca0488e27b --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java @@ -0,0 +1,167 @@ +package org.keycloak.services.managers; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.ClientConnection; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.CredentialValidationOutput; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KerberosConstants; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.flows.Flows; + +/** + * Handle HTTP authentication types requiring complex handshakes with multiple HTTP request/responses + * + * @author Marek Posolda + */ +public class HttpAuthenticationManager { + + private static final Logger logger = Logger.getLogger(HttpAuthenticationManager.class); + + private KeycloakSession session; + private RealmModel realm; + private UriInfo uriInfo; + private HttpRequest request; + private EventBuilder event; + private ClientConnection clientConnection; + private ClientSessionModel clientSession; + + public HttpAuthenticationManager(KeycloakSession session, ClientSessionModel clientSession, RealmModel realm, UriInfo uriInfo, + HttpRequest request, + ClientConnection clientConnection, + EventBuilder event) { + this.session = session; + this.realm = realm; + this.uriInfo = uriInfo; + this.request = request; + this.event = event; + this.clientConnection = clientConnection; + this.clientSession = clientSession; + } + + + public HttpAuthOutput spnegoAuthenticate() { + boolean kerberosSupported = false; + for (RequiredCredentialModel c : realm.getRequiredCredentials()) { + if (c.getType().equals(CredentialRepresentation.KERBEROS)) { + logger.debug("Kerberos authentication is supported"); + kerberosSupported = true; + } + } + + if (!kerberosSupported) { + return new HttpAuthOutput(null, null); + } + + String authHeader = request.getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); + + // Case when we don't yet have any Negotiate header + if (authHeader == null) { + return challengeNegotiation(null); + } + + String[] tokens = authHeader.split(" "); + if (tokens.length != 2) { + logger.warn("Invalid length of tokens: " + tokens.length); + return challengeNegotiation(null); + } else if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) { + logger.warn("Unknown scheme " + tokens[0]); + return challengeNegotiation(null); + } else { + String spnegoToken = tokens[1]; + UserCredentialModel spnegoCredential = UserCredentialModel.kerberos(spnegoToken); + + CredentialValidationOutput output = session.users().validCredentials(realm, spnegoCredential); + + if (output.getAuthStatus() == CredentialValidationOutput.Status.AUTHENTICATED) { + return sendResponse(output.getAuthenticatedUser(), "spnego"); + } else { + String spnegoResponseToken = (String) output.getState().get(KerberosConstants.RESPONSE_TOKEN); + return challengeNegotiation(spnegoResponseToken); + } + } + } + + + // Send response after successful authentication + private HttpAuthOutput sendResponse(UserModel user, String authMethod) { + Response response; + if (!user.isEnabled()) { + event.error(Errors.USER_DISABLED); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, Messages.ACCOUNT_DISABLED); + } else { + UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), authMethod, false); + TokenManager.attachClientSession(userSession, clientSession); + event.session(userSession); + response = AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); + } + + return new HttpAuthOutput(response, null); + } + + + private HttpAuthOutput challengeNegotiation(final String negotiateToken) { + return new HttpAuthOutput(null, new HttpAuthChallenge() { + + @Override + public void sendChallenge(LoginFormsProvider loginFormsProvider) { + String negotiateHeader = negotiateToken == null ? KerberosConstants.NEGOTIATE : KerberosConstants.NEGOTIATE + " " + negotiateToken; + + if (logger.isTraceEnabled()) { + logger.trace("Sending back " + HttpHeaders.WWW_AUTHENTICATE + ": " + negotiateHeader); + } + + loginFormsProvider.setStatus(Response.Status.UNAUTHORIZED); + loginFormsProvider.setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader); + loginFormsProvider.setWarning("errorKerberosLogin"); + } + + }); + } + + + public class HttpAuthOutput { + + // It's non-null if we want to immediately send response to user + private final Response response; + + // It's non-null if challenge should be attached to rendered login form + private final HttpAuthChallenge challenge; + + public HttpAuthOutput(Response response, HttpAuthChallenge challenge) { + this.response = response; + this.challenge = challenge; + } + + public Response getResponse() { + return response; + } + + public HttpAuthChallenge getChallenge() { + return challenge; + } + } + + + public interface HttpAuthChallenge { + + void sendChallenge(LoginFormsProvider loginFormsProvider); + + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java index 751f0929db..730252cda1 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java @@ -5,9 +5,12 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.utils.KerberosConstants; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation; @@ -118,6 +121,7 @@ public class UserFederationResource { UserFederationProviderModel model = realm.addUserFederationProvider(rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName, rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync()); new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); + checkKerberosCredential(model); return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } @@ -141,6 +145,7 @@ public class UserFederationResource { rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync()); realm.updateUserFederationProvider(model); new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); + checkKerberosCredential(model); } /** @@ -223,5 +228,23 @@ public class UserFederationResource { throw new NotFoundException("could not find provider"); } + // Automatically add "kerberos" to required realm credentials if it's supported by saved provider + private void checkKerberosCredential(UserFederationProviderModel model) { + String allowKerberosCfg = model.getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION); + if (Boolean.valueOf(allowKerberosCfg)) { + boolean found = false; + List currentCreds = realm.getRequiredCredentials(); + for (RequiredCredentialModel cred : currentCreds) { + if (cred.getType().equals(UserCredentialModel.KERBEROS)) { + found = true; + } + } + + if (!found) { + realm.addRequiredCredential(UserCredentialModel.KERBEROS); + logger.info("Added 'kerberos' to required realm credentials"); + } + } + } } diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java index 753409a1eb..99644214bc 100755 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java @@ -1,5 +1,6 @@ package org.keycloak.testutils; +import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -76,6 +77,11 @@ public class DummyUserFederationProvider implements UserFederationProvider { return Collections.emptySet(); } + @Override + public Set getSupportedCredentialTypes() { + return Collections.emptySet(); + } + @Override public boolean validCredentials(RealmModel realm, UserModel user, List input) { return false; @@ -86,6 +92,11 @@ public class DummyUserFederationProvider implements UserFederationProvider { return false; } + @Override + public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) { + return CredentialValidationOutput.failed(); + } + @Override public void close() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java index 45f638cac6..2dd3164575 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java @@ -18,7 +18,6 @@ package org.keycloak.testsuite.broker; import org.junit.Before; -import org.keycloak.broker.kerberos.KerberosIdentityProviderFactory; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.saml.SAMLIdentityProviderFactory; import org.keycloak.social.facebook.FacebookIdentityProviderFactory; @@ -48,7 +47,6 @@ public abstract class AbstractIdentityProviderModelTest extends AbstractModelTes this.expectedProviders.add(FacebookIdentityProviderFactory.PROVIDER_ID); this.expectedProviders.add(GitHubIdentityProviderFactory.PROVIDER_ID); this.expectedProviders.add(TwitterIdentityProviderFactory.PROVIDER_ID); - this.expectedProviders.add(KerberosIdentityProviderFactory.PROVIDER_ID); this.expectedProviders = Collections.unmodifiableSet(this.expectedProviders); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java index 9b9d72367a..36d2bda835 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java @@ -18,9 +18,6 @@ package org.keycloak.testsuite.broker; import org.junit.Test; -import org.keycloak.broker.kerberos.KerberosIdentityProvider; -import org.keycloak.broker.kerberos.KerberosIdentityProviderConfig; -import org.keycloak.broker.kerberos.KerberosIdentityProviderFactory; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; @@ -157,8 +154,6 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertGitHubIdentityProviderConfig(identityProvider); } else if (TwitterIdentityProviderFactory.PROVIDER_ID.equals(providerId)) { assertTwitterIdentityProviderConfig(identityProvider); - } else if (KerberosIdentityProviderFactory.PROVIDER_ID.equals(providerId)) { - assertKerberosIdentityProviderConfig(identityProvider); } else { continue; } @@ -276,21 +271,6 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertEquals("clientSecret", config.getClientSecret()); } - private void assertKerberosIdentityProviderConfig(IdentityProviderModel identityProvider) { - KerberosIdentityProvider kerberosIdentityProvider = new KerberosIdentityProviderFactory().create(identityProvider); - KerberosIdentityProviderConfig config = kerberosIdentityProvider.getConfig(); - - assertEquals("model-kerberos", config.getId()); - assertEquals(KerberosIdentityProviderFactory.PROVIDER_ID, config.getProviderId()); - assertEquals("Kerberos", config.getName()); - assertEquals(true, config.isEnabled()); - assertEquals(true, config.isUpdateProfileFirstLogin()); - assertEquals(false, config.isAuthenticateByDefault()); - assertEquals("HTTP/server.domain.org@DOMAIN.ORG", config.getServerPrincipal()); - assertEquals("/etc/http.keytab", config.getKeyTab()); - assertTrue(config.getDebug()); - } - private RealmModel installTestRealm() throws IOException { RealmRepresentation realmRepresentation = loadJson("broker-test/test-realm-with-broker.json"); From 8aa19fe8ebf51fb19851ac462edf313420c91fb8 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 18 Feb 2015 17:38:58 +0100 Subject: [PATCH 7/7] KEYCLOAK-1041 Fix examples packaging --- examples/fuse/camel/pom.xml | 7 +++++++ examples/fuse/cxf-jaxrs/pom.xml | 7 +++++++ examples/fuse/cxf-jaxws/pom.xml | 7 +++++++ examples/fuse/features/pom.xml | 7 +++++++ examples/fuse/product-app-fuse/pom.xml | 7 +++++++ examples/providers/event-listener-sysout/pom.xml | 4 ++-- examples/providers/event-store-mem/pom.xml | 4 ++-- examples/providers/federation-provider/pom.xml | 4 ++-- examples/providers/pom.xml | 6 ++++-- examples/saml/pom.xml | 4 ++-- 10 files changed, 47 insertions(+), 10 deletions(-) diff --git a/examples/fuse/camel/pom.xml b/examples/fuse/camel/pom.xml index 51869c0797..a81554e88c 100644 --- a/examples/fuse/camel/pom.xml +++ b/examples/fuse/camel/pom.xml @@ -50,6 +50,13 @@ install + + org.apache.maven.plugins + maven-deploy-plugin + + true + + org.apache.maven.plugins maven-compiler-plugin diff --git a/examples/fuse/cxf-jaxrs/pom.xml b/examples/fuse/cxf-jaxrs/pom.xml index 5486df2a3e..eea285860c 100644 --- a/examples/fuse/cxf-jaxrs/pom.xml +++ b/examples/fuse/cxf-jaxrs/pom.xml @@ -54,6 +54,13 @@ install + + org.apache.maven.plugins + maven-deploy-plugin + + true + + org.apache.maven.plugins maven-compiler-plugin diff --git a/examples/fuse/cxf-jaxws/pom.xml b/examples/fuse/cxf-jaxws/pom.xml index 6f38b76e20..8cefdf4dfb 100644 --- a/examples/fuse/cxf-jaxws/pom.xml +++ b/examples/fuse/cxf-jaxws/pom.xml @@ -77,6 +77,13 @@ install + + org.apache.maven.plugins + maven-deploy-plugin + + true + + org.apache.maven.plugins maven-compiler-plugin diff --git a/examples/fuse/features/pom.xml b/examples/fuse/features/pom.xml index e6e8bfc594..452f1909f5 100644 --- a/examples/fuse/features/pom.xml +++ b/examples/fuse/features/pom.xml @@ -21,6 +21,13 @@ + + org.apache.maven.plugins + maven-deploy-plugin + + true + + org.apache.maven.plugins maven-resources-plugin diff --git a/examples/fuse/product-app-fuse/pom.xml b/examples/fuse/product-app-fuse/pom.xml index fdd218d6b7..96a0410ce8 100644 --- a/examples/fuse/product-app-fuse/pom.xml +++ b/examples/fuse/product-app-fuse/pom.xml @@ -56,6 +56,13 @@ install + + org.apache.maven.plugins + maven-deploy-plugin + + true + + org.apache.maven.plugins maven-compiler-plugin diff --git a/examples/providers/event-listener-sysout/pom.xml b/examples/providers/event-listener-sysout/pom.xml index b5bbd22f14..c6579da6c0 100755 --- a/examples/providers/event-listener-sysout/pom.xml +++ b/examples/providers/event-listener-sysout/pom.xml @@ -1,10 +1,10 @@ - examples-providers-pom + keycloak-parent org.keycloak 1.2.0.Beta1-SNAPSHOT - ../pom.xml + ../../../pom.xml Event Listener System.out Example diff --git a/examples/providers/event-store-mem/pom.xml b/examples/providers/event-store-mem/pom.xml index c8d4174bc7..7e657b1162 100755 --- a/examples/providers/event-store-mem/pom.xml +++ b/examples/providers/event-store-mem/pom.xml @@ -1,10 +1,10 @@ - examples-providers-pom + keycloak-parent org.keycloak 1.2.0.Beta1-SNAPSHOT - ../pom.xml + ../../../pom.xml Event Store In-Mem Example diff --git a/examples/providers/federation-provider/pom.xml b/examples/providers/federation-provider/pom.xml index ef8ddf36ca..eaa07d29a4 100755 --- a/examples/providers/federation-provider/pom.xml +++ b/examples/providers/federation-provider/pom.xml @@ -1,10 +1,10 @@ - examples-providers-pom + keycloak-parent org.keycloak 1.2.0.Beta1-SNAPSHOT - ../pom.xml + ../../../pom.xml Properties Authentication Provider Example diff --git a/examples/providers/pom.xml b/examples/providers/pom.xml index 2dbe041271..0459b74845 100755 --- a/examples/providers/pom.xml +++ b/examples/providers/pom.xml @@ -1,11 +1,13 @@ + - examples-pom + keycloak-parent org.keycloak 1.2.0.Beta1-SNAPSHOT - ../pom.xml + ../../pom.xml + Provider Examples 4.0.0 diff --git a/examples/saml/pom.xml b/examples/saml/pom.xml index bd10f77f35..6570429123 100755 --- a/examples/saml/pom.xml +++ b/examples/saml/pom.xml @@ -1,10 +1,10 @@ - examples-pom + keycloak-parent org.keycloak 1.2.0.Beta1-SNAPSHOT - ../pom.xml + ../../pom.xml Provider Examples