+ *
+ * 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 hello.config;
+
+import de.sephirothj.spring.security.ltpa2.reactive.Ltpa2AuthConverter;
+import de.sephirothj.spring.security.ltpa2.reactive.Ltpa2AuthManager;
+import de.sephirothj.spring.security.ltpa2.LtpaKeyUtils;
+import hello.auth.MyLdapUserDetailsManager;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+import javax.crypto.SecretKey;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.ldap.DefaultLdapUsernameToDnMapper;
+import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
+
+@Configuration
+@EnableWebFluxSecurity
+@EnableReactiveMethodSecurity
+@EnableCaching
+public class WebSecurityConfig
+{
+
+ @Bean
+ public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http, final ReactiveUserDetailsService userDetailsService, AuthenticationWebFilter ltpa2AuthenticationWebFilter)
+ {
+ http
+ .csrf().disable()
+ .httpBasic().disable()
+ .authorizeExchange()
+ .pathMatchers(
+ "/",
+ "/home"
+ ).permitAll()
+ .pathMatchers("/hello").hasRole("DEVELOPERS")
+ // all other require any authentication
+ .anyExchange().authenticated()
+ .and()
+ // apply ltpa2 authentication filter
+ .addFilterAt(ltpa2AuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
+ return http.build();
+ }
+
+ @Bean
+ AuthenticationWebFilter x509AuthenticationWebFilter(ReactiveUserDetailsService userDetailsService) throws GeneralSecurityException
+ {
+ final Ltpa2AuthConverter converter = new Ltpa2AuthConverter();
+ converter.setSharedKey(sharedKey());
+ converter.setSignerKey(signerKey());
+
+ final AuthenticationWebFilter webfilter = new AuthenticationWebFilter(new Ltpa2AuthManager(userDetailsService));
+ webfilter.setServerAuthenticationConverter(converter);
+ return webfilter;
+ }
+
+ @Bean
+ public ReactiveUserDetailsService userDetailsService()
+ {
+ final DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource("ldap://127.0.0.1:33389/dc=foo,dc=bar");
+ contextSource.afterPropertiesSet();
+
+ MyLdapUserDetailsManager manager = new MyLdapUserDetailsManager(contextSource);
+ manager.setUsernameMapper(new DefaultLdapUsernameToDnMapper("ou=user", "cn"));
+ manager.setGroupSearchBase("ou=groups");
+
+ return manager;
+ }
+
+ private SecretKey sharedKey() throws GeneralSecurityException
+ {
+ String testKey = "JvywHhxC+EhtUdeusbo31E5IUOEPmbMxMnKTTOB39fo=";
+ String testKeyPass = "test123";
+ return LtpaKeyUtils.decryptSharedKey(testKey, testKeyPass);
+ }
+
+ private PublicKey signerKey() throws GeneralSecurityException
+ {
+ String testSignerKey = "AOECPMDAs0o7MzQIgxZhAXJZ2BaDE3mqRZAbkbQO38CgUIgeAPEA3iWIYp+p/Ai0J4//UOml20an+AuCnDGzcFCaf3S3EAiR4cK59vl/u8TIswPIg2akh4J7qL3E/qRxN9WD945tS3h0YhJZSq7rC22wytLsxbFuKpEuYfm1i5spAQAB";
+ return LtpaKeyUtils.decodePublicKey(testSignerKey);
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..f368dc4
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,7 @@
+spring:
+ main:
+ web-application-type: "reactive"
+ ldap:
+ embedded:
+ baseDn: dc=foo,dc=bar
+ port: 33389
diff --git a/src/main/resources/schema.ldif b/src/main/resources/schema.ldif
new file mode 100644
index 0000000..3582f1e
--- /dev/null
+++ b/src/main/resources/schema.ldif
@@ -0,0 +1,31 @@
+dn: dc=foo,dc=bar
+objectclass: top
+objectclass: dcObject
+objectclass: organization
+dc: foo
+o: foo
+
+dn: ou=user,dc=foo,dc=bar
+objectclass: top
+objectclass: organizationalUnit
+ou: user
+
+dn: cn=fae6d87c-c642-45a6-9f09-915c7fd8b08c,ou=user,dc=foo,dc=bar
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Rod Johnson
+sn: Johnson
+uid: fae6d87c-c642-45a6-9f09-915c7fd8b08c
+
+dn: ou=groups,dc=foo,dc=bar
+objectclass: top
+objectclass: organizationalUnit
+ou: groups
+
+dn: cn=developers,ou=groups,dc=foo,dc=bar
+objectclass: top
+objectclass: groupOfUniqueNames
+cn: developers
+uniqueMember: cn=fae6d87c-c642-45a6-9f09-915c7fd8b08c,ou=user,dc=foo,dc=bar
diff --git a/src/main/resources/templates/hello.html b/src/main/resources/templates/hello.html
new file mode 100644
index 0000000..8cf4d9c
--- /dev/null
+++ b/src/main/resources/templates/hello.html
@@ -0,0 +1,10 @@
+
+
+
+ Hello World!
+
+
+ Hello !
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html
new file mode 100644
index 0000000..359d8f3
--- /dev/null
+++ b/src/main/resources/templates/home.html
@@ -0,0 +1,11 @@
+
+
+
+ Spring Security LTPA2 Example
+
+
+ Welcome!
+
+ Click here to see a greeting.
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/securedMethod.html b/src/main/resources/templates/securedMethod.html
new file mode 100644
index 0000000..df46a86
--- /dev/null
+++ b/src/main/resources/templates/securedMethod.html
@@ -0,0 +1,10 @@
+
+
+
+ Getting Started: Serving Web Content
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/hello/ApplicationTests.java b/src/test/java/hello/ApplicationTests.java
new file mode 100644
index 0000000..2a47d07
--- /dev/null
+++ b/src/test/java/hello/ApplicationTests.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2019 Ronny "Sephiroth" Perinke
+ *
+ * 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 hello;
+
+import de.sephirothj.spring.security.ltpa2.Ltpa2Token;
+import de.sephirothj.spring.security.ltpa2.Ltpa2Utils;
+import de.sephirothj.spring.security.ltpa2.LtpaKeyUtils;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.time.LocalDateTime;
+import javax.crypto.SecretKey;
+import lombok.NonNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest
+class ApplicationTests
+{
+ @Autowired
+ private ApplicationContext context;
+
+ private WebTestClient webTestClient;
+
+ @BeforeEach
+ void setup()
+ {
+ webTestClient = WebTestClient.bindToApplicationContext(context)
+ .apply(springSecurity())
+ .configureClient()
+ .build();
+ }
+
+ @Test
+ void accessUnsecuredResourceThenOk()
+ {
+ webTestClient.get().uri("/")
+ .exchange()
+ .expectStatus().isOk();
+ }
+
+ @Test
+ void accessSecuredResourceUnauthenticatedShouldBeForbidden()
+ {
+ webTestClient.get().uri("/hello")
+ .exchange()
+ .expectStatus().isUnauthorized();
+ }
+
+ @Test
+ void accessSecuredResourceWithInvalidAuthenticationShouldBeForbidden() throws Exception
+ {
+ webTestClient.get().uri("/hello")
+ .header(HttpHeaders.AUTHORIZATION, "sadfdas")
+ .exchange()
+ .expectStatus().isUnauthorized();
+ }
+
+ @Test
+ void accessSecuredResourceWithAuthenticationThenOk() throws Exception
+ {
+ Ltpa2Token token = createTestToken();
+
+ webTestClient.get().uri("/hello")
+ .header(HttpHeaders.AUTHORIZATION, "LtpaToken2 ".concat(encryptToken(token)))
+ .exchange()
+ .expectStatus().isOk();
+ }
+
+ @Test
+ void accessSecuredResourceWithCookieThenOk() throws Exception
+ {
+ Ltpa2Token token = createTestToken();
+
+ webTestClient.get().uri("/secured-method")
+ .cookie("LtpaToken2", encryptToken(token))
+ .exchange()
+ .expectStatus().isOk();
+ }
+
+ @Test
+ @WithMockUser(roles = "DEVELOPERS")
+ void accessSecuredResourceAuthenticatedThenOk() throws Exception
+ {
+ webTestClient.get().uri("/hello")
+ .exchange()
+ .expectStatus().isOk();
+ }
+
+ private Ltpa2Token createTestToken()
+ {
+ Ltpa2Token token = new Ltpa2Token();
+ token.setUser("user:LdapRegistry/CN=fae6d87c-c642-45a6-9f09-915c7fd8b08c,OU=user,DC=foo,DC=bar");
+ token.setExpire(LocalDateTime.now().plusMinutes(1));
+ return token;
+ }
+
+ private String encryptToken(@NonNull Ltpa2Token token) throws GeneralSecurityException
+ {
+ SecretKey sharedKey = LtpaKeyUtils.decryptSharedKey(Constants.ENCRYPTED_SHARED_KEY, Constants.ENCRYPTION_PASSWORD);
+ PrivateKey privateKey = LtpaKeyUtils.decryptPrivateKey(Constants.ENCRYPTED_PRIVATE_KEY, Constants.ENCRYPTION_PASSWORD);
+
+ return Ltpa2Utils.encryptToken(token, privateKey, sharedKey);
+ }
+}
diff --git a/src/test/java/hello/Constants.java b/src/test/java/hello/Constants.java
new file mode 100644
index 0000000..52b1ad7
--- /dev/null
+++ b/src/test/java/hello/Constants.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 Ronny "Sephiroth" Perinke
+ *
+ * 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 hello;
+
+import lombok.experimental.UtilityClass;
+
+/**
+ *
+ * @author Sephiroth
+ */
+@UtilityClass
+public class Constants
+{
+ /**
+ * base64-encoded and encrypted private key used to sign the tokens
+ */
+ public final String ENCRYPTED_PRIVATE_KEY = "dP4F2H1MSphvjXHFnLqc1sUYiM83Mkg5MzCQWbxya0xMLPl6lwSbC9+SuCpbHTb9Qdl1w3d5bcDf400tBnfStdtkRYSOeo9oEbXOG4RqIV0x3WZx7AyJ5D8wVIfzOjgvfdQXqNkoiatyMwptvCytyEVbWH2kj3j0gB8O2/miPsbnZqNdIRDAt4TE2YjhVagC/ZP2xxxwncLDexF8Bme7NaMtJUlGMe8Nhkb61Z52PU2FHJAF6zPaTwj+JcZ/tg63lr5wRI9hGFOb7MhBrhgm9YiBqPOT30Crl28FHtTP9pnrqiC45QxU3aXVsYFh0hXptkkK9HeTk/YWFjDPVlfg9azrgGq64wHHg3cSjV21GAE=";
+
+ /**
+ * base64-encoded public key which corresponds to {@link #ENCRYPTED_PRIVATE_KEY}
+ */
+ public final String ENCODED_PUBLIC_KEY = "AOECPMDAs0o7MzQIgxZhAXJZ2BaDE3mqRZAbkbQO38CgUIgeAPEA3iWIYp+p/Ai0J4//UOml20an+AuCnDGzcFCaf3S3EAiR4cK59vl/u8TIswPIg2akh4J7qL3E/qRxN9WD945tS3h0YhJZSq7rC22wytLsxbFuKpEuYfm1i5spAQAB";
+
+ /**
+ * base64-encoded and encrypted shared secret key used to encrypt and decrypt the tokens
+ */
+ public final String ENCRYPTED_SHARED_KEY = "JvywHhxC+EhtUdeusbo31E5IUOEPmbMxMnKTTOB39fo=";
+
+ /**
+ * password that has been used for encypting of {@link #ENCRYPTED_PRIVATE_KEY} and {@link #ENCRYPTED_SHARED_KEY}
+ */
+ public final String ENCRYPTION_PASSWORD = "test123";
+}
diff --git a/src/test/java/hello/auth/MyLdapUserDetailsManagerTest.java b/src/test/java/hello/auth/MyLdapUserDetailsManagerTest.java
new file mode 100644
index 0000000..c70a471
--- /dev/null
+++ b/src/test/java/hello/auth/MyLdapUserDetailsManagerTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 Ronny "Sephiroth" Perinke
+ *
+ * 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 hello.auth;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ *
+ * @author Sephiroth
+ */
+class MyLdapUserDetailsManagerTest
+{
+ @Test
+ void extractUuidTestWithFullDn()
+ {
+ String username = "user:LdapRegistry/CN=fae6d87c-c642-45a6-9f09-915c7fd8b08c,OU=user,DC=foo,DC=bar";
+
+ assertThat(MyLdapUserDetailsManager.extractUuid(username)).isEqualTo("fae6d87c-c642-45a6-9f09-915c7fd8b08c");
+ }
+
+ @Test
+ void extractUuidTestWithUIDOnly()
+ {
+ String username = "fae6d87c-c642-45a6-9f09-915c7fd8b08c";
+
+ assertThat(MyLdapUserDetailsManager.extractUuid(username)).isEqualTo(username);
+ }
+
+}
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
new file mode 100644
index 0000000..c25977d
--- /dev/null
+++ b/src/test/resources/application.yml
@@ -0,0 +1,17 @@
+spring:
+ main:
+ web-application-type: "reactive"
+ ldap:
+ embedded:
+ baseDn: dc=foo,dc=bar
+ port: 33389
+
+logging:
+ level:
+ org:
+ springframework:
+ security:
+ ldap: DEBUG
+ hello:
+ auth: DEBUG
+
\ No newline at end of file