keycloak-developers

Details

diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index 7b72d6a..b5c21ab 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -15,6 +15,7 @@
                 <!ENTITY Jetty9Adapter SYSTEM "modules/jetty9-adapter.xml">
                 <!ENTITY Jetty8Adapter SYSTEM "modules/jetty8-adapter.xml">
                 <!ENTITY FuseAdapter SYSTEM "modules/fuse-adapter.xml">
+                <!ENTITY SpringBootAdapter SYSTEM "modules/spring-boot-adapter.xml">
                 <!ENTITY InstalledApplications SYSTEM "modules/installed-applications.xml">
                 <!ENTITY Logout SYSTEM "modules/logout.xml">
                 <!ENTITY SAML SYSTEM "modules/saml.xml">
@@ -95,6 +96,7 @@ This one is short
         &Jetty8Adapter;
         &FuseAdapter;
         &JavascriptAdapter;
+        &SpringBootAdapter;
         &InstalledApplications;
         &Logout;
         &MultiTenancy;
diff --git a/docbook/reference/en/en-US/modules/spring-boot-adapter.xml b/docbook/reference/en/en-US/modules/spring-boot-adapter.xml
new file mode 100755
index 0000000..6728e03
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/spring-boot-adapter.xml
@@ -0,0 +1,72 @@
+<section id="spring-boot-adapter">
+    <title>Spring Boot Adapter</title>
+    <para>
+        To be able to secure Spring Boot apps you must add the Keycloak Spring Boot adapter
+        JAR to your app. You then have to provide some extra configuration via normal Spring
+        Boot configuration (<literal>application.properties</literal>).  Let's go over these steps.
+    </para>
+    <section id="spring-boot-adapter-installation">
+        <title>Adapter Installation</title>
+        <para>
+            The Keycloak Spring Boot adapter takes advantage of Spring Boot's autoconfiguration so all
+            you need to do is add the Keycloak Spring Boot adapter JAR to your project. Depending on
+            what container you are using with Spring Boot, you also need to add the appropriate
+            Keycloak container adapter. If you are using Maven, add the following to your pom.xml (using
+            Tomcat as an example):
+        </para>
+        <para>
+            <programlisting>
+<![CDATA[
+<dependency>
+    <groupId>org.keycloak</groupId>
+    <artifactId>keycloak-spring-boot-adapter</artifactId>
+    <version>1.2.0.Beta1-SNAPSHOT</version>
+</dependency>
+<dependency>
+    <groupId>org.keycloak</groupId>
+    <artifactId>keycloak-tomcat8-adapter</artifactId>
+    <version>${keycloak.version}</version>
+</dependency>
+]]>
+            </programlisting>
+        </para>
+    </section>
+
+    <section id="spring-boot-adapter-configuration">
+        <title>Required Spring Boot Adapter Configuration</title>
+        <para>
+            This section describes how to configure your Spring Boot app to use Keycloak.
+        </para>
+        <para>
+            Instead of a <literal>keycloak.json</literal> file, you configure the realm for the Spring
+            Boot Keycloak adapter via the normal Spring Boot configuration. For example:
+        </para>
+        <programlisting>
+<![CDATA[
+keycloak.realm = demorealm
+keycloak.realmKey = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLCWYuxXmsmfV+Xc9Ik8QET8lD4wuHrJAXbbutS2O/eMjQQLNK7QDX/k/XhOkhxP0YBEypqeXeGaeQJjCxDhFjJXQuewUEMlmSja3IpoJ9/hFn4Cns4m7NGO+rtvnfnwgVfsEOS5EmZhRddp+40KBPPJfTH6Vgu6KjQwuFPj6DTwIDAQAB
+keycloak.auth-server-url = http://127.0.0.1:8080/auth
+keycloak.ssl-required = external
+keycloak.resource = demoapp
+keycloak.credentials.secret = 11111111-1111-1111-1111-111111111111
+keycloak.use-resource-role-mappings = true
+]]>
+        </programlisting>
+        <para>
+            You also need to specify the J2EE security config that would normally go in the <literal>web.xml</literal>.
+            Here's an example configuration:
+        </para>
+        <programlisting>
+<![CDATA[
+keycloak.securityConstraints[0].securityCollections[0].name = insecure stuff
+keycloak.securityConstraints[0].securityCollections[0].authRoles[0] = admin
+keycloak.securityConstraints[0].securityCollections[0].authRoles[0] = user
+keycloak.securityConstraints[0].securityCollections[0].patterns[0] = /insecure
+
+keycloak.securityConstraints[0].securityCollections[1].name = admin stuff
+keycloak.securityConstraints[0].securityCollections[1].authRoles[0] = admin
+keycloak.securityConstraints[0].securityCollections[1].patterns[0] = /admin
+]]>
+        </programlisting>
+    </section>
+</section>
\ No newline at end of file
diff --git a/integration/pom.xml b/integration/pom.xml
index c14ec59..2ccda8e 100755
--- a/integration/pom.xml
+++ b/integration/pom.xml
@@ -29,5 +29,6 @@
         <module>installed</module>
         <module>admin-client</module>
         <module>osgi-adapter</module>
