Add Security Tests

- Embedded LDAP
- Authorization Server
- Resource Server
- Security with MVC
- Security with WebFlux
This commit is contained in:
Josh Cummings
2023-11-06 08:49:15 -07:00
committed by Sébastien Deleuze
parent 35a61264d5
commit 04e5f249a4
42 changed files with 1506 additions and 2 deletions

View File

@@ -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]
|
|===

View File

@@ -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

View File

@@ -0,0 +1 @@
Tests Spring Security Embedded LDAP

View File

@@ -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
}

View File

@@ -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!"));
}
}

View File

@@ -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() + "!";
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -0,0 +1 @@
Tests Spring Security OAuth2 Authorization Server

View File

@@ -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
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<HttpHeaders> badCredentials() {
return (headers) -> headers.setBasicAuth("bad", "password");
}
private String getAccessToken(WebTestClient client) throws IOException {
// @formatter:off
MultiValueMap<String, String> 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<String, Object> tokenResponse = this.objectMapper.readValue(responseBody, new TypeReference<>() {
});
return tokenResponse.get("access_token").toString();
}
}

View File

@@ -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
}
}

View File

@@ -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<SecurityContext> 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;
}
}

View File

@@ -0,0 +1 @@
Tests Spring Security OAuth2 Resource Server

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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<AtomicReference<MockWebServer>>
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<? extends Resource> context) throws Exception {
destroy();
getSource().set(new MockWebServer());
}
@Override
public void afterRestore(Context<? extends Resource> 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);
}
}

View File

@@ -0,0 +1 @@
org.springframework.boot.env.EnvironmentPostProcessor=com.example.security.oauth2resourceserver.env.MockWebServerEnvironmentPostProcessor

View File

@@ -0,0 +1,7 @@
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json

View File

@@ -0,0 +1 @@
Tests the `SecurityFilterChain` from Spring Security together with Spring WebFlux.

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> anonymous() {
return Mono.just("anonymous");
}
@GetMapping("/authorized")
public Mono<String> authorized(Principal principal) {
return Mono.just("authorized: " + principal.getName());
}
@GetMapping("/admin")
public Mono<String> admin(Principal principal) {
return Mono.just("admin: " + principal.getName());
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
Bar

View File

@@ -0,0 +1 @@
Foo

View File

@@ -0,0 +1 @@
Tests the `SecurityFilterChain` from Spring Security together with Spring WebMVC.

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
Bar

View File

@@ -0,0 +1 @@
Foo

View File

@@ -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}"
}
}
}