From 04e5f249a45a8dd6a30740b7bb2d11c01f56bd9e Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 6 Nov 2023 08:49:15 -0700 Subject: [PATCH] Add Security Tests - Embedded LDAP - Authorization Server - Resource Server - Security with MVC - Security with WebFlux --- STATUS.adoc | 16 ++ ci/smoke-tests.yml | 5 + security/security-ldap/README.md | 1 + security/security-ldap/build.gradle | 28 +++ ...ecurityLdapApplicationCheckpointTests.java | 43 +++++ .../example/security/ldap/MainController.java | 22 +++ .../example/security/ldap/SecurityConfig.java | 38 ++++ .../ldap/SecurityLdapApplication.java | 13 ++ .../src/main/resources/users.ldif | 43 +++++ .../ldap/SecurityLdapApplicationTests.java | 48 +++++ .../README.md | 1 + .../build.gradle | 24 +++ ...ationServerApplicationCheckpointTests.java | 143 +++++++++++++++ .../OAuth2AuthorizationServerApplication.java | 35 ++++ ...horizationServerSecurityConfiguration.java | 166 ++++++++++++++++++ .../security-oauth2-resource-server/README.md | 1 + .../build.gradle | 23 +++ ...ourceServerApplicationCheckpointTests.java | 79 +++++++++ .../OAuth2ResourceServerApplication.java | 28 +++ .../OAuth2ResourceServerController.java | 43 +++++ ...h2ResourceServerSecurityConfiguration.java | 55 ++++++ ...MockWebServerEnvironmentPostProcessor.java | 43 +++++ .../env/MockWebServerPropertySource.java | 148 ++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../src/main/resources/application.yml | 7 + security/security-webflux/README.adoc | 1 + security/security-webflux/build.gradle | 21 +++ ...rityWebFluxApplicationCheckpointTests.java | 111 ++++++++++++ .../webflux/SecurityWebFluxApplication.java | 13 ++ .../security/webflux/TestRestController.java | 31 ++++ .../security/webflux/WebSecurityConfig.java | 47 +++++ .../src/main/resources/public/bar.html | 1 + .../src/main/resources/static/foo.html | 1 + security/security-webmvc/README.adoc | 1 + security/security-webmvc/build.gradle | 21 +++ ...urityWebMvcApplicationCheckpointTests.java | 111 ++++++++++++ .../webmvc/SecurityWebMvcApplication.java | 13 ++ .../security/webmvc/TestRestController.java | 29 +++ .../security/webmvc/WebSecurityConfig.java | 47 +++++ .../src/main/resources/public/bar.html | 1 + .../src/main/resources/static/foo.html | 1 + settings.gradle | 4 +- 42 files changed, 1506 insertions(+), 2 deletions(-) create mode 100644 security/security-ldap/README.md create mode 100644 security/security-ldap/build.gradle create mode 100644 security/security-ldap/src/appTest/java/com/example/security/ldap/SecurityLdapApplicationCheckpointTests.java create mode 100644 security/security-ldap/src/main/java/com/example/security/ldap/MainController.java create mode 100644 security/security-ldap/src/main/java/com/example/security/ldap/SecurityConfig.java create mode 100644 security/security-ldap/src/main/java/com/example/security/ldap/SecurityLdapApplication.java create mode 100644 security/security-ldap/src/main/resources/users.ldif create mode 100644 security/security-ldap/src/test/java/com/example/security/ldap/SecurityLdapApplicationTests.java create mode 100644 security/security-oauth2-authorization-server/README.md create mode 100644 security/security-oauth2-authorization-server/build.gradle create mode 100644 security/security-oauth2-authorization-server/src/appTest/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplicationCheckpointTests.java create mode 100644 security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplication.java create mode 100644 security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerSecurityConfiguration.java create mode 100644 security/security-oauth2-resource-server/README.md create mode 100644 security/security-oauth2-resource-server/build.gradle create mode 100644 security/security-oauth2-resource-server/src/appTest/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplicationCheckpointTests.java create mode 100644 security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplication.java create mode 100644 security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerController.java create mode 100644 security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerSecurityConfiguration.java create mode 100644 security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerEnvironmentPostProcessor.java create mode 100644 security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerPropertySource.java create mode 100644 security/security-oauth2-resource-server/src/main/resources/META-INF/spring.factories create mode 100644 security/security-oauth2-resource-server/src/main/resources/application.yml create mode 100644 security/security-webflux/README.adoc create mode 100644 security/security-webflux/build.gradle create mode 100644 security/security-webflux/src/appTest/java/com/example/security/webflux/SecurityWebFluxApplicationCheckpointTests.java create mode 100644 security/security-webflux/src/main/java/com/example/security/webflux/SecurityWebFluxApplication.java create mode 100644 security/security-webflux/src/main/java/com/example/security/webflux/TestRestController.java create mode 100644 security/security-webflux/src/main/java/com/example/security/webflux/WebSecurityConfig.java create mode 100644 security/security-webflux/src/main/resources/public/bar.html create mode 100644 security/security-webflux/src/main/resources/static/foo.html create mode 100644 security/security-webmvc/README.adoc create mode 100644 security/security-webmvc/build.gradle create mode 100644 security/security-webmvc/src/appTest/java/com/example/security/webmvc/SecurityWebMvcApplicationCheckpointTests.java create mode 100644 security/security-webmvc/src/main/java/com/example/security/webmvc/SecurityWebMvcApplication.java create mode 100644 security/security-webmvc/src/main/java/com/example/security/webmvc/TestRestController.java create mode 100644 security/security-webmvc/src/main/java/com/example/security/webmvc/WebSecurityConfig.java create mode 100644 security/security-webmvc/src/main/resources/public/bar.html create mode 100644 security/security-webmvc/src/main/resources/static/foo.html diff --git a/STATUS.adoc b/STATUS.adoc index f20b04d..8699703 100644 --- a/STATUS.adoc +++ b/STATUS.adoc @@ -182,3 +182,19 @@ h|test |=== +== Security + +[%header,cols="4"] +|=== +h|Smoke Test +h|appTest +h|checkpointRestoreAppTest +h|test + +|hello-security +|image:https://ci.spring.io/api/v1/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/hello-security-app-test/badge[link=https://ci.spring.io/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/hello-security-app-test] +|image:https://ci.spring.io/api/v1/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/hello-security-cr-app-test/badge[link=https://ci.spring.io/teams/spring-checkpoint-restore-smoke-tests/pipelines/spring-checkpoint-restore-smoke-tests-3.2.x/jobs/hello-security-cr-app-test] +| + +|=== + diff --git a/ci/smoke-tests.yml b/ci/smoke-tests.yml index b8d011d..30be95d 100644 --- a/ci/smoke-tests.yml +++ b/ci/smoke-tests.yml @@ -84,3 +84,8 @@ groups: - name: spring-pulsar-reactive app_test: true test: false + - name: security + smoke_tests: + - name: hello-security + app_test: true + test: false diff --git a/security/security-ldap/README.md b/security/security-ldap/README.md new file mode 100644 index 0000000..f2be944 --- /dev/null +++ b/security/security-ldap/README.md @@ -0,0 +1 @@ +Tests Spring Security Embedded LDAP diff --git a/security/security-ldap/build.gradle b/security/security-ldap/build.gradle new file mode 100644 index 0000000..90a7ffe --- /dev/null +++ b/security/security-ldap/build.gradle @@ -0,0 +1,28 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + constraints { + implementation("org.springframework.security:spring-security-ldap:6.2.0-SNAPSHOT") + } + + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.security:spring-security-ldap") + implementation("com.unboundid:unboundid-ldapsdk") + implementation("org.crac:crac:$cracVersion") + implementation(project(":cr-listener")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + + appTestImplementation(project(":cr-smoke-test-support")) +} + +crSmokeTest { + webApplication = true +} diff --git a/security/security-ldap/src/appTest/java/com/example/security/ldap/SecurityLdapApplicationCheckpointTests.java b/security/security-ldap/src/appTest/java/com/example/security/ldap/SecurityLdapApplicationCheckpointTests.java new file mode 100644 index 0000000..4c2af3e --- /dev/null +++ b/security/security-ldap/src/appTest/java/com/example/security/ldap/SecurityLdapApplicationCheckpointTests.java @@ -0,0 +1,43 @@ +package com.example.security.ldap; + +import org.junit.jupiter.api.Test; + +import org.springframework.cr.smoketest.support.junit.ApplicationTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +public class SecurityLdapApplicationCheckpointTests { + + @Test + void anonymousShouldBeUnauthorizedWithoutCredentials(WebTestClient client) { + client.get().uri("/").exchange().expectStatus().isUnauthorized(); + } + + @Test + void homeShouldShowUsername(WebTestClient client) { + client.get() + .uri("/") + .headers((header) -> header.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("Hello, user!")); + } + + @Test + void friendlyShouldShowGivenName(WebTestClient client) { + client.get() + .uri("/friendly") + .headers((header) -> header.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())) + .isEqualTo("Hello, Dianne Emu!")); + } + +} diff --git a/security/security-ldap/src/main/java/com/example/security/ldap/MainController.java b/security/security-ldap/src/main/java/com/example/security/ldap/MainController.java new file mode 100644 index 0000000..93dfc20 --- /dev/null +++ b/security/security-ldap/src/main/java/com/example/security/ldap/MainController.java @@ -0,0 +1,22 @@ +package com.example.security.ldap; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.ldap.userdetails.Person; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MainController { + + @GetMapping("/") + public String hello(Authentication authentication) { + return "Hello, " + authentication.getName() + "!"; + } + + @GetMapping("/friendly") + public String hello(@AuthenticationPrincipal Person person) { + return "Hello, " + person.getGivenName() + "!"; + } + +} diff --git a/security/security-ldap/src/main/java/com/example/security/ldap/SecurityConfig.java b/security/security-ldap/src/main/java/com/example/security/ldap/SecurityConfig.java new file mode 100644 index 0000000..f2530ea --- /dev/null +++ b/security/security-ldap/src/main/java/com/example/security/ldap/SecurityConfig.java @@ -0,0 +1,38 @@ +package com.example.security.ldap; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.UnboundIdContainer; +import org.springframework.security.ldap.userdetails.PersonContextMapper; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + UnboundIdContainer ldapContainer() throws Exception { + UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org", "classpath:users.ldif"); + container.setPort(0); + return container; + } + + @Bean + BaseLdapPathContextSource contextSource(UnboundIdContainer container) { + int port = container.getPort(); + return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org"); + } + + @Bean + AuthenticationManager ldapAuthenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setUserDetailsContextMapper(new PersonContextMapper()); + return factory.createAuthenticationManager(); + } + +} diff --git a/security/security-ldap/src/main/java/com/example/security/ldap/SecurityLdapApplication.java b/security/security-ldap/src/main/java/com/example/security/ldap/SecurityLdapApplication.java new file mode 100644 index 0000000..e07724b --- /dev/null +++ b/security/security-ldap/src/main/java/com/example/security/ldap/SecurityLdapApplication.java @@ -0,0 +1,13 @@ +package com.example.security.ldap; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SecurityLdapApplication { + + public static void main(String[] args) throws Throwable { + SpringApplication.run(SecurityLdapApplication.class, args); + } + +} diff --git a/security/security-ldap/src/main/resources/users.ldif b/security/security-ldap/src/main/resources/users.ldif new file mode 100644 index 0000000..d96e2ae --- /dev/null +++ b/security/security-ldap/src/main/resources/users.ldif @@ -0,0 +1,43 @@ +dn: ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: groups + +dn: ou=people,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: people + +dn: uid=admin,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Rod Johnson +sn: Johnson +uid: admin +userPassword: password + +dn: uid=user,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Dianne Emu +sn: Emu +uid: user +userPassword: password +givenName: Dianne Emu + +dn: cn=user,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: user +member: uid=admin,ou=people,dc=springframework,dc=org +member: uid=user,ou=people,dc=springframework,dc=org + +dn: cn=admin,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: admin +member: uid=admin,ou=people,dc=springframework,dc=org \ No newline at end of file diff --git a/security/security-ldap/src/test/java/com/example/security/ldap/SecurityLdapApplicationTests.java b/security/security-ldap/src/test/java/com/example/security/ldap/SecurityLdapApplicationTests.java new file mode 100644 index 0000000..b86282e --- /dev/null +++ b/security/security-ldap/src/test/java/com/example/security/ldap/SecurityLdapApplicationTests.java @@ -0,0 +1,48 @@ +package com.example.security.ldap; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class SecurityLdapApplicationTests { + + @Autowired + private MockMvc mvc; + + @Test + void rootWhenAuthenticatedThenSaysHelloUser() throws Exception { + // @formatter:off + this.mvc.perform(get("/") + .with(httpBasic("user", "password"))) + .andExpect(content().string("Hello, user!")); + // @formatter:on + } + + @Test + void rootWhenUnauthenticatedThen401() throws Exception { + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + void tokenWhenBadCredentialsThen401() throws Exception { + // @formatter:off + this.mvc.perform(get("/") + .with(httpBasic("user", "passwerd"))) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + +} diff --git a/security/security-oauth2-authorization-server/README.md b/security/security-oauth2-authorization-server/README.md new file mode 100644 index 0000000..3900d1b --- /dev/null +++ b/security/security-oauth2-authorization-server/README.md @@ -0,0 +1 @@ +Tests Spring Security OAuth2 Authorization Server diff --git a/security/security-oauth2-authorization-server/build.gradle b/security/security-oauth2-authorization-server/build.gradle new file mode 100644 index 0000000..bcf1b90 --- /dev/null +++ b/security/security-oauth2-authorization-server/build.gradle @@ -0,0 +1,24 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.security:spring-security-oauth2-authorization-server:1.0.0-SNAPSHOT") + implementation("org.crac:crac:$cracVersion") + implementation(project(":cr-listener")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + + appTestImplementation("com.fasterxml.jackson.core:jackson-databind") + appTestImplementation(project(":cr-smoke-test-support")) +} + +crSmokeTest { + webApplication = true +} + diff --git a/security/security-oauth2-authorization-server/src/appTest/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplicationCheckpointTests.java b/security/security-oauth2-authorization-server/src/appTest/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplicationCheckpointTests.java new file mode 100644 index 0000000..319a877 --- /dev/null +++ b/security/security-oauth2-authorization-server/src/appTest/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplicationCheckpointTests.java @@ -0,0 +1,143 @@ +package com.example.security.oauth2authorizationserver; + +import java.io.IOException; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.cr.smoketest.support.junit.ApplicationTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; + +@ApplicationTest +class OAuth2AuthorizationServerApplicationCheckpointTests { + + private static final String CLIENT_ID = "messaging-client"; + + private static final String CLIENT_SECRET = "secret"; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void performTokenRequestWhenValidClientCredentialsThenOk(WebTestClient client) { + // @formatter:off + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "client_credentials"); + formData.add("scope", "message:read"); + client.post().uri("/oauth2/token").headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET)) + .body(BodyInserters.fromFormData(formData)) + .exchange().expectStatus().isOk() + .expectBody().jsonPath("$.access_token").exists() + .jsonPath("$.expires_in").isNumber() + .jsonPath("$.scope").isEqualTo("message:read") + .jsonPath("$.token_type").isEqualTo("Bearer"); + // @formatter:on + } + + @Test + void performTokenRequestWhenMissingScopeThenOk(WebTestClient client) { + // @formatter:off + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "client_credentials"); + formData.add("scope", "message:read message:write"); + client.post().uri("/oauth2/token").headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET)) + .body(BodyInserters.fromFormData(formData)) + .exchange().expectStatus().isOk() + .expectBody().jsonPath("$.access_token").exists() + .jsonPath("$.expires_in").isNumber() + .jsonPath("$.scope").isEqualTo("message:read message:write") + .jsonPath("$.token_type").isEqualTo("Bearer"); + // @formatter:on + } + + @Test + void performTokenRequestWhenInvalidClientCredentialsThenUnauthorized(WebTestClient client) { + // @formatter:off + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "client_credentials"); + formData.add("scope", "message:read"); + client.post().uri("/oauth2/token").headers(badCredentials()) + .body(BodyInserters.fromFormData(formData)) + .exchange().expectStatus().isUnauthorized() + .expectBody().jsonPath("$.error").isEqualTo("invalid_client"); + // @formatter:on + } + + @Test + void performTokenRequestWhenMissingGrantTypeThenUnauthorized(WebTestClient client) { + // @formatter:off + client.post().uri("/oauth2/token").headers(badCredentials()) + .exchange().expectStatus().isUnauthorized() + .expectBody().jsonPath("$.error").isEqualTo("invalid_client"); + // @formatter:on + } + + @Test + void performTokenRequestWhenGrantTypeNotRegisteredThenBadRequest(WebTestClient client) { + // @formatter:off + client.post().uri("/oauth2/token").headers((headers) -> headers.setBasicAuth("login-client", "openid-connect")) + .body(BodyInserters.fromFormData("grant_type", "client_credentials")) + .exchange().expectStatus().isBadRequest() + .expectBody().jsonPath("$.error").isEqualTo("unauthorized_client"); + // @formatter:on + } + + @Test + void performIntrospectionRequestWhenValidTokenThenOk(WebTestClient client) throws Exception { + // @formatter:off + client.post().uri("/oauth2/introspect").headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET)) + .body(BodyInserters.fromFormData("token", getAccessToken(client))) + .exchange().expectStatus().isOk() + .expectBody().jsonPath("$.active").isEqualTo("true") + .jsonPath("$.aud[0]").isEqualTo(CLIENT_ID) + .jsonPath("$.client_id").isEqualTo(CLIENT_ID) + .jsonPath("$.exp").isNumber() + .jsonPath("$.iat").isNumber() + .jsonPath("$.iss").isEqualTo("http://localhost:9000") + .jsonPath("$.nbf").isNumber() + .jsonPath("$.scope").isEqualTo("message:read") + .jsonPath("$.sub").isEqualTo(CLIENT_ID) + .jsonPath("$.token_type").isEqualTo("Bearer"); + // @formatter:on + } + + @Test + void performIntrospectionRequestWhenInvalidCredentialsThenUnauthorized(WebTestClient client) throws Exception { + // @formatter:off + client.post().uri("/oauth2/introspect").headers(badCredentials()) + .body(BodyInserters.fromFormData("token", getAccessToken(client))) + .exchange().expectStatus().isUnauthorized() + .expectBody().jsonPath("$.error").isEqualTo("invalid_client"); + // @formatter:on + } + + private static Consumer badCredentials() { + return (headers) -> headers.setBasicAuth("bad", "password"); + } + + private String getAccessToken(WebTestClient client) throws IOException { + // @formatter:off + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "client_credentials"); + formData.add("scope", "message:read"); + byte[] responseBody = client.post().uri("/oauth2/token") + .headers((headers) -> headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET)) + .body(BodyInserters.fromFormData(formData)) + .exchange().expectStatus().isOk() + .expectBody().jsonPath("$.access_token").exists() + .returnResult().getResponseBody(); + // @formatter:on + + Map tokenResponse = this.objectMapper.readValue(responseBody, new TypeReference<>() { + }); + + return tokenResponse.get("access_token").toString(); + } + +} diff --git a/security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplication.java b/security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplication.java new file mode 100644 index 0000000..5d7b5ab --- /dev/null +++ b/security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.oauth2authorizationserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * OAuth Authorization Server Application. + * + * @author Steve Riesenberg + */ +@SpringBootApplication +public class OAuth2AuthorizationServerApplication { + + public static void main(String[] args) throws InterruptedException { + SpringApplication.run(OAuth2AuthorizationServerApplication.class, args); + Thread.currentThread().join(); // To be able to measure memory consumption + } + +} diff --git a/security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerSecurityConfiguration.java b/security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerSecurityConfiguration.java new file mode 100644 index 0000000..2f131d4 --- /dev/null +++ b/security/security-oauth2-authorization-server/src/main/java/com/example/security/oauth2authorizationserver/OAuth2AuthorizationServerSecurityConfiguration.java @@ -0,0 +1,166 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.oauth2authorizationserver; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.UUID; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +/** + * OAuth Authorization Server Configuration. + * + * @author Steve Riesenberg + */ +@Configuration +@EnableWebSecurity +public class OAuth2AuthorizationServerSecurityConfiguration { + + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + return http.formLogin(Customizer.withDefaults()).build(); + } + + @Bean + @Order(2) + public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .formLogin(Customizer.withDefaults()); + // @formatter:on + + return http.build(); + } + + @Bean + public RegisteredClientRepository registeredClientRepository() { + // @formatter:off + RegisteredClient loginClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("login-client") + .clientSecret("{noop}openid-connect") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .redirectUri("http://127.0.0.1:8080/login/oauth2/code/login-client") + .redirectUri("http://127.0.0.1:8080/authorized") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) + .build(); + RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("messaging-client") + .clientSecret("{noop}secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("message:read") + .scope("message:write") + .build(); + // @formatter:on + + return new InMemoryRegisteredClientRepository(loginClient, registeredClient); + } + + @Bean + public JWKSource jwkSource(KeyPair keyPair) { + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // @formatter:off + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + + @Bean + public JwtDecoder jwtDecoder(KeyPair keyPair) { + return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build(); + } + + @Bean + public AuthorizationServerSettings providerSettings() { + return AuthorizationServerSettings.builder().issuer("http://localhost:9000").build(); + } + + @Bean + public UserDetailsService userDetailsService() { + // @formatter:off + UserDetails userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + // @formatter:on + + return new InMemoryUserDetailsManager(userDetails); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + KeyPair generateRsaKey() { + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } + +} diff --git a/security/security-oauth2-resource-server/README.md b/security/security-oauth2-resource-server/README.md new file mode 100644 index 0000000..3d92613 --- /dev/null +++ b/security/security-oauth2-resource-server/README.md @@ -0,0 +1 @@ +Tests Spring Security OAuth2 Resource Server diff --git a/security/security-oauth2-resource-server/build.gradle b/security/security-oauth2-resource-server/build.gradle new file mode 100644 index 0000000..05a950f --- /dev/null +++ b/security/security-oauth2-resource-server/build.gradle @@ -0,0 +1,23 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + implementation("com.squareup.okhttp3:mockwebserver") + implementation("org.crac:crac:$cracVersion") + implementation(project(":cr-listener")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + + appTestImplementation(project(":cr-smoke-test-support")) +} + +crSmokeTest { + webApplication = true +} diff --git a/security/security-oauth2-resource-server/src/appTest/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplicationCheckpointTests.java b/security/security-oauth2-resource-server/src/appTest/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplicationCheckpointTests.java new file mode 100644 index 0000000..38fcb55 --- /dev/null +++ b/security/security-oauth2-resource-server/src/appTest/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplicationCheckpointTests.java @@ -0,0 +1,79 @@ +package com.example.security.oauth2resourceserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.cr.smoketest.support.junit.ApplicationTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +public class OAuth2ResourceServerApplicationCheckpointTests { + + private static final String NONE_JWT = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjoyMTY0MjQ1ODgwLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMDFkOThlZWEtNjc0MC00OGRlLTk4ODAtYzM5ZjgyMGZiNzVlIiwiY2xpZW50X2lkIjoibm9zY29wZXMiLCJzY29wZSI6WyJub25lIl19.VOzgGLOUuQ_R2Ur1Ke41VaobddhKgUZgto7Y3AGxst7SuxLQ4LgWwdSSDRx-jRvypjsCgYPbjAYLhn9nCbfwtCitkymUKUNKdebvVAI0y8YvliWTL5S-GiJD9dN8SSsXUla9A4xB_9Mt5JAlRpQotQSCLojVSKQmjhMpQWmYAlKVjnlImoRwQFPI4w3Ijn4G4EMTKWUYRfrD0-WNT9ZYWBeza6QgV6sraP7ToRB3eQLy2p04cU40X-RHLeYCsMBfxsMMh89CJff-9tn7VDKi1hAGc_Lp9yS9ZaItJuFJTjf8S_vsjVB1nBhvdS_6IED_m_fOU52KiGSO2qL6shxHvg"; + + private static final String READ_JWT = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjoyMTY0MjQ1NjQ4LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiY2I1ZGMwNDYtMDkyMi00ZGJmLWE5MzAtOGI2M2FhZTYzZjk2IiwiY2xpZW50X2lkIjoicmVhZGVyIiwic2NvcGUiOlsibWVzc2FnZTpyZWFkIl19.Pre2ksnMiOGYWQtuIgHB0i3uTnNzD0SMFM34iyQJHK5RLlSjge08s9qHdx6uv5cZ4gZm_cB1D6f4-fLx76bCblK6mVcabbR74w_eCdSBXNXuqG-HNrOYYmmx5iJtdwx5fXPmF8TyVzsq_LvRm_LN4lWNYquT4y36Tox6ZD3feYxXvHQ3XyZn9mVKnlzv-GCwkBohCR3yPow5uVmr04qh_al52VIwKMrvJBr44igr4fTZmzwRAZmQw5rZeyep0b4nsCjadNcndHtMtYKNVuG5zbDLsB7GGvilcI9TDDnUXtwthB_3iq32DAd9x8wJmJ5K8gmX6GjZFtYzKk_zEboXoQ"; + + private static final String WRITE_JWT = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjoyMTY0MjQzOTA0LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiZGI4ZjgwMzQtM2VlNy00NjBjLTk3NTEtMDJiMDA1OWI5NzA4IiwiY2xpZW50X2lkIjoid3JpdGVyIiwic2NvcGUiOlsibWVzc2FnZTp3cml0ZSJdfQ.USvpx_ntKXtchLmc93auJq0qSav6vLm4B7ItPzhrDH2xmogBP35eKeklwXK5GCb7ck1aKJV5SpguBlTCz0bZC1zAWKB6gyFIqedALPAran5QR-8WpGfl0wFqds7d8Jw3xmpUUBduRLab9hkeAhgoVgxevc8d6ITM7kRnHo5wT3VzvBU8DquedVXm5fbBnRPgG4_jOWJKbqYpqaR2z2TnZRWh3CqL82Orh1Ww1dJYF_fae1dTVV4tvN5iSndYcGxMoBaiw3kRRi6EyNxnXnt1pFtZqc1f6D9x4AHiri8_vpBp2vwG5OfQD5-rrleP_XlIB3rNQT7tu3fiqu4vUzQaEg"; + + @Test + void shouldRespondWhenTokenPresent(WebTestClient client) { + client.get() + .uri("/") + .header("Authorization", bearer(NONE_JWT)) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith( + (result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("Hello, subject!")); + } + + @Test + void shouldAllowGetWhenTokenWithReadScope(WebTestClient client) { + client.get() + .uri("/message") + .header("Authorization", bearer(READ_JWT)) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith( + (result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("secret message")); + } + + @Test + void shouldBlockGetWhenTokenWithNoScope(WebTestClient client) { + client.get().uri("/message").header("Authorization", bearer(NONE_JWT)).exchange().expectStatus().isForbidden(); + } + + @Test + void shouldAllowPostWhenTokenWithScope(WebTestClient client) { + client.post() + .uri("/message") + .header("Authorization", bearer(WRITE_JWT)) + .bodyValue("my message") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())) + .isEqualTo("Message was created. Content: my message")); + } + + @Test + void shouldBlockPostWhenTokenWithBoScope(WebTestClient client) { + client.post() + .uri("/message") + .header("Authorization", bearer(NONE_JWT)) + .bodyValue("my message") + .exchange() + .expectStatus() + .isForbidden(); + } + + private static String bearer(String token) { + return "Bearer " + token; + } + +} diff --git a/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplication.java b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplication.java new file mode 100644 index 0000000..6a2b755 --- /dev/null +++ b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.oauth2resourceserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OAuth2ResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(OAuth2ResourceServerApplication.class, args); + } + +} diff --git a/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerController.java b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerController.java new file mode 100644 index 0000000..b0c42c4 --- /dev/null +++ b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerController.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.oauth2resourceserver; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class OAuth2ResourceServerController { + + @GetMapping("/") + public String index(@AuthenticationPrincipal Jwt jwt) { + return String.format("Hello, %s!", jwt.getSubject()); + } + + @GetMapping("/message") + public String message() { + return "secret message"; + } + + @PostMapping("/message") + public String createMessage(@RequestBody String message) { + return String.format("Message was created. Content: %s", message); + } + +} diff --git a/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerSecurityConfiguration.java b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerSecurityConfiguration.java new file mode 100644 index 0000000..72a540b --- /dev/null +++ b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/OAuth2ResourceServerSecurityConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.oauth2resourceserver; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration(proxyBeanMethods = false) +@EnableWebSecurity +public class OAuth2ResourceServerSecurityConfiguration { + + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + String jwkSetUri; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers(HttpMethod.GET, "/message/**").hasAuthority("SCOPE_message:read") + .requestMatchers(HttpMethod.POST, "/message/**").hasAuthority("SCOPE_message:write") + .anyRequest().authenticated() + ) + .oauth2ResourceServer((jwt) -> jwt.jwt(Customizer.withDefaults())); + // @formatter:on + return http.build(); + } + + @Bean + JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build(); + } + +} diff --git a/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerEnvironmentPostProcessor.java b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerEnvironmentPostProcessor.java new file mode 100644 index 0000000..9c60a08 --- /dev/null +++ b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerEnvironmentPostProcessor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.oauth2resourceserver.env; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * Adds {@link MockWebServerPropertySource} to the environment. + * + * @author Rob Winch + */ +public class MockWebServerEnvironmentPostProcessor implements EnvironmentPostProcessor, DisposableBean { + + private final MockWebServerPropertySource propertySource = new MockWebServerPropertySource(); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + environment.getPropertySources().addFirst(this.propertySource); + } + + @Override + public void destroy() throws Exception { + this.propertySource.destroy(); + } + +} diff --git a/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerPropertySource.java b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerPropertySource.java new file mode 100644 index 0000000..1be4fc9 --- /dev/null +++ b/security/security-oauth2-resource-server/src/main/java/com/example/security/oauth2resourceserver/env/MockWebServerPropertySource.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.oauth2resourceserver.env; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.crac.Context; +import org.crac.Core; +import org.crac.Resource; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.env.PropertySource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +/** + * Adds value for mockwebserver.url property. + * + * @author Rob Winch + */ +public class MockWebServerPropertySource extends PropertySource> + implements DisposableBean, Resource { + + private static final MockResponse JWKS_RESPONSE = response( + "{ \"keys\": [ { \"kty\": \"RSA\", \"e\": \"AQAB\", \"n\": \"jvBtqsGCOmnYzwe_-HvgOqlKk6HPiLEzS6uCCcnVkFXrhnkPMZ-uQXTR0u-7ZklF0XC7-AMW8FQDOJS1T7IyJpCyeU4lS8RIf_Z8RX51gPGnQWkRvNw61RfiSuSA45LR5NrFTAAGoXUca_lZnbqnl0td-6hBDVeHYkkpAsSck1NPhlcsn-Pvc2Vleui_Iy1U2mzZCM1Vx6Dy7x9IeP_rTNtDhULDMFbB_JYs-Dg6Zd5Ounb3mP57tBGhLYN7zJkN1AAaBYkElsc4GUsGsUWKqgteQSXZorpf6HdSJsQMZBDd7xG8zDDJ28hGjJSgWBndRGSzQEYU09Xbtzk-8khPuw\" } ] }", + 200); + + private static final MockResponse NOT_FOUND_RESPONSE = response( + "{ \"message\" : \"This mock authorization server responds to just one request: GET /.well-known/jwks.json.\" }", + 404); + + /** + * Name of the random {@link PropertySource}. + */ + public static final String MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME = "mockwebserver"; + + private static final String NAME = "mockwebserver.url"; + + private static final Log logger = LogFactory.getLog(MockWebServerPropertySource.class); + + private boolean started; + + private int port; + + public MockWebServerPropertySource() { + super(MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME, new AtomicReference<>(new MockWebServer())); + logger.info("Initializing MockWebServerPropertySource"); + Core.getGlobalContext().register(this); + } + + @Override + public Object getProperty(String name) { + if (!name.equals(NAME)) { + return null; + } + logger.trace("Looking up the url for '%s'".formatted(name)); + try { + String url = getUrl(); + logger.trace("Property value: '%s' = '%s'".formatted(name, url)); + return url; + } + catch (RuntimeException ex) { + logger.error("Failed to get property value for '%s'".formatted(name), ex); + throw ex; + } + } + + @Override + public void destroy() throws Exception { + getSource().get().shutdown(); + this.started = false; + } + + @Override + public void beforeCheckpoint(Context context) throws Exception { + destroy(); + getSource().set(new MockWebServer()); + } + + @Override + public void afterRestore(Context context) throws Exception { + getUrl(); + } + + /** + * Gets the URL (i.e. "http://localhost:123456") + * @return the url with the dynamic port + */ + private String getUrl() { + MockWebServer mockWebServer = getSource().get(); + if (!this.started) { + initializeMockWebServer(mockWebServer); + } + String url = mockWebServer.url("").url().toExternalForm(); + return url.substring(0, url.length() - 1); + } + + private void initializeMockWebServer(MockWebServer mockWebServer) { + Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + if ("/.well-known/jwks.json".equals(request.getPath())) { + return JWKS_RESPONSE; + } + + return NOT_FOUND_RESPONSE; + } + }; + + mockWebServer.setDispatcher(dispatcher); + try { + mockWebServer.start(this.port); + this.started = true; + this.port = mockWebServer.getPort(); + } + catch (IOException ex) { + throw new RuntimeException("Could not start " + mockWebServer, ex); + } + } + + private static MockResponse response(String body, int status) { + return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setResponseCode(status) + .setBody(body); + } + +} diff --git a/security/security-oauth2-resource-server/src/main/resources/META-INF/spring.factories b/security/security-oauth2-resource-server/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..1473cd9 --- /dev/null +++ b/security/security-oauth2-resource-server/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=com.example.security.oauth2resourceserver.env.MockWebServerEnvironmentPostProcessor diff --git a/security/security-oauth2-resource-server/src/main/resources/application.yml b/security/security-oauth2-resource-server/src/main/resources/application.yml new file mode 100644 index 0000000..e15871d --- /dev/null +++ b/security/security-oauth2-resource-server/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json + diff --git a/security/security-webflux/README.adoc b/security/security-webflux/README.adoc new file mode 100644 index 0000000..ce61827 --- /dev/null +++ b/security/security-webflux/README.adoc @@ -0,0 +1 @@ +Tests the `SecurityFilterChain` from Spring Security together with Spring WebFlux. diff --git a/security/security-webflux/build.gradle b/security/security-webflux/build.gradle new file mode 100644 index 0000000..092b56c --- /dev/null +++ b/security/security-webflux/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.crac:crac:$cracVersion") + implementation(project(":cr-listener")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + appTestImplementation(project(":cr-smoke-test-support")) +} + +crSmokeTest { + webApplication = true +} diff --git a/security/security-webflux/src/appTest/java/com/example/security/webflux/SecurityWebFluxApplicationCheckpointTests.java b/security/security-webflux/src/appTest/java/com/example/security/webflux/SecurityWebFluxApplicationCheckpointTests.java new file mode 100644 index 0000000..a14680c --- /dev/null +++ b/security/security-webflux/src/appTest/java/com/example/security/webflux/SecurityWebFluxApplicationCheckpointTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.webflux; + +import org.junit.jupiter.api.Test; + +import org.springframework.cr.smoketest.support.junit.ApplicationTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +class SecurityWebFluxApplicationCheckpointTests { + + @Test + void anonymousShouldBeAccessibleWithoutCredentials(WebTestClient client) { + client.get() + .uri("/rest/anonymous") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("anonymous")); + } + + @Test + void authorizedShouldBeProtectedWithoutCredentials(WebTestClient client) { + client.get().uri("/rest/authorized").exchange().expectStatus().isUnauthorized(); + } + + @Test + void authorizedShouldBeAccessibleWithCredentials(WebTestClient client) { + client.get() + .uri("/rest/authorized") + .headers((header) -> header.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith( + (result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("authorized: user")); + } + + @Test + void authorizedShouldBeProtectedWithWrongCredentials(WebTestClient client) { + client.get() + .uri("/rest/authorized") + .headers((header) -> header.setBasicAuth("wrong-user", "wrong-password")) + .exchange() + .expectStatus() + .isUnauthorized(); + } + + @Test + void adminShouldBeProtectedWithoutCredentials(WebTestClient client) { + client.get().uri("/rest/admin").exchange().expectStatus().isUnauthorized(); + } + + @Test + void adminShouldBeAccessibleWithCredentials(WebTestClient client) { + client.get() + .uri("/rest/admin") + .headers((header) -> header.setBasicAuth("admin", "password")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("admin: admin")); + } + + @Test + void adminShouldBeProtectedWithWrongCredentials(WebTestClient client) { + client.get() + .uri("/rest/admin") + .headers((header) -> header.setBasicAuth("wrong-admin", "wrong-password")) + .exchange() + .expectStatus() + .isUnauthorized(); + } + + @Test + void adminShouldBeProtectedWithWrongRole(WebTestClient client) { + client.get() + .uri("/rest/admin") + .headers((header) -> header.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isForbidden(); + } + + @Test + void staticResourcesShouldBeProtected(WebTestClient client) { + client.get().uri("/foo.html").exchange().expectStatus().isUnauthorized(); + client.get().uri("/bar.html").exchange().expectStatus().isUnauthorized(); + } + +} diff --git a/security/security-webflux/src/main/java/com/example/security/webflux/SecurityWebFluxApplication.java b/security/security-webflux/src/main/java/com/example/security/webflux/SecurityWebFluxApplication.java new file mode 100644 index 0000000..164bc46 --- /dev/null +++ b/security/security-webflux/src/main/java/com/example/security/webflux/SecurityWebFluxApplication.java @@ -0,0 +1,13 @@ +package com.example.security.webflux; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SecurityWebFluxApplication { + + public static void main(String[] args) { + SpringApplication.run(SecurityWebFluxApplication.class, args); + } + +} diff --git a/security/security-webflux/src/main/java/com/example/security/webflux/TestRestController.java b/security/security-webflux/src/main/java/com/example/security/webflux/TestRestController.java new file mode 100644 index 0000000..27ed2bf --- /dev/null +++ b/security/security-webflux/src/main/java/com/example/security/webflux/TestRestController.java @@ -0,0 +1,31 @@ +package com.example.security.webflux; + +import java.security.Principal; + +import reactor.core.publisher.Mono; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/rest", produces = MediaType.TEXT_PLAIN_VALUE) +public class TestRestController { + + @GetMapping("/anonymous") + public Mono anonymous() { + return Mono.just("anonymous"); + } + + @GetMapping("/authorized") + public Mono authorized(Principal principal) { + return Mono.just("authorized: " + principal.getName()); + } + + @GetMapping("/admin") + public Mono admin(Principal principal) { + return Mono.just("admin: " + principal.getName()); + } + +} diff --git a/security/security-webflux/src/main/java/com/example/security/webflux/WebSecurityConfig.java b/security/security-webflux/src/main/java/com/example/security/webflux/WebSecurityConfig.java new file mode 100644 index 0000000..7d64b7e --- /dev/null +++ b/security/security-webflux/src/main/java/com/example/security/webflux/WebSecurityConfig.java @@ -0,0 +1,47 @@ +package com.example.security.webflux; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.server.SecurityWebFilterChain; + +@Configuration +@EnableWebFluxSecurity +public class WebSecurityConfig { + + @Bean + public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { + return http + .authorizeExchange((exchanges) -> exchanges.pathMatchers("/rest/anonymous") + .permitAll() + .pathMatchers("/rest/admin") + .hasRole("ADMIN") + .anyExchange() + .authenticated()) + .httpBasic(Customizer.withDefaults()) + .build(); + } + + @Bean + public ReactiveUserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + UserDetails admin = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN") + .build(); + + return new MapReactiveUserDetailsService(user, admin); + } + +} diff --git a/security/security-webflux/src/main/resources/public/bar.html b/security/security-webflux/src/main/resources/public/bar.html new file mode 100644 index 0000000..d9d3a9a --- /dev/null +++ b/security/security-webflux/src/main/resources/public/bar.html @@ -0,0 +1 @@ +Bar \ No newline at end of file diff --git a/security/security-webflux/src/main/resources/static/foo.html b/security/security-webflux/src/main/resources/static/foo.html new file mode 100644 index 0000000..9f26b63 --- /dev/null +++ b/security/security-webflux/src/main/resources/static/foo.html @@ -0,0 +1 @@ +Foo \ No newline at end of file diff --git a/security/security-webmvc/README.adoc b/security/security-webmvc/README.adoc new file mode 100644 index 0000000..aad1cbd --- /dev/null +++ b/security/security-webmvc/README.adoc @@ -0,0 +1 @@ +Tests the `SecurityFilterChain` from Spring Security together with Spring WebMVC. diff --git a/security/security-webmvc/build.gradle b/security/security-webmvc/build.gradle new file mode 100644 index 0000000..4d3e750 --- /dev/null +++ b/security/security-webmvc/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.crac:crac:$cracVersion") + implementation(project(":cr-listener")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + appTestImplementation(project(":cr-smoke-test-support")) +} + +crSmokeTest { + webApplication = true +} diff --git a/security/security-webmvc/src/appTest/java/com/example/security/webmvc/SecurityWebMvcApplicationCheckpointTests.java b/security/security-webmvc/src/appTest/java/com/example/security/webmvc/SecurityWebMvcApplicationCheckpointTests.java new file mode 100644 index 0000000..e594726 --- /dev/null +++ b/security/security-webmvc/src/appTest/java/com/example/security/webmvc/SecurityWebMvcApplicationCheckpointTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 com.example.security.webmvc; + +import org.junit.jupiter.api.Test; + +import org.springframework.cr.smoketest.support.junit.ApplicationTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +class SecurityWebMvcApplicationCheckpointTests { + + @Test + void anonymousShouldBeAccessibleWithoutCredentials(WebTestClient client) { + client.get() + .uri("/rest/anonymous") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("anonymous")); + } + + @Test + void authorizedShouldBeProtectedWithoutCredentials(WebTestClient client) { + client.get().uri("/rest/authorized").exchange().expectStatus().isUnauthorized(); + } + + @Test + void authorizedShouldBeAccessibleWithCredentials(WebTestClient client) { + client.get() + .uri("/rest/authorized") + .headers((header) -> header.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith( + (result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("authorized: user")); + } + + @Test + void authorizedShouldBeProtectedWithWrongCredentials(WebTestClient client) { + client.get() + .uri("/rest/authorized") + .headers((header) -> header.setBasicAuth("wrong-user", "wrong-password")) + .exchange() + .expectStatus() + .isUnauthorized(); + } + + @Test + void adminShouldBeProtectedWithoutCredentials(WebTestClient client) { + client.get().uri("/rest/admin").exchange().expectStatus().isUnauthorized(); + } + + @Test + void adminShouldBeAccessibleWithCredentials(WebTestClient client) { + client.get() + .uri("/rest/admin") + .headers((header) -> header.setBasicAuth("admin", "password")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("admin: admin")); + } + + @Test + void adminShouldBeProtectedWithWrongCredentials(WebTestClient client) { + client.get() + .uri("/rest/admin") + .headers((header) -> header.setBasicAuth("wrong-admin", "wrong-password")) + .exchange() + .expectStatus() + .isUnauthorized(); + } + + @Test + void adminShouldBeProtectedWithWrongRole(WebTestClient client) { + client.get() + .uri("/rest/admin") + .headers((header) -> header.setBasicAuth("user", "password")) + .exchange() + .expectStatus() + .isForbidden(); + } + + @Test + void staticResourcesShouldBeProtected(WebTestClient client) { + client.get().uri("/foo.html").exchange().expectStatus().isUnauthorized(); + client.get().uri("/bar.html").exchange().expectStatus().isUnauthorized(); + } + +} diff --git a/security/security-webmvc/src/main/java/com/example/security/webmvc/SecurityWebMvcApplication.java b/security/security-webmvc/src/main/java/com/example/security/webmvc/SecurityWebMvcApplication.java new file mode 100644 index 0000000..6879d58 --- /dev/null +++ b/security/security-webmvc/src/main/java/com/example/security/webmvc/SecurityWebMvcApplication.java @@ -0,0 +1,13 @@ +package com.example.security.webmvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SecurityWebMvcApplication { + + public static void main(String[] args) { + SpringApplication.run(SecurityWebMvcApplication.class, args); + } + +} diff --git a/security/security-webmvc/src/main/java/com/example/security/webmvc/TestRestController.java b/security/security-webmvc/src/main/java/com/example/security/webmvc/TestRestController.java new file mode 100644 index 0000000..64b4c83 --- /dev/null +++ b/security/security-webmvc/src/main/java/com/example/security/webmvc/TestRestController.java @@ -0,0 +1,29 @@ +package com.example.security.webmvc; + +import java.security.Principal; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/rest", produces = MediaType.TEXT_PLAIN_VALUE) +public class TestRestController { + + @GetMapping("/anonymous") + public String anonymous() { + return "anonymous"; + } + + @GetMapping("/authorized") + public String authorized(Principal principal) { + return "authorized: " + principal.getName(); + } + + @GetMapping("/admin") + public String admin(Principal principal) { + return "admin: " + principal.getName(); + } + +} diff --git a/security/security-webmvc/src/main/java/com/example/security/webmvc/WebSecurityConfig.java b/security/security-webmvc/src/main/java/com/example/security/webmvc/WebSecurityConfig.java new file mode 100644 index 0000000..b8ff49f --- /dev/null +++ b/security/security-webmvc/src/main/java/com/example/security/webmvc/WebSecurityConfig.java @@ -0,0 +1,47 @@ +package com.example.security.webmvc; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(authorize -> authorize.forServletPattern("/", + (r) -> r.requestMatchers("/rest/anonymous") + .permitAll() + .requestMatchers("/rest/admin") + .hasRole("ADMIN") + .anyRequest() + .authenticated())) + .httpBasic(Customizer.withDefaults()) + .build(); + } + + @Bean + public UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + UserDetails admin = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN") + .build(); + + return new InMemoryUserDetailsManager(user, admin); + } + +} diff --git a/security/security-webmvc/src/main/resources/public/bar.html b/security/security-webmvc/src/main/resources/public/bar.html new file mode 100644 index 0000000..d9d3a9a --- /dev/null +++ b/security/security-webmvc/src/main/resources/public/bar.html @@ -0,0 +1 @@ +Bar \ No newline at end of file diff --git a/security/security-webmvc/src/main/resources/static/foo.html b/security/security-webmvc/src/main/resources/static/foo.html new file mode 100644 index 0000000..9f26b63 --- /dev/null +++ b/security/security-webmvc/src/main/resources/static/foo.html @@ -0,0 +1 @@ +Foo \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 804005b..6187e6a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,8 +27,8 @@ rootProject.name="spring-checkpoint-restore-smoke-tests" include "cr-smoke-test-support" include "cr-listener" -["boot", "cloud", "data", "framework", "integration"].each { group -> +["boot", "cloud", "data", "framework", "integration", "security"].each { group -> file(group).eachDirMatch(~/[a-z].*/) { smokeTest -> include "$group:${smokeTest.name}" } -} \ No newline at end of file +}