From d57fb445eb873696c45afd9acff58adab097fc5f Mon Sep 17 00:00:00 2001 From: Ryan Dawson Date: Fri, 30 Jun 2017 14:31:30 +0100 Subject: [PATCH] adapter for spring boot 2 remove built directory update snapshot version references refactor out core library to remove duplication adapter for spring boot 2 remove built directory update snapshot version references Revert "merge from upstream" This reverts commit 88c39a2f23b8f2d4b25360e2b46e683d11b4972b, reversing changes made to f0811145ceeb8ec609ed66b06067f797e288aa89. setting correct versions updating to latest keycloak arquillian test app for spring boot2 update to 2.0.0.RELEASE added Rest Customizer --- adapters/oidc/pom.xml | 2 + .../oidc/spring-boot-adapter-core/pom.xml | 116 ++++++++ .../KeycloakBaseSpringBootConfiguration.java | 262 ++++++++++++++++++ .../KeycloakSpringBootConfigResolver.java | 0 .../KeycloakSpringBootProperties.java | 0 .../main/resources/META-INF/spring.factories | 2 + adapters/oidc/spring-boot/pom.xml | 6 + .../springboot/KeycloakAutoConfiguration.java | 206 +------------- adapters/oidc/spring-boot2/pom.xml | 151 ++++++++++ .../springboot/KeycloakAutoConfiguration.java | 116 ++++++++ .../KeycloakRestTemplateCustomizer.java | 24 ++ ...curityContextClientRequestInterceptor.java | 55 ++++ .../main/resources/META-INF/spring.factories | 2 + .../KeycloakRestTemplateCustomizerTest.java | 28 ++ ...tyContextClientRequestInterceptorTest.java | 87 ++++++ boms/adapter/pom.xml | 15 + .../keycloak-spring-boot-2-starter/pom.xml | 40 +++ misc/spring-boot-2-starter/pom.xml | 30 ++ .../keycloak-spring-boot-starter/pom.xml | 2 +- pom.xml | 10 + .../test-apps/spring-boot-2-adapter/mvnw | 225 +++++++++++++++ .../test-apps/spring-boot-2-adapter/mvnw.cmd | 143 ++++++++++ .../test-apps/spring-boot-2-adapter/pom.xml | 224 +++++++++++++++ .../java/org/keycloak/AdminController.java | 142 ++++++++++ .../SpringBootAdapterApplication.java | 12 + .../src/main/resources/application.properties | 12 + .../main/resources/static/admin/index.html | 12 + .../src/main/resources/static/index.html | 12 + .../src/main/resources/templates/linking.html | 9 + .../src/main/resources/templates/session.html | 9 + .../src/main/resources/templates/tokens.html | 11 + .../SpringBootAdapterApplicationTests.java | 16 ++ .../test-apps/spring-boot-adapter/pom.xml | 2 +- .../tests/other/springboot-tests/pom.xml | 49 ++++ 34 files changed, 1831 insertions(+), 201 deletions(-) create mode 100755 adapters/oidc/spring-boot-adapter-core/pom.xml create mode 100755 adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java rename adapters/oidc/{spring-boot => spring-boot-adapter-core}/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java (100%) rename adapters/oidc/{spring-boot => spring-boot-adapter-core}/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java (100%) create mode 100644 adapters/oidc/spring-boot-adapter-core/src/main/resources/META-INF/spring.factories create mode 100755 adapters/oidc/spring-boot2/pom.xml create mode 100755 adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java create mode 100644 adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizer.java create mode 100644 adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptor.java create mode 100644 adapters/oidc/spring-boot2/src/main/resources/META-INF/spring.factories create mode 100644 adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizerTest.java create mode 100644 adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptorTest.java create mode 100644 misc/spring-boot-2-starter/keycloak-spring-boot-2-starter/pom.xml create mode 100644 misc/spring-boot-2-starter/pom.xml create mode 100755 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw.cmd create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/pom.xml create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/AdminController.java create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/application.properties create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/admin/index.html create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/index.html create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/linking.html create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/session.html create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/tokens.html create mode 100644 testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml index 42522537dbb..e559c5a33bb 100755 --- a/adapters/oidc/pom.xml +++ b/adapters/oidc/pom.xml @@ -42,6 +42,8 @@ servlet-filter servlet-oauth-client spring-boot + spring-boot2 + spring-boot-adapter-core spring-boot-container-bundle spring-security tomcat diff --git a/adapters/oidc/spring-boot-adapter-core/pom.xml b/adapters/oidc/spring-boot-adapter-core/pom.xml new file mode 100755 index 00000000000..7c4c5ab9d54 --- /dev/null +++ b/adapters/oidc/spring-boot-adapter-core/pom.xml @@ -0,0 +1,116 @@ + + + + + + keycloak-parent + org.keycloak + 4.0.0.CR1-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + keycloak-spring-boot-adapter-core + Keycloak Spring Boot Adapter Core + + + + 1.3.0.RELEASE + + + + + org.jboss.logging + jboss-logging + + + org.keycloak + keycloak-core + + + org.keycloak + spring-boot-container-bundle + ${project.version} + true + compile + + + org.keycloak + keycloak-spring-security-adapter + ${project.version} + compile + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + compile + true + + + io.undertow + undertow-servlet + compile + true + + + org.eclipse.jetty + jetty-server + ${jetty9.version} + compile + true + + + + org.eclipse.jetty + jetty-security + ${jetty9.version} + compile + true + + + + org.eclipse.jetty + jetty-webapp + ${jetty9.version} + compile + true + + + junit + junit + test + + + org.springframework.boot + spring-boot-configuration-processor + true + ${spring-boot.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java new file mode 100755 index 00000000000..8afd32b4675 --- /dev/null +++ b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java @@ -0,0 +1,262 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.springboot; + +import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.WebResourceCollection; +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.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.webapp.WebAppContext; +import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator; +import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve; +import org.keycloak.adapters.undertow.KeycloakServletExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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.jetty.JettyServerCustomizer; +import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.undertow.UndertowDeploymentInfoCustomizer; +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.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Keycloak authentication base integration for Spring Boot - base to be extended for particular boot versions. + */ +public class KeycloakBaseSpringBootConfiguration { + + protected KeycloakSpringBootProperties keycloakProperties; + + @Autowired + public void setKeycloakSpringBootProperties(KeycloakSpringBootProperties keycloakProperties) { + this.keycloakProperties = keycloakProperties; + KeycloakSpringBootConfigResolver.setAdapterConfig(keycloakProperties); + } + + + static class KeycloakBaseUndertowDeploymentInfoCustomizer { + + protected final KeycloakSpringBootProperties keycloakProperties; + + public KeycloakBaseUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) { + this.keycloakProperties = keycloakProperties; + } + + public void customize(DeploymentInfo deploymentInfo) { + + io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm()); + loginConfig.addFirstAuthMethod("KEYCLOAK"); + + deploymentInfo.setLoginConfig(loginConfig); + + deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName()); + deploymentInfo.addSecurityConstraints(getSecurityConstraints()); + + deploymentInfo.addServletExtension(new KeycloakServletExtension()); + } + + private List getSecurityConstraints() { + + List undertowSecurityConstraints = new ArrayList(); + for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) { + + io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint(); + undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles()); + + for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) { + + WebResourceCollection webResourceCollection = new WebResourceCollection(); + webResourceCollection.addHttpMethods(collectionDefinition.getMethods()); + webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods()); + webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns()); + + undertowSecurityConstraint.addWebResourceCollections(webResourceCollection); + + } + + undertowSecurityConstraints.add(undertowSecurityConstraint); + } + return undertowSecurityConstraints; + } + } + + static class KeycloakBaseJettyServerCustomizer { + + protected final KeycloakSpringBootProperties keycloakProperties; + + public KeycloakBaseJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) { + this.keycloakProperties = keycloakProperties; + } + + public void customize(Server server) { + + KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator(); + keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver()); + + /* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example + on how to map servlet spec to Constraints */ + + List jettyConstraintMappings = new ArrayList(); + for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) { + + for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition + .getSecurityCollections()) { + // securityCollection matches servlet spec's web-resource-collection + Constraint jettyConstraint = new Constraint(); + + if (constraintDefinition.getAuthRoles().size() > 0) { + jettyConstraint.setAuthenticate(true); + jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0])); + } + + jettyConstraint.setName(securityCollectionDefinition.getName()); + + // according to the servlet spec each security-constraint has at least one URL pattern + for(String pattern : securityCollectionDefinition.getPatterns()) { + + /* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method, + but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed + mapping but only one mapping in the cases of omitted methods or no methods. + */ + + if (securityCollectionDefinition.getMethods().size() > 0) { + // according to the servlet spec we have either methods ... + for(String method : securityCollectionDefinition.getMethods()) { + ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); + jettyConstraintMappings.add(jettyConstraintMapping); + + jettyConstraintMapping.setConstraint(jettyConstraint); + jettyConstraintMapping.setPathSpec(pattern); + jettyConstraintMapping.setMethod(method); + } + } else if (securityCollectionDefinition.getOmittedMethods().size() > 0){ + // ... omitted methods ... + ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); + jettyConstraintMappings.add(jettyConstraintMapping); + + jettyConstraintMapping.setConstraint(jettyConstraint); + jettyConstraintMapping.setPathSpec(pattern); + jettyConstraintMapping.setMethodOmissions( + securityCollectionDefinition.getOmittedMethods().toArray(new String[0])); + } else { + // ... or no methods at all + ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); + jettyConstraintMappings.add(jettyConstraintMapping); + + jettyConstraintMapping.setConstraint(jettyConstraint); + jettyConstraintMapping.setPathSpec(pattern); + } + + } + + } + } + + WebAppContext webAppContext = server.getBean(WebAppContext.class); + //if not found as registered bean let's try the handler + if(webAppContext==null){ + webAppContext = (WebAppContext) server.getHandler(); + } + + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + securityHandler.setConstraintMappings(jettyConstraintMappings); + securityHandler.setAuthenticator(keycloakJettyAuthenticator); + + webAppContext.setSecurityHandler(securityHandler); + } + } + + static class KeycloakBaseTomcatContextCustomizer { + + protected final KeycloakSpringBootProperties keycloakProperties; + + public KeycloakBaseTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) { + this.keycloakProperties = keycloakProperties; + } + + public void customize(Context context) { + LoginConfig loginConfig = new LoginConfig(); + loginConfig.setAuthMethod("KEYCLOAK"); + context.setLoginConfig(loginConfig); + + Set authRoles = new HashSet(); + for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) { + for (String authRole : constraint.getAuthRoles()) { + if (!authRoles.contains(authRole)) { + context.addSecurityRole(authRole); + authRoles.add(authRole); + } + } + } + + for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) { + SecurityConstraint tomcatConstraint = new SecurityConstraint(); + + for (String authRole : constraint.getAuthRoles()) { + tomcatConstraint.addAuthRole(authRole); + } + + 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 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/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java similarity index 100% rename from adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java rename to adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootConfigResolver.java diff --git a/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java b/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java similarity index 100% rename from adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java rename to adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakSpringBootProperties.java diff --git a/adapters/oidc/spring-boot-adapter-core/src/main/resources/META-INF/spring.factories b/adapters/oidc/spring-boot-adapter-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..0c80e3bd8b2 --- /dev/null +++ b/adapters/oidc/spring-boot-adapter-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.keycloak.adapters.springboot.KeycloakAutoConfiguration \ No newline at end of file diff --git a/adapters/oidc/spring-boot/pom.xml b/adapters/oidc/spring-boot/pom.xml index 3e7eb2beb16..75fbbfdc6be 100755 --- a/adapters/oidc/spring-boot/pom.xml +++ b/adapters/oidc/spring-boot/pom.xml @@ -37,6 +37,12 @@ + + + org.keycloak + keycloak-spring-boot-adapter-core + + org.jboss.logging jboss-logging diff --git a/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java b/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java index e18677a3c23..90837f5bcb9 100755 --- a/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java +++ b/adapters/oidc/spring-boot/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java @@ -17,20 +17,7 @@ package org.keycloak.adapters.springboot; -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.WebResourceCollection; -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.eclipse.jetty.security.ConstraintMapping; -import org.eclipse.jetty.security.ConstraintSecurityHandler; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.util.security.Constraint; -import org.eclipse.jetty.webapp.WebAppContext; -import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator; import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve; -import org.keycloak.adapters.undertow.KeycloakServletExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -47,10 +34,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; /** * Keycloak authentication integration for Spring Boot @@ -62,7 +45,7 @@ import java.util.Set; @ConditionalOnWebApplication @EnableConfigurationProperties(KeycloakSpringBootProperties.class) @ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true) -public class KeycloakAutoConfiguration { +public class KeycloakAutoConfiguration extends KeycloakBaseSpringBootConfiguration { private KeycloakSpringBootProperties keycloakProperties; @@ -117,202 +100,27 @@ public class KeycloakAutoConfiguration { return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties); } - static class KeycloakUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer { - - private final KeycloakSpringBootProperties keycloakProperties; + static class KeycloakUndertowDeploymentInfoCustomizer extends KeycloakBaseUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer { public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) { - this.keycloakProperties = keycloakProperties; + super(keycloakProperties); } - @Override - public void customize(DeploymentInfo deploymentInfo) { - - io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm()); - loginConfig.addFirstAuthMethod("KEYCLOAK"); - - deploymentInfo.setLoginConfig(loginConfig); - deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName()); - deploymentInfo.addSecurityConstraints(getSecurityConstraints()); - deploymentInfo.addServletExtension(new KeycloakServletExtension()); - } - - private List getSecurityConstraints() { - - List undertowSecurityConstraints = new ArrayList(); - for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) { - - io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint(); - undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles()); - - for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) { - - WebResourceCollection webResourceCollection = new WebResourceCollection(); - webResourceCollection.addHttpMethods(collectionDefinition.getMethods()); - webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods()); - webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns()); - - undertowSecurityConstraint.addWebResourceCollections(webResourceCollection); - - } - undertowSecurityConstraints.add(undertowSecurityConstraint); - } - return undertowSecurityConstraints; - } } - static class KeycloakJettyServerCustomizer implements JettyServerCustomizer { - - private final KeycloakSpringBootProperties keycloakProperties; + static class KeycloakJettyServerCustomizer extends KeycloakBaseJettyServerCustomizer implements JettyServerCustomizer { public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) { - this.keycloakProperties = keycloakProperties; + super(keycloakProperties); } - @Override - public void customize(Server server) { - - KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator(); - keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver()); - - /* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example - on how to map servlet spec to Constraints */ - - List jettyConstraintMappings = new ArrayList(); - for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) { - - for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition - .getSecurityCollections()) { - // securityCollection matches servlet spec's web-resource-collection - Constraint jettyConstraint = new Constraint(); - - if (constraintDefinition.getAuthRoles().size() > 0) { - jettyConstraint.setAuthenticate(true); - jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0])); - } - - jettyConstraint.setName(securityCollectionDefinition.getName()); - - // according to the servlet spec each security-constraint has at least one URL pattern - for(String pattern : securityCollectionDefinition.getPatterns()) { - - /* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method, - but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed - mapping but only one mapping in the cases of omitted methods or no methods. - */ - - if (securityCollectionDefinition.getMethods().size() > 0) { - // according to the servlet spec we have either methods ... - for(String method : securityCollectionDefinition.getMethods()) { - ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); - jettyConstraintMappings.add(jettyConstraintMapping); - - jettyConstraintMapping.setConstraint(jettyConstraint); - jettyConstraintMapping.setPathSpec(pattern); - jettyConstraintMapping.setMethod(method); - } - } else if (securityCollectionDefinition.getOmittedMethods().size() > 0){ - // ... omitted methods ... - ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); - jettyConstraintMappings.add(jettyConstraintMapping); - - jettyConstraintMapping.setConstraint(jettyConstraint); - jettyConstraintMapping.setPathSpec(pattern); - jettyConstraintMapping.setMethodOmissions( - securityCollectionDefinition.getOmittedMethods().toArray(new String[0])); - } else { - // ... or no methods at all - ConstraintMapping jettyConstraintMapping = new ConstraintMapping(); - jettyConstraintMappings.add(jettyConstraintMapping); - - jettyConstraintMapping.setConstraint(jettyConstraint); - jettyConstraintMapping.setPathSpec(pattern); - } - - } - - } - } - - WebAppContext webAppContext = server.getBean(WebAppContext.class); - //if not found as registered bean let's try the handler - if(webAppContext==null){ - webAppContext = (WebAppContext) server.getHandler(); - } - - ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); - securityHandler.setConstraintMappings(jettyConstraintMappings); - securityHandler.setAuthenticator(keycloakJettyAuthenticator); - - webAppContext.setSecurityHandler(securityHandler); - } } - static class KeycloakTomcatContextCustomizer implements TomcatContextCustomizer { - - private final KeycloakSpringBootProperties keycloakProperties; + static class KeycloakTomcatContextCustomizer extends KeycloakBaseTomcatContextCustomizer implements TomcatContextCustomizer { public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) { - this.keycloakProperties = keycloakProperties; + super(keycloakProperties); } - @Override - public void customize(Context context) { - LoginConfig loginConfig = new LoginConfig(); - loginConfig.setAuthMethod("KEYCLOAK"); - context.setLoginConfig(loginConfig); - - Set authRoles = new HashSet(); - for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) { - for (String authRole : constraint.getAuthRoles()) { - if (!authRoles.contains(authRole)) { - context.addSecurityRole(authRole); - authRoles.add(authRole); - } - } - } - - for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) { - SecurityConstraint tomcatConstraint = new SecurityConstraint(); - - for (String authRole : constraint.getAuthRoles()) { - tomcatConstraint.addAuthRole(authRole); - if(authRole.equals("*") || authRole.equals("**")) { - // For some reasons embed tomcat don't set the auth constraint on true when wildcard is - // used - tomcatConstraint.setAuthConstraint(true); - } - } - - 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 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/adapters/oidc/spring-boot2/pom.xml b/adapters/oidc/spring-boot2/pom.xml new file mode 100755 index 00000000000..466b6b3532e --- /dev/null +++ b/adapters/oidc/spring-boot2/pom.xml @@ -0,0 +1,151 @@ + + + + + + keycloak-parent + org.keycloak + 4.0.0.CR1-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + keycloak-spring-boot-2-adapter + Keycloak Spring Boot 2 Integration + + + + 2.0.0.RELEASE + 5.0.2.RELEASE + 1.9.5 + + + + + + org.keycloak + keycloak-spring-boot-adapter-core + + + + org.jboss.logging + jboss-logging + + + org.keycloak + keycloak-core + + + org.keycloak + spring-boot-container-bundle + ${project.version} + true + compile + + + org.keycloak + keycloak-spring-security-adapter + ${project.version} + compile + + + com.fasterxml.jackson.core + jackson-databind + 2.9.4 + provided + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.4 + provided + + + org.springframework + spring-core + 5.0.2.RELEASE + provided + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + io.undertow + undertow-servlet + compile + true + + + org.eclipse.jetty + jetty-server + ${jetty9.version} + compile + true + + + + org.eclipse.jetty + jetty-security + ${jetty9.version} + compile + true + + + + org.eclipse.jetty + jetty-webapp + ${jetty9.version} + compile + true + + + junit + junit + test + + + org.springframework + spring-test + ${spring.version} + test + + + org.mockito + mockito-all + ${mockito.version} + test + + + org.springframework.boot + spring-boot-configuration-processor + true + ${spring-boot.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java new file mode 100755 index 00000000000..6b16541a28b --- /dev/null +++ b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/KeycloakAutoConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.springboot; + +import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; + + + + +/** + * Keycloak authentication integration for Spring Boot 2 + * + */ +@Configuration +@ConditionalOnWebApplication +@EnableConfigurationProperties(KeycloakSpringBootProperties.class) +@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true) +public class KeycloakAutoConfiguration extends KeycloakBaseSpringBootConfiguration { + + + @Bean + public WebServerFactoryCustomizer getKeycloakContainerCustomizer() { + return new WebServerFactoryCustomizer() { + @Override + public void customize(ConfigurableServletWebServerFactory configurableServletWebServerFactory) { + if(configurableServletWebServerFactory instanceof TomcatServletWebServerFactory){ + + TomcatServletWebServerFactory container = (TomcatServletWebServerFactory)configurableServletWebServerFactory; + container.addContextValves(new KeycloakAuthenticatorValve()); + container.addContextCustomizers(tomcatKeycloakContextCustomizer()); + + } else if (configurableServletWebServerFactory instanceof UndertowServletWebServerFactory){ + + UndertowServletWebServerFactory container = (UndertowServletWebServerFactory)configurableServletWebServerFactory; + container.addDeploymentInfoCustomizers(undertowKeycloakContextCustomizer()); + + } else if (configurableServletWebServerFactory instanceof JettyServletWebServerFactory){ + + JettyServletWebServerFactory container = (JettyServletWebServerFactory)configurableServletWebServerFactory; + container.addServerCustomizers(jettyKeycloakServerCustomizer()); + } + } + + }; + } + + @Bean + @ConditionalOnClass(name = {"org.eclipse.jetty.webapp.WebAppContext"}) + public JettyServerCustomizer jettyKeycloakServerCustomizer() { + return new KeycloakJettyServerCustomizer(keycloakProperties); + } + + @Bean + @ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"}) + public TomcatContextCustomizer tomcatKeycloakContextCustomizer() { + return new KeycloakTomcatContextCustomizer(keycloakProperties); + } + + @Bean + @ConditionalOnClass(name = {"io.undertow.Undertow"}) + public UndertowDeploymentInfoCustomizer undertowKeycloakContextCustomizer() { + return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties); + } + + static class KeycloakJettyServerCustomizer extends KeycloakBaseJettyServerCustomizer implements JettyServerCustomizer { + + + public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) { + super(keycloakProperties); + } + + } + + static class KeycloakTomcatContextCustomizer extends KeycloakBaseTomcatContextCustomizer implements TomcatContextCustomizer { + + public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) { + super(keycloakProperties); + } + } + + static class KeycloakUndertowDeploymentInfoCustomizer extends KeycloakBaseUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer { + + public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties){ + super(keycloakProperties); + } + } +} diff --git a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizer.java b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizer.java new file mode 100644 index 00000000000..ae4836c7139 --- /dev/null +++ b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizer.java @@ -0,0 +1,24 @@ +package org.keycloak.adapters.springboot.client; + +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.web.client.RestTemplate; + +public class KeycloakRestTemplateCustomizer implements RestTemplateCustomizer { + + private final KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor; + + public KeycloakRestTemplateCustomizer() { + this(new KeycloakSecurityContextClientRequestInterceptor()); + } + + protected KeycloakRestTemplateCustomizer( + KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor + ) { + this.keycloakInterceptor = keycloakInterceptor; + } + + @Override + public void customize(RestTemplate restTemplate) { + restTemplate.getInterceptors().add(keycloakInterceptor); + } +} diff --git a/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptor.java b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptor.java new file mode 100644 index 00000000000..200a9035f1e --- /dev/null +++ b/adapters/oidc/spring-boot2/src/main/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptor.java @@ -0,0 +1,55 @@ +package org.keycloak.adapters.springboot.client; + +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.io.IOException; +import java.security.Principal; + +/** + * Interceptor for {@link ClientHttpRequestExecution} objects created for server to server secured + * communication using OAuth2 bearer tokens issued by Keycloak. + * + * @author James McShane + * @version $Revision: 1 $ + */ +public class KeycloakSecurityContextClientRequestInterceptor implements ClientHttpRequestInterceptor { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + + /** + * Returns the {@link KeycloakSecurityContext} from the Spring {@link ServletRequestAttributes}'s {@link Principal}. + * + * The principal must support retrieval of the KeycloakSecurityContext, so at this point, only {@link KeycloakPrincipal} + * values are supported + * + * @return the current KeycloakSecurityContext + */ + protected KeycloakSecurityContext getKeycloakSecurityContext() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + Principal principal = attributes.getRequest().getUserPrincipal(); + if (principal == null) { + throw new IllegalStateException("Cannot set authorization header because there is no authenticated principal"); + } + if (!(principal instanceof KeycloakPrincipal)) { + throw new IllegalStateException( + String.format( + "Cannot set authorization header because the principal type %s does not provide the KeycloakSecurityContext", + principal.getClass())); + } + return ((KeycloakPrincipal) principal).getKeycloakSecurityContext(); + } + + @Override + public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { + KeycloakSecurityContext context = this.getKeycloakSecurityContext(); + httpRequest.getHeaders().set(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString()); + return clientHttpRequestExecution.execute(httpRequest, bytes); + } +} diff --git a/adapters/oidc/spring-boot2/src/main/resources/META-INF/spring.factories b/adapters/oidc/spring-boot2/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..0c80e3bd8b2 --- /dev/null +++ b/adapters/oidc/spring-boot2/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.keycloak.adapters.springboot.KeycloakAutoConfiguration \ No newline at end of file diff --git a/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizerTest.java b/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizerTest.java new file mode 100644 index 00000000000..e8e599e40d3 --- /dev/null +++ b/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakRestTemplateCustomizerTest.java @@ -0,0 +1,28 @@ +package org.keycloak.adapters.springboot.client; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class KeycloakRestTemplateCustomizerTest { + + private KeycloakRestTemplateCustomizer customizer; + private KeycloakSecurityContextClientRequestInterceptor interceptor = + mock(KeycloakSecurityContextClientRequestInterceptor.class); + + @Before + public void setup() { + customizer = new KeycloakRestTemplateCustomizer(interceptor); + } + + @Test + public void interceptorIsAddedToRequest() { + RestTemplate restTemplate = new RestTemplate(); + customizer.customize(restTemplate); + assertTrue(restTemplate.getInterceptors().contains(interceptor)); + } + +} diff --git a/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptorTest.java b/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptorTest.java new file mode 100644 index 00000000000..689cc65274d --- /dev/null +++ b/adapters/oidc/spring-boot2/src/test/java/org/keycloak/adapters/springboot/client/KeycloakSecurityContextClientRequestInterceptorTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.springboot.client; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.security.Principal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +/** + * Keycloak spring boot client request factory tests. + */ +public class KeycloakSecurityContextClientRequestInterceptorTest { + + @Spy + private KeycloakSecurityContextClientRequestInterceptor factory; + + private MockHttpServletRequest servletRequest; + + @Mock + private KeycloakSecurityContext keycloakSecurityContext; + + @Mock + private KeycloakPrincipal keycloakPrincipal; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + servletRequest = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(servletRequest)); + servletRequest.setUserPrincipal(keycloakPrincipal); + when(keycloakPrincipal.getKeycloakSecurityContext()).thenReturn(keycloakSecurityContext); + } + + @Test + public void testGetKeycloakSecurityContext() throws Exception { + KeycloakSecurityContext context = factory.getKeycloakSecurityContext(); + assertNotNull(context); + assertEquals(keycloakSecurityContext, context); + } + + @Test(expected = IllegalStateException.class) + public void testGetKeycloakSecurityContextInvalidPrincipal() throws Exception { + servletRequest.setUserPrincipal(new MarkerPrincipal()); + factory.getKeycloakSecurityContext(); + } + + @Test(expected = IllegalStateException.class) + public void testGetKeycloakSecurityContextNullAuthentication() throws Exception { + servletRequest.setUserPrincipal(null); + factory.getKeycloakSecurityContext(); + } + + private static class MarkerPrincipal implements Principal { + @Override + public String getName() { + return null; + } + } +} diff --git a/boms/adapter/pom.xml b/boms/adapter/pom.xml index 1f4a23bf7ed..3594cba3f2d 100644 --- a/boms/adapter/pom.xml +++ b/boms/adapter/pom.xml @@ -109,6 +109,11 @@ keycloak-spring-boot-adapter 4.0.0.CR1-SNAPSHOT + + org.keycloak + keycloak-spring-boot-2-adapter + 4.0.0.CR1-SNAPSHOT + org.keycloak spring-boot-container-bundle @@ -124,6 +129,16 @@ keycloak-spring-boot-starter 4.0.0.CR1-SNAPSHOT + + org.keycloak + keycloak-spring-boot-2-starter + 4.0.0.CR1-SNAPSHOT + + + org.keycloak + keycloak-spring-boot-2-starter + 4.0.0.CR1-SNAPSHOT + org.keycloak keycloak-authz-client diff --git a/misc/spring-boot-2-starter/keycloak-spring-boot-2-starter/pom.xml b/misc/spring-boot-2-starter/keycloak-spring-boot-2-starter/pom.xml new file mode 100644 index 00000000000..2a2ff27095d --- /dev/null +++ b/misc/spring-boot-2-starter/keycloak-spring-boot-2-starter/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + org.keycloak + keycloak-spring-boot-2-starter-parent + 4.0.0.CR1-SNAPSHOT + + keycloak-spring-boot-2-starter + Keycloak :: Spring :: Boot :: 2 :: Default :: Starter + Spring Boot 2 Default Starter for Keycloak + + + 2.0.0.RELEASE + + + + + org.keycloak + keycloak-spring-boot-2-adapter + + + org.keycloak + keycloak-authz-client + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.keycloak + spring-boot-container-bundle + + + org.keycloak + keycloak-spring-security-adapter + + + diff --git a/misc/spring-boot-2-starter/pom.xml b/misc/spring-boot-2-starter/pom.xml new file mode 100644 index 00000000000..69cd97a2be5 --- /dev/null +++ b/misc/spring-boot-2-starter/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + keycloak-misc-parent + org.keycloak + 4.0.0.CR1-SNAPSHOT + + org.keycloak + keycloak-spring-boot-2-starter-parent + Keycloak :: Spring :: Boot ::2 + Support for using Keycloak in Spring Boot 2 applications. + pom + + keycloak-spring-boot-2-starter + + + + + + org.keycloak.bom + keycloak-adapter-bom + 4.0.0.CR1-SNAPSHOT + pom + import + + + + diff --git a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml index f9d550b2d50..d130fec0348 100644 --- a/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml +++ b/misc/spring-boot-starter/keycloak-spring-boot-starter/pom.xml @@ -23,7 +23,7 @@ org.springframework.boot spring-boot-starter 1.5.2.RELEASE - + org.keycloak spring-boot-container-bundle diff --git a/pom.xml b/pom.xml index a82b1a2fa2b..7b4f3d42bae 100755 --- a/pom.xml +++ b/pom.xml @@ -882,11 +882,21 @@ keycloak-servlet-oauth-client ${project.version} + + org.keycloak + keycloak-spring-boot-adapter-core + ${project.version} + org.keycloak keycloak-spring-boot-adapter ${project.version} + + org.keycloak + keycloak-spring-boot-2-adapter + ${project.version} + org.keycloak keycloak-tomcat-adapter-spi diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw new file mode 100755 index 00000000000..5bf251c0774 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw @@ -0,0 +1,225 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +echo $MAVEN_PROJECTBASEDIR +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw.cmd b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw.cmd new file mode 100644 index 00000000000..019bd74d766 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/mvnw.cmd @@ -0,0 +1,143 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/pom.xml b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/pom.xml new file mode 100644 index 00000000000..a4fc004e310 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/pom.xml @@ -0,0 +1,224 @@ + + + 4.0.0 + + org.keycloak + spring-boot-2-adapter + 0.0.1-SNAPSHOT + jar + + spring-boot-adapter + Spring boot adapter test application + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + 4.0.0.CR1-SNAPSHOT + + + + + + + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-web + + + + org.keycloak + keycloak-spring-boot-2-adapter + ${keycloak.version} + + + + + + + spring-boot-adapter-tomcat + + + org.springframework.boot + spring-boot-starter-web + + + org.keycloak + keycloak-tomcat8-adapter + ${keycloak.version} + + + + + + spring-boot-adapter-jetty + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + + spring-boot-adapter-undertow + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + org.keycloak + keycloak-undertow-adapter + ${keycloak.version} + + + + + + repo-url + + + repo.url + + + + + custom-repo + custom repo + ${repo.url} + + + + + + jetty-version-81 + + + jetty.adapter.version + 81 + + + + 8.1.22.v20160922 + + + + org.keycloak + keycloak-jetty81-adapter + ${keycloak.version} + + + org.springframework.boot + spring-boot-starter-jetty + + + org.eclipse.jetty.websocket + * + + + + + + + + jetty-version-92 + + + jetty.adapter.version + 92 + + + + 9.2.23.v20171218 + + + + org.keycloak + keycloak-jetty92-adapter + ${keycloak.version} + + + org.springframework.boot + spring-boot-starter-jetty + + + + + + jetty-version-93 + + + jetty.adapter.version + 93 + + + + 9.3.22.v20171030 + + + + org.keycloak + keycloak-jetty93-adapter + ${keycloak.version} + + + org.springframework.boot + spring-boot-starter-jetty + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/AdminController.java b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/AdminController.java new file mode 100644 index 00000000000..28571729d49 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/AdminController.java @@ -0,0 +1,142 @@ +package org.keycloak; + +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeycloakUriBuilder; +import org.keycloak.common.util.Time; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.util.JsonSerialization; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.request.WebRequest; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.UUID; + +@Controller +@RequestMapping(path = "/admin") +public class AdminController { + + private static Logger logger = LoggerFactory.getLogger(AdminController.class); + + @RequestMapping(path = "/TokenServlet", method = RequestMethod.GET) + public String showTokens(WebRequest req, Model model, @RequestParam Map attributes) throws IOException { + String timeOffset = attributes.get("timeOffset"); + if (!StringUtils.isEmpty(timeOffset)) { + int offset; + try { + offset = Integer.parseInt(timeOffset, 10); + } + catch (NumberFormatException e) { + offset = 0; + } + + Time.setOffset(offset); + } + + RefreshableKeycloakSecurityContext ctx = + (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName(), WebRequest.SCOPE_REQUEST); + String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken()); + RefreshToken refreshToken; + try { + refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class); + } catch (JWSInputException e) { + throw new IOException(e); + } + String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken); + + model.addAttribute("accessToken", accessTokenPretty); + model.addAttribute("refreshToken", refreshTokenPretty); + model.addAttribute("accessTokenString", ctx.getTokenString()); + + return "tokens"; + } + + @RequestMapping(path = "/SessionServlet", method = RequestMethod.GET) + public String sessionServlet(WebRequest webRequest, Model model) { + String counterString = (String) webRequest.getAttribute("counter", RequestAttributes.SCOPE_SESSION); + int counter = 0; + try { + counter = Integer.parseInt(counterString, 10); + } + catch (NumberFormatException ignored) { + } + + model.addAttribute("counter", counter); + + webRequest.setAttribute("counter", Integer.toString(counter+1), RequestAttributes.SCOPE_SESSION); + + return "session"; + } + + @RequestMapping(path = "/LinkServlet", method = RequestMethod.GET) + public String tokenController(WebRequest webRequest, + @RequestParam Map attributes, + Model model) { + + ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpSession httpSession = attr.getRequest().getSession(true); + +// response.addHeader("Cache-Control", "no-cache"); + + String responseAttr = attributes.get("response"); + + if (StringUtils.isEmpty(responseAttr)) { + String provider = attributes.get("provider"); + String realm = attributes.get("realm"); + KeycloakSecurityContext keycloakSession = + (KeycloakSecurityContext) webRequest.getAttribute( + KeycloakSecurityContext.class.getName(), + RequestAttributes.SCOPE_REQUEST); + AccessToken token = keycloakSession.getToken(); + String clientId = token.getAudience()[0]; + String nonce = UUID.randomUUID().toString(); + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + String input = nonce + token.getSessionState() + clientId + provider; + byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8)); + String hash = Base64Url.encode(check); + httpSession.setAttribute("hash", hash); + String redirectUri = KeycloakUriBuilder.fromUri("http://localhost:8280/admin/LinkServlet") + .queryParam("response", "true").build().toString(); + String accountLinkUrl = KeycloakUriBuilder.fromUri("http://localhost:8180/") + .path("/auth/realms/{realm}/broker/{provider}/link") + .queryParam("nonce", nonce) + .queryParam("hash", hash) + .queryParam("client_id", token.getIssuedFor()) + .queryParam("redirect_uri", redirectUri).build(realm, provider).toString(); + + return "redirect:" + accountLinkUrl; + } else { + String error = attributes.get("link_error"); + if (StringUtils.isEmpty(error)) + model.addAttribute("error", "Account linked"); + else + model.addAttribute("error", error); + + return "linking"; + } + } +} diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java new file mode 100644 index 00000000000..38332990344 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/java/org/keycloak/SpringBootAdapterApplication.java @@ -0,0 +1,12 @@ +package org.keycloak; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringBootAdapterApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootAdapterApplication.class, args); + } +} diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/application.properties b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/application.properties new file mode 100644 index 00000000000..84de1bbcd47 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/application.properties @@ -0,0 +1,12 @@ +server.port=8280 + +keycloak.realm=test +keycloak.auth-server-url=http://localhost:8180/auth +keycloak.ssl-required=external +keycloak.resource=spring-boot-app +keycloak.credentials.secret=e3789ac5-bde6-4957-a7b0-612823dac101 +keycloak.realm-key=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB + +keycloak.security-constraints[0].authRoles[0]=admin +keycloak.security-constraints[0].securityCollections[0].name=Admin zone +keycloak.security-constraints[0].securityCollections[0].patterns[0]=/admin/* \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/admin/index.html b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/admin/index.html new file mode 100644 index 00000000000..acb47afb71d --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/admin/index.html @@ -0,0 +1,12 @@ + + + + + springboot admin page + + + +
You are now admin
+ + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/index.html b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/index.html new file mode 100644 index 00000000000..5ca73039924 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/static/index.html @@ -0,0 +1,12 @@ + + + + + springboot test page + + + +
Click here to go admin
+ + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/linking.html b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/linking.html new file mode 100644 index 00000000000..6c7d5bd2b68 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/linking.html @@ -0,0 +1,9 @@ + + + + Linking page result + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/session.html b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/session.html new file mode 100644 index 00000000000..9a7e52f027a --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/session.html @@ -0,0 +1,9 @@ + + + + session counter page + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/tokens.html b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/tokens.html new file mode 100644 index 00000000000..09dee7263d2 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/main/resources/templates/tokens.html @@ -0,0 +1,11 @@ + + + + Tokens from spring boot + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java new file mode 100644 index 00000000000..8df20da764d --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-2-adapter/src/test/java/org/keycloak/SpringBootAdapterApplicationTests.java @@ -0,0 +1,16 @@ +package org.keycloak; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SpringBootAdapterApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml index ab852a8935a..30bb02b2599 100644 --- a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/pom.xml @@ -23,7 +23,7 @@ UTF-8 1.8 - 3.3.0.CR1-SNAPSHOT + 4.0.0.CR1-SNAPSHOT diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml index a25a03922c6..ec70801c236 100644 --- a/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/pom.xml @@ -145,6 +145,55 @@ + + test-springboot-2 + + - + + + + + + com.bazaarvoice.maven.plugins + process-exec-maven-plugin + 0.7 + + + spring-boot-application-process + generate-test-resources + + start + + + springboot + ../../../../test-apps/spring-boot-2-adapter + http://localhost:8280/index.html + 360 + + mvn + spring-boot:run + -B + -Dkeycloak.version=${project.version} + -Pspring-boot-adapter-${adapter.container} + -Dmaven.repo.local=${settings.localRepository} + -Djetty.adapter.version=${jetty.adapter.version} + ${repo.argument} + + + + + + kill-processes + post-integration-test + + stop-all + + + + + + + turn-on-repo-url