+        <module>spring-boot</module>
     </modules>
 </project>
diff --git a/integration/spring-boot/pom.xml b/integration/spring-boot/pom.xml
new file mode 100755
index 0000000..22552cb
--- /dev/null
+++ b/integration/spring-boot/pom.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <parent>
+    <artifactId>keycloak-parent</artifactId>
+    <groupId>org.keycloak</groupId>
+    <version>1.2.0.Beta1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>keycloak-spring-boot-adapter</artifactId>
+  <name>Keycloak Spring Boot Integration</name>
+  <description/>
+
+  <properties>
+    <spring-boot.version>1.2.1.RELEASE</spring-boot.version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.jboss.logging</groupId>
+      <artifactId>jboss-logging</artifactId>
+      <version>${jboss.logging.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.keycloak</groupId>
+      <artifactId>keycloak-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.keycloak</groupId>
+      <artifactId>keycloak-tomcat8-adapter</artifactId>
+      <version>${project.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.keycloak</groupId>
+      <artifactId>keycloak-undertow-adapter</artifactId>
+      <version>${project.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.keycloak</groupId>
+      <artifactId>keycloak-jetty92-adapter</artifactId>
+      <version>${project.version}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-web</artifactId>
+      <version>${spring-boot.version}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.6</source>
+          <target>1.6</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java b/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java
new file mode 100644
index 0000000..c5834c2
--- /dev/null
+++ b/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java
@@ -0,0 +1,28 @@
+package org.keycloak.adapters.springboot;
+
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+
+public class KeycloakSpringBootConfigResolver implements org.keycloak.adapters.KeycloakConfigResolver {
+
+    private KeycloakDeployment keycloakDeployment;
+
+    private static AdapterConfig adapterConfig;
+
+    @Override
+    public KeycloakDeployment resolve(HttpFacade.Request request) {
+        if (keycloakDeployment != null) {
+            return keycloakDeployment;
+        }
+
+        keycloakDeployment = KeycloakDeploymentBuilder.build(KeycloakSpringBootConfigResolver.adapterConfig);
+
+        return keycloakDeployment;
+    }
+
+    static void setAdapterConfig(AdapterConfig adapterConfig) {
+        KeycloakSpringBootConfigResolver.adapterConfig = adapterConfig;
+    }
+}
\ No newline at end of file
diff --git a/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfiguration.java b/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfiguration.java
new file mode 100755
index 0000000..f48929d
--- /dev/null
+++ b/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfiguration.java
@@ -0,0 +1,123 @@
+package org.keycloak.adapters.springboot;
+
+import org.apache.catalina.Context;
+import org.apache.tomcat.util.descriptor.web.LoginConfig;
+import org.apache.tomcat.util.descriptor.web.SecurityCollection;
+import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
+import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
+import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
+import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
+import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
+import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
+import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Keycloak authentication integration for Spring Boot
+ *
+ * @author <a href="mailto:jimmidyson@gmail.com">Jimmi Dyson</a>
+ * @version $Revision: 1 $
+ */
+@Configuration
+@ConditionalOnWebApplication
+@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
+public class KeycloakSpringBootConfiguration {
+
+    private KeycloakSpringBootProperties keycloakProperties;
+
+    @Autowired
+    public void setKeycloakSpringBootProperties(KeycloakSpringBootProperties keycloakProperties) {
+        this.keycloakProperties = keycloakProperties;
+        KeycloakSpringBootConfigResolver.setAdapterConfig(keycloakProperties);
+    }
+
+    @Bean
+    public EmbeddedServletContainerCustomizer getKeycloakContainerCustomizer() {
+        return new EmbeddedServletContainerCustomizer() {
+            @Override
+            public void customize(ConfigurableEmbeddedServletContainer configurableEmbeddedServletContainer) {
+                if (configurableEmbeddedServletContainer instanceof TomcatEmbeddedServletContainerFactory) {
+                    TomcatEmbeddedServletContainerFactory container = (TomcatEmbeddedServletContainerFactory) configurableEmbeddedServletContainer;
+
+                    container.addContextValves(new KeycloakAuthenticatorValve());
+
+                    container.addContextCustomizers(getTomcatKeycloakContextCustomizer());
+                } else if (configurableEmbeddedServletContainer instanceof UndertowEmbeddedServletContainerFactory) {
+                    throw new IllegalArgumentException("Undertow Keycloak integration is not yet implemented");
+                } else if (configurableEmbeddedServletContainer instanceof JettyEmbeddedServletContainerFactory) {
+                    throw new IllegalArgumentException("Jetty Keycloak integration is not yet implemented");
+                }
+            }
+        };
+    }
+
+    @Bean
+    public TomcatContextCustomizer getTomcatKeycloakContextCustomizer() {
+        return new TomcatContextCustomizer() {
+            @Override
+            public void customize(Context context) {
+                LoginConfig loginConfig = new LoginConfig();
+                loginConfig.setAuthMethod("KEYCLOAK");
+                context.setLoginConfig(loginConfig);
+
+                Set<String> authRoles = new HashSet<String>();
+                for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
+                    for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
+                        for (String authRole : collection.getAuthRoles()) {
+                            if (!authRoles.contains(authRole)) {
+                                context.addSecurityRole(authRole);
+                                authRoles.add(authRole);
+                            }
+                        }
+                    }
+                }
+
+                for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
+                    SecurityConstraint tomcatConstraint = new SecurityConstraint();
+
+                    for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
+                        SecurityCollection tomcatSecCollection = new SecurityCollection();
+
+                        if (collection.getName() != null) {
+                            tomcatSecCollection.setName(collection.getName());
+                        }
+                        if (collection.getDescription() != null) {
+                            tomcatSecCollection.setDescription(collection.getDescription());
+                        }
+
+                        for (String authRole : collection.getAuthRoles()) {
+                            tomcatConstraint.addAuthRole(authRole);
+                        }
+
+                        for (String pattern : collection.getPatterns()) {
+                            tomcatSecCollection.addPattern(pattern);
+                        }
+
+                        for (String method : collection.getMethods()) {
+                            tomcatSecCollection.addMethod(method);
+                        }
+
+                        for (String method : collection.getOmittedMethods()) {
+                            tomcatSecCollection.addOmittedMethod(method);
+                        }
+
+                        tomcatConstraint.addCollection(tomcatSecCollection);
+                    }
+
+                    context.addConstraint(tomcatConstraint);
+                }
+
+                context.addParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
+            }
+        };
+    }
+
+}
diff --git a/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java b/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java
new file mode 100644
index 0000000..18ec791
--- /dev/null
+++ b/integration/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java
@@ -0,0 +1,90 @@
+package org.keycloak.adapters.springboot;
+
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@ConfigurationProperties(prefix = "keycloak", ignoreUnknownFields = false)
+public class KeycloakSpringBootProperties extends AdapterConfig {
+
+    private List<SecurityConstraint> securityConstraints = new ArrayList<SecurityConstraint>();
+
+    public static class SecurityConstraint {
+        private List<SecurityCollection> securityCollections = new ArrayList<SecurityCollection>();
+
+        public List<SecurityCollection> getSecurityCollections() {
+            return securityCollections;
+        }
+
+        public void setSecurityCollections(List<SecurityCollection> securityCollections) {
+            this.securityCollections = securityCollections;
+        }
+    }
+
+    public static class SecurityCollection {
+        private String name;
+        private String description;
+        private List<String> authRoles = new ArrayList<String>();
+        private List<String> patterns = new ArrayList<String>();
+        private List<String> methods = new ArrayList<String>();
+        private List<String> omittedMethods = new ArrayList<String>();
+
+        public List<String> getAuthRoles() {
+            return authRoles;
+        }
+
+        public List<String> getPatterns() {
+            return patterns;
+        }
+
+        public List<String> getMethods() {
+            return methods;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public List<String> getOmittedMethods() {
+            return omittedMethods;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
+        public void setAuthRoles(List<String> authRoles) {
+            this.authRoles = authRoles;
+        }
+
+        public void setPatterns(List<String> patterns) {
+            this.patterns = patterns;
+        }
+
+        public void setMethods(List<String> methods) {
+            this.methods = methods;
+        }
+
+        public void setOmittedMethods(List<String> omittedMethods) {
+            this.omittedMethods = omittedMethods;
+        }
+    }
+
+    public List<SecurityConstraint> getSecurityConstraints() {
+        return securityConstraints;
+    }
+
+    public void setSecurityConstraints(List<SecurityConstraint> securityConstraints) {
+        this.securityConstraints = securityConstraints;
+    }
+}
diff --git a/integration/spring-boot/src/main/resources/META-INF/spring.factories b/integration/spring-boot/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..3193ee6
--- /dev/null
+++ b/integration/spring-boot/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.keycloak.adapters.springboot.KeycloakSpringBootConfiguration
\ No newline at end of file