diff --git a/README.MD b/README.MD
index e40d1c39c..52b5e7430 100644
--- a/README.MD
+++ b/README.MD
@@ -1,26 +1,19 @@
# Ease OAuth2 / OpenID Configuration & Tests in Spring Boot 3
-## What's new in the `8.x` branch
+## What's new in `8.0.0`
-`8.0.0-RC1`, is out. It is designed to work with Spring Boot `3.4.0-RC1`, Security `6.4.0-RC1`, and Cloud `2024.0.0-M2`.
+`8.0.0`, is out. It is designed to work with Spring Boot `3.4.0` (Security `6.4.0` and Cloud `2024.0.0`).
-- [`spring-addons-starter-rest`](https://github.com/ch4mpy/spring-addons/tree/master/spring-addons-starter-rest) is gaining in maturity. It can now expose as `@Bean` some `RestClient` and `WebClient` instances (or builders) with the following configured using application properties:
+- [`spring-addons-starter-rest`](https://github.com/ch4mpy/spring-addons/tree/master/spring-addons-starter-rest) now expose as `@Bean` some `RestClient` and `WebClient` instances (or builders) with the following configured using application properties:
- Base URI
- `Basic` or `Bearer` authorization. For the second, with a choice of using an OAuth2 client registration or forwarding the access token in the security context.
- Connection & read timeouts
- HTTP or SOCKS proxy, with consideration of the standard `HTTP_PROXY` and `NO_PROXY` environment variables (finer-grained configuration can be applied with custom properties)
- [`spring-addons-starter-oidc`](https://github.com/ch4mpy/spring-addons/tree/master/spring-addons-starter-oidc) auto-configuration for `oauth2Login` is improved with:
- - Working [Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) (at last :/).
- - Configurable status for unauthorized requests. The default is still `302 Found` (redirect to login), but it's a snap to change it to `401 Unauthorized` (BFF for single page or mobile applications, stateful REST APIs, ...).
-- `OAuthentication` now extends `AbstractOAuth2TokenAuthenticationToken`. This makes integrating with the rest of the Spring Security ecosystem easier but requires its `principal` to implement `OAuth2Token`. Migration guide:
- - if using `OpenidClaimSet` directly, wrap it in an `OpenidToken`; if extending it, extend `OpenidToken` instead.
- - move the token string argument from the `OAuthentication` constructor to the `principal` one (probably an `OpenidToken`)
-```java
-new OAuthentication<>(new OpenidClaimSet(claims), authorities, tokenString);
-```
-becomes
-```java
-new OAuthentication<>(new OpenidToken(new OpenidClaimSet(claims), tokenString), authorities);
+ - Working [Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html), even with cookie-based CSRF protection and `logout+jwt` tokens (which makes it finally usable with [OAuth2 BFF](https://www.baeldung.com/spring-cloud-gateway-bff-oauth2) & [Keycloak](https://www.baeldung.com/spring-boot-keycloak)).
+ - Configurable status for unauthorized requests. The default is still `302 Found` (redirect to login), but it's a snap to change it to `401 Unauthorized` (like REST APIs should return, even stateful ones).
+- `OAuthentication` now extends `AbstractOAuth2TokenAuthenticationToken` for a better integration with the rest of the Spring Security ecosystem. See the [migration guide](https://github.com/ch4mpy/spring-addons/tree/master/migrate-to-8.0.0.md) for details.
+
```
## [`spring-addons-starter-oidc`](https://github.com/ch4mpy/spring-addons/tree/master/spring-addons-starter-oidc)
@@ -46,31 +39,53 @@ com:
springaddons:
rest:
client:
+ # Exposes a RestClient bean named machinClient (or WebClient in a WebFlux app)
machin-client:
base-url: ${machin-api}
authorization:
oauth2:
+ # Authorize outgoing requests with the Bearer token in the security context (possible only in a resource server app)
forward-bearer: true
+ # Exposes a RestClient.Builder bean named biduleClientBuilder (mind the "expose-builder: true")
bidule-client:
base-url: ${bidule-api}
+ # Expose the builder instead of an already built client (to fine tune its conf)
expose-builder: true
authorization:
oauth2:
+ # Authorize outgoing requests with the Bearer token obtained using an OAuth2 client registration
oauth2-registration-id: bidule-registration
```
-This exposes two beans that we can auto-wire in `@Component` or `@Configuration`, for instance to generate `@HttpExchange` implementations as follows (mind the `expose-builder: true` for `bidule-client`):
+This exposes pre-configured beans that we can auto-wire in any kind of `@Component`, like `@Controller` or `@Service`, or use in `@Configuration`. For instance:
```java
@Configuration
public class RestConfiguration {
+ /**
+ * @param machinClient pre-configured by spring-addons-starter-rest using application properties
+ * @return a generated implementation of the {@link MachinApi} {@link HttpExchange @HttpExchange}, exposed as a bean named "machinApi".
+ */
+ @Bean
+ MachinApi machinApi(RestClient machinClient) throws Exception {
+ return new RestClientHttpExchangeProxyFactoryBean<>(MachinApi.class, machinClient).getObject();
+ }
+ /**
+ * @param biduleClientBuilder pre-configured using application properties
+ * @return a {@link RestClient} bean named "biduleClient"
+ */
@Bean
- BiduleApi biduleApi(RestClient.Builder biduleClientBuilder) throws Exception {
- return new RestClientHttpExchangeProxyFactoryBean<>(BiduleApi.class, biduleClientBuilder.build()).getObject();
+ RestClient biduleClient(RestClient.Builder biduleClientBuilder) throws Exception {
+ // Fine-tune biduleClientBuilder configuration
+ return biduleClientBuilder.build();
}
+ /**
+ * @param biduleClient the bean exposed just above
+ * @return a generated implementation of the {@link BiduleApi} {@link HttpExchange @HttpExchange}, exposed as a bean named "biduleApi".
+ */
@Bean
- MachinApi machinApi(RestClient machinClient) throws Exception {
- return new RestClientHttpExchangeProxyFactoryBean<>(MachinApi.class, machinClient).getObject();
+ BiduleApi biduleApi(RestClient biduleClient) throws Exception {
+ return new RestClientHttpExchangeProxyFactoryBean<>(BiduleApi.class, biduleClient).getObject();
}
}
```
diff --git a/migrate-to-8.0.0.md b/migrate-to-8.0.0.md
new file mode 100644
index 000000000..2ac8141d6
--- /dev/null
+++ b/migrate-to-8.0.0.md
@@ -0,0 +1,32 @@
+# Migrating from `7.x` to `8.x`
+
+## `spring-addons-starter-oidc`
+
+The only breaking changes are around `OAuthentication` which now extends `AbstractOAuth2TokenAuthenticationToken` for a better integration with the rest of the Spring Security ecosystem.
+
+If using `OpenidClaimSet` directly, wrap it in an `OpenidToken`; if extending it, extend `OpenidToken` instead.
+
+Move the token string argument from the `OAuthentication` constructor to the `principal` one (probably an `OpenidToken`).
+
+```java
+new OAuthentication<>(new OpenidClaimSet(claims), authorities, tokenString);
+```
+becomes
+```java
+new OAuthentication<>(new OpenidToken(new OpenidClaimSet(claims), tokenString), authorities);
+```
+
+## `spring-addons-starter-rest`
+
+`SpringAddonsRestClientSupport`, `SpringAddonsWebClientSupport`, and `ReactiveSpringAddonsWebClientSupport` are replaced by `ProxyFactoryBean`s:
+- `RestClient` and `WebClient` bean definitions (or the definition of their builders) are registered as bart of the bean registry post processing => remove any explicit bean definition in application conf, the Boot starter does it already.
+- change `@HttpExchange` service proxy bean defintions to use `RestClientHttpExchangeProxyFactoryBean` or `WebClientHttpExchangeProxyFactoryBean`
+
+Proxy properties are now configurable for each client => in YAML, move it down one level (copy it to each client needing proxy configuration).
+
+There are more configuration options available:
+- a flag to expose the client builder inttead of an already built client
+- force the bean name (by default, it's the camelCase transformation of the kebab-case client ID in properties, with `Builder` suffix when `expose-builder` is `true`)
+- set connect and read timeouts
+- expose a `WebClient` instead of the default `RestClient` in a servlet application
+- set chunk-size (only applied to `RestClient`)
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 040cc0df2..094dde74e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,12 +1,17 @@
4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.0
+
com.c4-soft.springaddons
spring-addons
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
pom
spring-addons
- Set of tools I find useful to work with Spring (mostly spring-security for OpenID)
+ Make Spring developpers' life easier when OAuth2 / OpenID is involved
https://github.com/ch4mpy/spring-addons/
@@ -54,15 +59,17 @@
3.13.0
3.1.2
+ 3.2.5
3.4.1
3.6.3
3.0.1
3.5.3
3.3.1
+ 1.7.0
3.11.0.3922
- 3.3.4
+ ${project.parent.version}
0.2.0
1.6.0.Beta2
@@ -70,9 +77,9 @@
${env.HOSTNAME}
https
- 2.5.0
+ 2.5.0
1.4
-
+
paketobuildpacks/builder:tiny
${project.basedir}/bindings/ca-certificates
@@ -82,14 +89,6 @@
-
- org.springframework.boot
- spring-boot-dependencies
- ${spring-boot.version}
- pom
- import
-
-
com.c4-soft.springaddons
spring-addons-oauth2
@@ -111,13 +110,13 @@
spring-addons-starter-oidc-test
${project.version}
-
+
com.c4-soft.springaddons
spring-addons-starter-rest
${project.version}
-
+
org.springdoc
springdoc-openapi-starter-webflux-api
@@ -142,7 +141,7 @@
-
-
+
repository.spring.release
Spring GA Repository
@@ -180,7 +179,7 @@
-
-
+
repository.spring.release
Spring GA Repository
@@ -221,15 +220,41 @@
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+ ${repackage.classifier}
+
+ ${image.builder}
+
+
+ ${ca-certificates.binding}:/platform/bindings/ca-certificates:ro
+
+
+ ${java.version}
+
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
org.sonatype.plugins
nexus-staging-maven-plugin
- 1.6.13
+ ${nexus-staging-maven-plugin.version}
org.apache.maven.plugins
maven-gpg-plugin
- 3.1.0
+ ${maven-gpg-plugin.version}
org.apache.maven.plugins
@@ -296,7 +321,8 @@
-
+
org.apache.maven.plugins
maven-javadoc-plugin
@@ -353,7 +379,7 @@
spring-addons-starter-oidc-test
spring-addons-starter-rest
spring-addons-starter-openapi
- starters
+ spring-addons-starter-recaptcha
@@ -370,7 +396,8 @@
gpg
-
+
--pinentry-mode
loopback
@@ -394,7 +421,7 @@
spring-addons-starter-oidc-test
spring-addons-starter-rest
spring-addons-starter-openapi
- starters
+ spring-addons-starter-recaptcha
samples
diff --git a/release-notes.md b/release-notes.md
index 819d7b001..366708c68 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -5,7 +5,7 @@ For Spring Boot 3.4.x.
`spring-addons-starter-rest` provides auto-configuration for `RestClient`, `WebClient` and tooling for `@HttpExchange` proxy generation.
-### `8.0.0-RC1`
+### `8.0.0`
- `spring-addons-starter-oidc`:
- **[Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html)** support. Enabled only if an `OidcBackChannel(Server)LogoutHandler` bean is present. A default `OidcBackChannel(Server)LogoutHandler` bean is provided if `com.c4-soft.springaddons.oidc.client.back-channel-logout.enabled` property is `true` (`false` by default).
- `authenticationEntryPoint` is now configurable with spring-addons for OAuth2 clients with `oauth2Login` (instead of `oauth2ResourceServer`). The default bean returns `302 redirect to login` unless another status is set with other OAuth2 responses statuses overrides in properties. Resource servers authentication entrypoint returns `401`.
@@ -16,7 +16,7 @@ For Spring Boot 3.4.x.
- HTTP proxy. Supports `HTTP_PROXY` & `NO_PROXY` environment variables, but finer grained custom properties can be used.
- connect and read timeouts.
- force usage of `WebClient` in a servlet app (`RestClient` is the default for servlets).
-- Boot `3.4.0-RC1` and Security `6.4.0-RC1` as transitive dependencies (adapts to the new Back-Channel Logout configuration).
+- Boot `3.4.0` (with Security `6.4.0` as transitive dependencies).
## `7.x` Branch
diff --git a/samples/pom.xml b/samples/pom.xml
index 93eb8b568..59d692417 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -4,7 +4,7 @@
com.c4-soft.springaddons
spring-addons
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
com.c4-soft.springaddons.samples
@@ -13,7 +13,7 @@
17
- 2023.0.2
+ 2024.0.0-M2
2.2.19
@@ -40,13 +40,6 @@
-
- org.springframework.boot
- spring-boot-dependencies
- ${spring-boot.version}
- pom
- import
-
org.springframework.cloud
spring-cloud-dependencies
@@ -117,32 +110,6 @@
-
- org.springframework.boot
- spring-boot-maven-plugin
- ${spring-boot.version}
-
- ${repackage.classifier}
-
- ${image.builder}
-
-
- ${ca-certificates.binding}:/platform/bindings/ca-certificates:ro
-
-
- ${java.version}
-
-
-
-
- org.projectlombok
- lombok
-
-
-
-
-
-
diff --git a/samples/release.properties b/samples/release.properties
deleted file mode 100644
index d7087299f..000000000
--- a/samples/release.properties
+++ /dev/null
@@ -1,35 +0,0 @@
-#release configuration
-#Thu Apr 25 14:23:36 TAHT 2024
-projectVersionPolicyId=default
-project.scm.com.c4-soft.springaddons.samples.tutorials\:resource-server_with_oauthentication.empty=true
-scm.branchCommitComment=@{prefix} prepare branch @{releaseLabel}
-pinExternals=false
-project.scm.com.c4-soft.springaddons.samples.tutorials\:resource-server_multitenant_dynamic.empty=true
-project.scm.com.c4-soft.springaddons.samples.tutorials\:servlet-resource-server.empty=true
-project.scm.com.c4-soft.springaddons.samples.tutorials\:resource-server_with_ui.empty=true
-projectVersionPolicyConfig=${projectVersionPolicyConfig}\n
-pushChanges=true
-project.scm.com.c4-soft.springaddons.samples\:webmvc-jwt-default-jpa-authorities.empty=true
-project.scm.com.c4-soft.springaddons.samples.tutorials\:servlet-client.empty=true
-scm.rollbackCommitComment=@{prefix} rollback the release of @{releaseLabel}
-remoteTagging=true
-project.scm.com.c4-soft.springaddons.samples\:enum-bug-reproducer-reactive.empty=true
-scm.commentPrefix=[maven-release-plugin]
-releaseStrategyId=default
-project.scm.com.c4-soft.springaddons.samples.tutorials\:resource-server_with_introspection.empty=true
-project.scm.com.c4-soft.springaddons.samples.tutorials\:resource-server_with_specialized_oauthentication.empty=true
-project.scm.com.c4-soft.springaddons.samples.tutorials\:reactive-resource-server.empty=true
-project.scm.com.c4-soft.springaddons.samples\:webmvc-jwt-default.empty=true
-completedPhase=scm-check-modifications
-scm.url=scm\:git\:git@github.com\:ch4mpy/spring-addons.git/spring-addons-samples/webmvc-jwt-default
-project.scm.com.c4-soft.springaddons.samples.tutorials\:resource-server_with_additional-header.empty=true
-project.scm.com.c4-soft.springaddons.samples\:webmvc-jwt-oauthentication.empty=true
-scm.developmentCommitComment=@{prefix} prepare for next development iteration
-scm.tagNameFormat=@{project.artifactId}-@{project.version}
-project.scm.com.c4-soft.springaddons.samples.tutorials\:reactive-client.empty=true
-project.scm.com.c4-soft.springaddons.samples\:enum-bug-reproducer-servlet.empty=true
-project.scm.com.c4-soft.springaddons.samples.tutorials\:tutorials.empty=true
-exec.snapshotReleasePluginAllowed=false
-preparationGoals=clean verify
-scm.releaseCommitComment=@{prefix} prepare release @{releaseLabel}
-exec.pomFileName=webmvc-jwt-default\\pom.xml
diff --git a/samples/tutorials/pom.xml b/samples/tutorials/pom.xml
index b5c979424..445b27c9a 100644
--- a/samples/tutorials/pom.xml
+++ b/samples/tutorials/pom.xml
@@ -4,7 +4,7 @@
com.c4-soft.springaddons.samples
spring-addons-samples
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
com.c4-soft.springaddons.samples.tutorials
diff --git a/samples/tutorials/reactive-client/pom.xml b/samples/tutorials/reactive-client/pom.xml
index d9c03cf90..363c3409c 100644
--- a/samples/tutorials/reactive-client/pom.xml
+++ b/samples/tutorials/reactive-client/pom.xml
@@ -4,7 +4,7 @@
com.c4-soft.springaddons.samples.tutorials
tutorials
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
reactive-client
@@ -73,14 +73,6 @@
-
- org.graalvm.buildtools
- native-maven-plugin
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
diff --git a/samples/tutorials/reactive-resource-server/pom.xml b/samples/tutorials/reactive-resource-server/pom.xml
index 342509537..66e7462ff 100644
--- a/samples/tutorials/reactive-resource-server/pom.xml
+++ b/samples/tutorials/reactive-resource-server/pom.xml
@@ -4,7 +4,7 @@
com.c4-soft.springaddons.samples.tutorials
tutorials
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
reactive-resource-server
@@ -58,14 +58,6 @@
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
- org.graalvm.buildtools
- native-maven-plugin
-
diff --git a/samples/tutorials/resource-server_multitenant_dynamic/pom.xml b/samples/tutorials/resource-server_multitenant_dynamic/pom.xml
index c194947a8..878b4e45d 100644
--- a/samples/tutorials/resource-server_multitenant_dynamic/pom.xml
+++ b/samples/tutorials/resource-server_multitenant_dynamic/pom.xml
@@ -4,7 +4,7 @@
com.c4-soft.springaddons.samples.tutorials
tutorials
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
resource-server_multitenant_dynamic
@@ -61,14 +61,6 @@
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
- org.graalvm.buildtools
- native-maven-plugin
-
diff --git a/samples/tutorials/resource-server_with_additional-header/pom.xml b/samples/tutorials/resource-server_with_additional-header/pom.xml
index 299c6267f..ed3f4ea41 100644
--- a/samples/tutorials/resource-server_with_additional-header/pom.xml
+++ b/samples/tutorials/resource-server_with_additional-header/pom.xml
@@ -4,7 +4,7 @@
com.c4-soft.springaddons.samples.tutorials
tutorials
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
resource-server_with_additional-header
@@ -58,14 +58,6 @@
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
- org.graalvm.buildtools
- native-maven-plugin
-
diff --git a/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java b/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java
index c090b5092..71aa96314 100644
--- a/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java
+++ b/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java
@@ -3,21 +3,20 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
-
import com.c4soft.springaddons.tutorials.SecurityConfig.MyAuth;
@RestController
@PreAuthorize("isAuthenticated()")
public class GreetingController {
- @GetMapping("/greet")
- public MessageDto getGreeting(MyAuth auth) {
- return new MessageDto(
- "Hi %s! You are granted with: %s.".formatted(
- auth.getIdClaims().getEmail(), // From ID token in X-ID-Token header
- auth.getAuthorities())); // From access token in Authorization header
- }
+ @GetMapping("/greet")
+ public MessageDto getGreeting(MyAuth auth) {
+ // email From ID token in X-ID-Token header
+ // Authorities from access token in Authorization header
+ return new MessageDto("Hi %s! You are granted with: %s.".formatted(auth.getIdToken().getEmail(),
+ auth.getAuthorities()));
+ }
- static record MessageDto(String body) {
- }
+ static record MessageDto(String body) {
+ }
}
diff --git a/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java b/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java
index 1df0d37d8..bec4c9e70 100644
--- a/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java
+++ b/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java
@@ -4,7 +4,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
-
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
@@ -18,86 +17,77 @@
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-
import com.c4_soft.springaddons.security.oidc.OAuthentication;
import com.c4_soft.springaddons.security.oidc.OpenidClaimSet;
+import com.c4_soft.springaddons.security.oidc.OpenidToken;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.HttpServletRequestSupport;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.HttpServletRequestSupport.InvalidHeaderException;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor;
-
import lombok.Data;
import lombok.EqualsAndHashCode;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
- public static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
- private static final Map idTokenDecoders = new ConcurrentHashMap<>();
+ public static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
+ private static final Map idTokenDecoders = new ConcurrentHashMap<>();
- private JwtDecoder getJwtDecoder(Map accessClaims) {
- if (accessClaims == null) {
- return null;
- }
- final var iss = Optional.ofNullable(accessClaims.get(JwtClaimNames.ISS)).map(Object::toString).orElse(null);
- if (iss == null) {
- return null;
- }
- if (!idTokenDecoders.containsKey(iss)) {
- idTokenDecoders.put(iss, JwtDecoders.fromIssuerLocation(iss));
- }
- return idTokenDecoders.get(iss);
- }
+ private JwtDecoder getJwtDecoder(Map accessClaims) {
+ if (accessClaims == null) {
+ return null;
+ }
+ final var iss =
+ Optional.ofNullable(accessClaims.get(JwtClaimNames.ISS)).map(Object::toString).orElse(null);
+ if (iss == null) {
+ return null;
+ }
+ if (!idTokenDecoders.containsKey(iss)) {
+ idTokenDecoders.put(iss, JwtDecoders.fromIssuerLocation(iss));
+ }
+ return idTokenDecoders.get(iss);
+ }
- @Bean
- JwtAbstractAuthenticationTokenConverter
- authenticationConverter(Converter
* See {@link AutoConfigureAddonsWebmvcMinimalSecurity}
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
* @see AddonsWebmvcComponentTest
* @see AutoConfigureAddonsWebmvcResourceServerSecurity
*/
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxMinimalSecurity.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxMinimalSecurity.java
index 87c98a4f3..926d1a0b8 100644
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxMinimalSecurity.java
+++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxMinimalSecurity.java
@@ -25,7 +25,7 @@
*
* See {@link AddonsWebmvcComponentTest} See {@link AutoConfigureAddonsWebmvcResourceServerSecurity} See {@link AutoConfigureAddonsWebfluxClientSecurity}
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
* @see AddonsWebmvcComponentTest
* @see AutoConfigureAddonsWebfluxClientSecurity
* @see AutoConfigureAddonsWebmvcResourceServerSecurity
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxResourceServerSecurity.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxResourceServerSecurity.java
index 3c6e97ed9..6268bfd3a 100644
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxResourceServerSecurity.java
+++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxResourceServerSecurity.java
@@ -16,7 +16,7 @@
* repositories (web context is not desired in that case).
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/OidcIdAuthenticationTokenWebTestClientConfigurer.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/OidcIdAuthenticationTokenWebTestClientConfigurer.java
index 25b9a82e9..c41ae6bd7 100644
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/OidcIdAuthenticationTokenWebTestClientConfigurer.java
+++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/OidcIdAuthenticationTokenWebTestClientConfigurer.java
@@ -1,26 +1,27 @@
/*
* Copyright 2019 Jérôme Wacongne
*
- * 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
+ * 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.
+ * 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.c4_soft.springaddons.security.oauth2.test.webflux;
import com.c4_soft.springaddons.security.oauth2.test.OAuthenticationTestingBuilder;
import com.c4_soft.springaddons.security.oidc.OAuthentication;
-import com.c4_soft.springaddons.security.oidc.OpenidClaimSet;
+import com.c4_soft.springaddons.security.oidc.OpenidToken;
-public class OidcIdAuthenticationTokenWebTestClientConfigurer extends OAuthenticationTestingBuilder
- implements
- AuthenticationConfigurer> {
+public class OidcIdAuthenticationTokenWebTestClientConfigurer extends OAuthenticationTestingBuilder
+ implements AuthenticationConfigurer> {
- public static OidcIdAuthenticationTokenWebTestClientConfigurer oidcId() {
- return new OidcIdAuthenticationTokenWebTestClientConfigurer();
- }
-}
\ No newline at end of file
+ public static OidcIdAuthenticationTokenWebTestClientConfigurer oidcId() {
+ return new OidcIdAuthenticationTokenWebTestClientConfigurer();
+ }
+}
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AuthenticationRequestPostProcessor.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AuthenticationRequestPostProcessor.java
index 915488002..1dea7d4ba 100644
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AuthenticationRequestPostProcessor.java
+++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AuthenticationRequestPostProcessor.java
@@ -21,7 +21,7 @@
/**
* Redundant code for {@link Authentication} MockMvc request post-processors
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
* @param concrete {@link Authentication} type to build and configure in test security context
*/
public interface AuthenticationRequestPostProcessor extends RequestPostProcessor, AuthenticationBuilder {
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcClientSecurity.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcClientSecurity.java
index 657db481c..504182491 100644
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcClientSecurity.java
+++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcClientSecurity.java
@@ -16,7 +16,7 @@
* test controllers but not services or repositories (web context is not desired in that case).
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
* @see AddonsWebmvcComponentTest
* @see AutoConfigureAddonsWebfluxClientSecurity
*/
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcMinimalSecurity.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcMinimalSecurity.java
index b10f587bf..f7cd6dd12 100644
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcMinimalSecurity.java
+++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcMinimalSecurity.java
@@ -24,7 +24,7 @@
*
* See {@link AddonsWebmvcComponentTest} See {@link AutoConfigureAddonsWebmvcResourceServerSecurity} See {@link AutoConfigureAddonsWebfluxClientSecurity}
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
* @see AddonsWebmvcComponentTest
* @see AutoConfigureAddonsWebfluxClientSecurity
* @see AutoConfigureAddonsWebmvcResourceServerSecurity
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcResourceServerSecurity.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcResourceServerSecurity.java
index cbee34e50..88fd812c5 100644
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcResourceServerSecurity.java
+++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/AutoConfigureAddonsWebmvcResourceServerSecurity.java
@@ -16,7 +16,7 @@
* used to test controllers but not services or repositories (web context is not desired in that case).
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
* @see AddonsWebmvcComponentTest
* @see AutoConfigureAddonsWebfluxClientSecurity
*/
diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/OidcIdAuthenticationTokenRequestPostProcessor.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/OidcIdAuthenticationTokenRequestPostProcessor.java
deleted file mode 100644
index 09698db39..000000000
--- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webmvc/OidcIdAuthenticationTokenRequestPostProcessor.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2019 Jérôme Wacongne
- *
- * 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.c4_soft.springaddons.security.oauth2.test.webmvc;
-
-import com.c4_soft.springaddons.security.oauth2.test.OAuthenticationTestingBuilder;
-import com.c4_soft.springaddons.security.oidc.OAuthentication;
-import com.c4_soft.springaddons.security.oidc.OpenidClaimSet;
-
-public class OidcIdAuthenticationTokenRequestPostProcessor extends OAuthenticationTestingBuilder
- implements
- AuthenticationRequestPostProcessor> {
-
- public static OidcIdAuthenticationTokenRequestPostProcessor mockOidcId() {
- return new OidcIdAuthenticationTokenRequestPostProcessor();
- }
-}
\ No newline at end of file
diff --git a/spring-addons-starter-oidc/README.MD b/spring-addons-starter-oidc/README.MD
index c89cb4e14..c5bba4ec5 100644
--- a/spring-addons-starter-oidc/README.MD
+++ b/spring-addons-starter-oidc/README.MD
@@ -4,7 +4,7 @@ This project is a Spring Boot starter to use in addition to `spring-boot-starter
```xml
- 8.0.0-RC1
+ 8.0.0
diff --git a/spring-addons-starter-oidc/pom.xml b/spring-addons-starter-oidc/pom.xml
index 1610c52bc..fb4c002d8 100644
--- a/spring-addons-starter-oidc/pom.xml
+++ b/spring-addons-starter-oidc/pom.xml
@@ -3,7 +3,7 @@
com.c4-soft.springaddons
spring-addons
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
spring-addons-starter-oidc
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java
index ed4c9ca54..08bcb929f 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java
@@ -5,312 +5,336 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
-
import lombok.Data;
/**
- * Auto-configuration for an OAuth2 client (secured with session, not access token) Security(Web)FilterChain with @Order(Ordered.LOWEST_PRECEDENCE - 1).
- * Typical use-cases are spring-cloud-gateway used as BFF and applications with Thymeleaf or another server-side rendering framework. Default configuration
- * includes: enabled sessions, CSRF protection, "oauth2Login", "logout". securityMatchers must be set for this filter-chain @Bean and its dependencies to be
- * defined. Properties defined here are a complement for spring.security.oauth2.client.* (which are required when enabling spring-addons client
- * filter-chain).
+ * Auto-configuration for an OAuth2 client (secured with session, not access token)
+ * Security(Web)FilterChain with @Order(Ordered.LOWEST_PRECEDENCE - 1). Typical use-cases are
+ * spring-cloud-gateway used as BFF and applications with Thymeleaf or another server-side rendering
+ * framework. Default configuration includes: enabled sessions, CSRF protection, "oauth2Login",
+ * "logout". securityMatchers must be set for this filter-chain @Bean and its dependencies to be
+ * defined. Properties defined here are a complement for spring.security.oauth2.client.*
+ * (which are required when enabling spring-addons client filter-chain).
*
* @author Jerome Wacongne ch4mp@c4-soft.com
*/
@Data
-@ConfigurationProperties
public class SpringAddonsOidcClientProperties {
- public static final String RESPONSE_STATUS_HEADER = "X-RESPONSE-STATUS";
-
- public static final String POST_AUTHENTICATION_SUCCESS_URI_HEADER = "X-POST-LOGIN-SUCCESS-URI";
- public static final String POST_AUTHENTICATION_SUCCESS_URI_PARAM = "post_login_success_uri";
- public static final String POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE = POST_AUTHENTICATION_SUCCESS_URI_PARAM;
-
- public static final String POST_AUTHENTICATION_FAILURE_URI_HEADER = "X-POST-LOGIN-FAILURE-URI";
- public static final String POST_AUTHENTICATION_FAILURE_URI_PARAM = "post_login_failure_uri";
- public static final String POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE = POST_AUTHENTICATION_FAILURE_URI_PARAM;
- public static final String POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE = "error";
-
- public static final String POST_LOGOUT_SUCCESS_URI_HEADER = "X-POST-LOGOUT-SUCCESS-URI";
- public static final String POST_LOGOUT_SUCCESS_URI_PARAM = "post_logout_success_uri";
-
- /**
- * Path matchers for the routes secured with the auto-configured client filter-chain. If left empty, OAuth2 client auto-configuration is disabled. It should
- * include "/login/**" and "/oauth2/**" for login process. Can be set to "/**" to intercept all requests (OAuth2 client only application, no REST API
- * secured with access tokens).
- */
- private List securityMatchers = List.of();
+ public static final String RESPONSE_STATUS_HEADER = "X-RESPONSE-STATUS";
+
+ public static final String POST_AUTHENTICATION_SUCCESS_URI_HEADER = "X-POST-LOGIN-SUCCESS-URI";
+ public static final String POST_AUTHENTICATION_SUCCESS_URI_PARAM = "post_login_success_uri";
+ public static final String POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE =
+ POST_AUTHENTICATION_SUCCESS_URI_PARAM;
+
+ public static final String POST_AUTHENTICATION_FAILURE_URI_HEADER = "X-POST-LOGIN-FAILURE-URI";
+ public static final String POST_AUTHENTICATION_FAILURE_URI_PARAM = "post_login_failure_uri";
+ public static final String POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE =
+ POST_AUTHENTICATION_FAILURE_URI_PARAM;
+ public static final String POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE = "error";
+
+ public static final String POST_LOGOUT_SUCCESS_URI_HEADER = "X-POST-LOGOUT-SUCCESS-URI";
+ public static final String POST_LOGOUT_SUCCESS_URI_PARAM = "post_logout_success_uri";
+
+ /**
+ * Path matchers for the routes secured with the auto-configured client filter-chain. If left
+ * empty, OAuth2 client auto-configuration is disabled. It should include "/login/**" and
+ * "/oauth2/**" for login process. Can be set to "/**" to intercept all requests (OAuth2 client
+ * only application, no REST API secured with access tokens).
+ */
+ private List securityMatchers = List.of();
+
+ /**
+ * Fully qualified URI of the configured OAuth2 client.
+ */
+ private URI clientUri = URI.create("/");
+
+ /**
+ * URI at which a login can be performed. If left empty, ${client-uri}/login is used. Can be
+ * changed to the URI on a SPA or a mobile application deep-link
+ */
+ private Optional loginUri = Optional.empty();
+
+ /**
+ * URI containing scheme, host and port where the user should be redirected after a successful
+ * login (defaults to the client URI)
+ */
+ private Optional postLoginRedirectHost = Optional.empty();
+
+ /**
+ * Where to redirect the user after successful login
+ */
+ private Optional postLoginRedirectPath = Optional.empty();
+
+ /**
+ * Where to redirect the user after login failure
+ */
+ private Optional loginErrorRedirectPath = Optional.empty();
+
+ /**
+ * HTTP status for redirections in OAuth2 login and logout. You might set this to something in 2xx
+ * range (like OK, ACCEPTED, NO_CONTENT, ...) for single page and mobile applications to handle
+ * this redirection as it wishes (change the user-agent, clear some headers, ...).
+ */
+ private OAuth2RedirectionProperties oauth2Redirections = new OAuth2RedirectionProperties();
+
+ public URI getPostLoginRedirectHost() {
+ return postLoginRedirectHost.orElse(clientUri);
+ }
+
+ public Optional getPostLoginRedirectUri() {
+ if (postLoginRedirectHost.isEmpty() && postLoginRedirectPath.isEmpty()) {
+ return Optional.empty();
+ }
+ final var uri = UriComponentsBuilder.fromUri(getPostLoginRedirectHost());
+ postLoginRedirectPath.ifPresent(uri::path);
+
+ return Optional.of(uri.build(Map.of()));
+ }
+
+ /**
+ * URI containing scheme, host and port where the user should be redirected after a successful
+ * logout (defaults to the client URI)
+ */
+ private Optional postLogoutRedirectHost = Optional.empty();
+
+ /**
+ * Path (relative to clientUri) where the user should be redirected after being logged out from
+ * authorization server(s)
+ */
+ private Optional postLogoutRedirectPath = Optional.empty();
+
+ public URI getPostLogoutRedirectHost() {
+ return postLogoutRedirectHost.orElse(clientUri);
+ }
+
+ public URI getPostLogoutRedirectUri() {
+ var uri = UriComponentsBuilder.fromUri(getPostLogoutRedirectHost());
+ postLogoutRedirectPath.ifPresent(uri::path);
+
+ return uri.build(Map.of());
+ }
+
+ /**
+ * Map of logout properties indexed by client registration ID (must match a registration in Spring
+ * Boot OAuth2 client configuration). {@link OAuth2LogoutProperties} are configuration for
+ * authorization server not strictly following the
+ * RP-Initiated Logout
+ * standard, but exposing a logout end-point expecting an authorized GET request with following
+ * request params:
+ *
+ * - "client-id" (required)
+ * - post-logout redirect URI (optional)
+ *
+ */
+ private Map oauth2Logout = new HashMap<>();
+
+ /**
+ *
+ * If true, AOP is used to instrument authorized client repository and keep the principalName
+ * current user has for each issuer he authenticates on.
+ *
+ *
+ * This is useful only if you allow a user to authenticate on more than one OpenID Provider at a
+ * time. For instance, user logs in on Google and on an authorization server of your own and your
+ * client sends direct queries to Google APIs (with an access token issued by Google) and resource
+ * servers of your own (with an access token from your authorization server).
+ *
+ */
+ private boolean multiTenancyEnabled = false;
+
+ /**
+ * Path matchers for the routes accessible to anonymous requests
+ */
+ private List permitAll = List.of("/login/**", "/oauth2/**");
+
+ /**
+ * CSRF protection configuration for the auto-configured client filter-chain
+ */
+ private Csrf csrf = Csrf.DEFAULT;
+
+ /**
+ * When true, PKCE is enabled (by default, Spring enables it only for "public" clients)
+ */
+ private boolean pkceForced = false;
+
+ /**
+ * Fine grained CORS configuration
+ *
+ * @deprecated use com.c4-soft.springaddons.oidc.cors instead
+ */
+ @Deprecated(forRemoval = true)
+ private List cors = List.of();
+
+ /**
+ * Additional parameters to send with authorization request, mapped by client registration IDs
+ *
+ * @deprecated use the more concise authorization-params syntax
+ */
+ @Deprecated
+ private Map> authorizationRequestParams = new HashMap<>();
+
+ /**
+ *
+ * Additional parameters to send with authorization request, mapped by client registration IDs.
+ *
+ *
+ * {@link OAuth2AuthorizationRequest#getAdditionalParameters()} return a Map<String,
+ * Object>, when it should probably be Map<String, List<String>>. Also the
+ * serializer does not handle collections correctly (serializes using {@link Object#toString()}
+ * instead of repeating the parameter with each value toString()). What spring-addons does is
+ * joining the String values with a comma.
+ *
+ */
+ private Map>> authorizationParams = new HashMap<>();
+
+ public MultiValueMap getExtraAuthorizationParameters(String registrationId) {
+ return getExtraParameters(registrationId, authorizationRequestParams, authorizationParams);
+ }
+
+ /**
+ * Additional parameters to send with token request, mapped by client registration IDs
+ *
+ * @deprecated use the more concise token-params syntax
+ */
+ @Deprecated
+ private Map> tokenRequestParams = new HashMap<>();
+
+ /**
+ * Additional parameters to send with authorization request, mapped by client registration IDs
+ */
+ private Map>> tokenParams = new HashMap<>();
+
+ public MultiValueMap getExtraTokenParameters(String registrationId) {
+ return getExtraParameters(registrationId, tokenRequestParams, tokenParams);
+ }
+
+ private static MultiValueMap getExtraParameters(String registrationId,
+ Map> requestParams,
+ Map>> requestParamsMap) {
+ final var extraParameters = Optional.ofNullable(requestParamsMap.get(registrationId))
+ .map(LinkedMultiValueMap::new).orElse(new LinkedMultiValueMap<>());
+ for (final var param : requestParams.getOrDefault(registrationId, List.of())) {
+ if (StringUtils.hasText(param.getName())) {
+ extraParameters.add(param.getName(), param.getValue());
+ }
+ }
+ return extraParameters;
+ }
- /**
- * Fully qualified URI of the configured OAuth2 client.
- */
- private URI clientUri = URI.create("/");
+ /**
+ * Logout properties for OpenID Providers which do not implement the RP-Initiated Logout spec
+ *
+ * @author Jerome Wacongne ch4mp@c4-soft.com
+ */
+ @Data
+ public static class OAuth2LogoutProperties {
/**
- * Path to the login page. Provide one only in the following cases:
- *
- * - you want to provide your own login @Controller
- * - you want to use port 80 or 8080 with SSL enabled (this will require you to provide with the login @Controller above)
- *
- * If left empty, the default Spring Boot configuration for OAuth2 login is applied
+ * URI on the authorization server where to redirect the user for logout
*/
- private Optional loginPath = Optional.empty();
+ private URI uri;
/**
- * URI containing scheme, host and port where the user should be redirected after a successful login (defaults to the client URI)
+ * request param name for client-id
*/
- private Optional postLoginRedirectHost = Optional.empty();
+ private Optional clientIdRequestParam = Optional.empty();
/**
- * Where to redirect the user after successful login
+ * request param name for post-logout redirect URI (where the user should be redirected after
+ * his session is closed on the authorization server)
*/
- private Optional postLoginRedirectPath = Optional.empty();
+ private Optional postLogoutUriRequestParam = Optional.empty();
/**
- * Where to redirect the user after login failure
+ * request param name for setting an ID-Token hint
*/
- private Optional loginErrorRedirectPath = Optional.empty();
+ private Optional idTokenHintRequestParam = Optional.empty();
/**
- * HTTP status for redirections in OAuth2 login and logout. You might set this to something in 2xx range (like OK, ACCEPTED, NO_CONTENT, ...) for single
- * page and mobile applications to handle this redirection as it wishes (change the user-agent, clear some headers, ...).
+ * RP-Initiated Logout is enabled by default. Setting this to false disables it.
*/
- private OAuth2RedirectionProperties oauth2Redirections = new OAuth2RedirectionProperties();
-
- public URI getPostLoginRedirectHost() {
- return postLoginRedirectHost.orElse(clientUri);
- }
+ private boolean enabled = true;
+ }
- public Optional getPostLoginRedirectUri() {
- if (postLoginRedirectHost.isEmpty() && postLoginRedirectPath.isEmpty()) {
- return Optional.empty();
- }
- final var uri = UriComponentsBuilder.fromUri(getPostLoginRedirectHost());
- postLoginRedirectPath.ifPresent(uri::path);
+ private BackChannelLogoutProperties backChannelLogout = new BackChannelLogoutProperties();
- return Optional.of(uri.build(Map.of()));
- }
-
- /**
- * URI containing scheme, host and port where the user should be redirected after a successful logout (defaults to the client URI)
- */
- private Optional postLogoutRedirectHost = Optional.empty();
+ @Data
+ public static class BackChannelLogoutProperties {
+ private boolean enabled = false;
/**
- * Path (relative to clientUri) where the user should be redirected after being logged out from authorization server(s)
+ * The URI for a loop of the Spring client to itself in which it actually ends the user session.
+ * Overriding this can be useful to force the scheme and port in the case where the client is
+ * behind a reverse proxy with different scheme and port (default URI uses the original
+ * Back-Channel Logout request scheme and ports).
*/
- private Optional postLogoutRedirectPath = Optional.empty();
-
- public URI getPostLogoutRedirectHost() {
- return postLogoutRedirectHost.orElse(clientUri);
- }
-
- public URI getPostLogoutRedirectUri() {
- var uri = UriComponentsBuilder.fromUri(getPostLogoutRedirectHost());
- postLogoutRedirectPath.ifPresent(uri::path);
-
- return uri.build(Map.of());
- }
-
+ private Optional internalLogoutUri = Optional.empty();
+
+ private Optional cookieName = Optional.empty();
+ }
+
+ /**
+ * Request parameter
+ *
+ * @author Jerome Wacongne ch4mp@c4-soft.com
+ */
+ @Data
+ public static class RequestParam {
/**
- * Map of logout properties indexed by client registration ID (must match a registration in Spring Boot OAuth2 client configuration).
- * {@link OAuth2LogoutProperties} are configuration for authorization server not strictly following the
- * RP-Initiated Logout standard, but exposing a logout end-point expecting an
- * authorized GET request with following request params:
- *
- * - "client-id" (required)
- * - post-logout redirect URI (optional)
- *
+ * request parameter name
*/
- private Map oauth2Logout = new HashMap<>();
+ private String name;
/**
- *
- * If true, AOP is used to instrument authorized client repository and keep the principalName current user has for each issuer he authenticates on.
- *
- *
- * This is useful only if you allow a user to authenticate on more than one OpenID Provider at a time. For instance, user logs in on Google and on an
- * authorization server of your own and your client sends direct queries to Google APIs (with an access token issued by Google) and resource servers of your
- * own (with an access token from your authorization server).
- *
+ * request parameter value
*/
- private boolean multiTenancyEnabled = false;
+ private String value;
+ }
+ @Data
+ public static class OAuth2RedirectionProperties {
/**
- * Path matchers for the routes accessible to anonymous requests
+ * Defines {@link AuthenticationEntryPoint} or {@link ServerAuthenticationEntryPoint} behavior
*/
- private List permitAll = List.of("/login/**", "/oauth2/**");
+ private HttpStatus authenticationEntryPoint = HttpStatus.FOUND;
/**
- * CSRF protection configuration for the auto-configured client filter-chain
+ * Status for the 1st response in authorization code flow, with location to get authorization
+ * code from authorization server
*/
- private Csrf csrf = Csrf.DEFAULT;
+ private HttpStatus preAuthorizationCode = HttpStatus.FOUND;
/**
- * When true, PKCE is enabled (by default, Spring enables it only for "public" clients)
+ * Status for the response after authorization code, with location to the UI
*/
- private boolean pkceForced = false;
+ private HttpStatus postAuthorizationCode = HttpStatus.FOUND;
/**
- * Fine grained CORS configuration
- *
- * @deprecated use com.c4-soft.springaddons.oidc.cors instead
+ * Status for the response after an authorization failure
*/
- @Deprecated(forRemoval = true)
- private List cors = List.of();
+ private HttpStatus postAuthorizationFailure = HttpStatus.FOUND;
/**
- * Additional parameters to send with authorization request, mapped by client registration IDs
- *
- * @deprecated use the more concise authorization-params syntax
+ * Status for the response after BFF logout, with location to authorization server logout
+ * endpoint
*/
- @Deprecated
- private Map> authorizationRequestParams = new HashMap<>();
-
+ private HttpStatus rpInitiatedLogout = HttpStatus.FOUND;
/**
- *
- * Additional parameters to send with authorization request, mapped by client registration IDs.
- *
- *
- * {@link OAuth2AuthorizationRequest#getAdditionalParameters()} return a Map<String, Object>, when it should probably be Map<String,
- * List<String>>. Also the serializer does not handle collections correctly (serializes using {@link Object#toString()} instead of repeating the
- * parameter with each value toString()). What spring-addons does is joining the String values with a comma.
- *
+ * Used only in servlet applications
*/
- private Map>> authorizationParams = new HashMap<>();
+ private HttpStatus invalidSessionStrategy = HttpStatus.FOUND;
+ }
- public MultiValueMap getExtraAuthorizationParameters(String registrationId) {
- return getExtraParameters(registrationId, authorizationRequestParams, authorizationParams);
- }
-
- /**
- * Additional parameters to send with token request, mapped by client registration IDs
- *
- * @deprecated use the more concise token-params syntax
- */
- @Deprecated
- private Map> tokenRequestParams = new HashMap<>();
-
- /**
- * Additional parameters to send with authorization request, mapped by client registration IDs
- */
- private Map>> tokenParams = new HashMap<>();
-
- public MultiValueMap getExtraTokenParameters(String registrationId) {
- return getExtraParameters(registrationId, tokenRequestParams, tokenParams);
- }
-
- private static MultiValueMap getExtraParameters(
- String registrationId,
- Map> requestParams,
- Map>> requestParamsMap) {
- final var extraParameters = Optional.ofNullable(requestParamsMap.get(registrationId)).map(LinkedMultiValueMap::new).orElse(new LinkedMultiValueMap<>());
- for (final var param : requestParams.getOrDefault(registrationId, List.of())) {
- if (StringUtils.hasText(param.getName())) {
- extraParameters.add(param.getName(), param.getValue());
- }
- }
- return extraParameters;
- }
-
- /**
- * Logout properties for OpenID Providers which do not implement the RP-Initiated Logout spec
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
- @Data
- @ConfigurationProperties
- public static class OAuth2LogoutProperties {
-
- /**
- * URI on the authorization server where to redirect the user for logout
- */
- private URI uri;
-
- /**
- * request param name for client-id
- */
- private Optional clientIdRequestParam = Optional.empty();
-
- /**
- * request param name for post-logout redirect URI (where the user should be redirected after his session is closed on the authorization server)
- */
- private Optional postLogoutUriRequestParam = Optional.empty();
-
- /**
- * request param name for setting an ID-Token hint
- */
- private Optional idTokenHintRequestParam = Optional.empty();
-
- /**
- * RP-Initiated Logout is enabled by default. Setting this to false disables it.
- */
- private boolean enabled = true;
- }
-
- private BackChannelLogoutProperties backChannelLogout = new BackChannelLogoutProperties();
-
- @Data
- @ConfigurationProperties
- public static class BackChannelLogoutProperties {
- private boolean enabled = false;
-
- /**
- * The URI for a loop of the Spring client to itself in which it actually ends the user session. Overriding this can be useful to force the scheme and
- * port in the case where the client is behind a reverse proxy with different scheme and port (default URI uses the original Back-Channel Logout request
- * scheme and ports).
- */
- private Optional internalLogoutUri = Optional.empty();
- }
-
- /**
- * Request parameter
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
- @Data
- @ConfigurationProperties
- public static class RequestParam {
- /**
- * request parameter name
- */
- private String name;
-
- /**
- * request parameter value
- */
- private String value;
- }
-
- @Data
- @ConfigurationProperties
- public static class OAuth2RedirectionProperties {
-
- /**
- * Status for the 1st response in authorization code flow, with location to get authorization code from authorization server
- */
- private HttpStatus preAuthorizationCode = HttpStatus.FOUND;
-
- /**
- * Status for the response after authorization code, with location to the UI
- */
- private HttpStatus postAuthorizationCode = HttpStatus.FOUND;
-
- /**
- * Status for the response after BFF logout, with location to authorization server logout endpoint
- */
- private HttpStatus rpInitiatedLogout = HttpStatus.FOUND;
- }
-
- public Optional getLogoutProperties(String clientRegistrationId) {
- return Optional.ofNullable(oauth2Logout.get(clientRegistrationId));
- }
+ public Optional getLogoutProperties(String clientRegistrationId) {
+ return Optional.ofNullable(oauth2Logout.get(clientRegistrationId));
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcProperties.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcProperties.java
index d63f88a04..b9c461716 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcProperties.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcProperties.java
@@ -2,17 +2,16 @@
import java.net.URI;
import java.util.List;
-
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
-
import lombok.Data;
/**
*
- * Configuration properties for OAuth2 auto-configuration extensions to spring-boot-starter-oauth2-client and spring-boot-starter-oauth2-resource-server.
+ * Configuration properties for OAuth2 auto-configuration extensions to
+ * spring-boot-starter-oauth2-client and spring-boot-starter-oauth2-resource-server.
*
* The following spring-boot standard properties are used:
*
@@ -20,8 +19,8 @@
* - spring.security.oauth2.client.registration.*
* - spring.security.oauth2.resourceserver.opaquetoken.*
*
- * spring.security.oauth2.resourceserver.jwt.* properties are ignored. The reason for that is it is applicable only to single tenant scenarios. Use
- * properties
+ * spring.security.oauth2.resourceserver.jwt.* properties are ignored. The reason for that is
+ * it is applicable only to single tenant scenarios. Use properties
*
* @author Jerome Wacongne ch4mp@c4-soft.com
*/
@@ -30,95 +29,100 @@
@ConfigurationProperties(prefix = "com.c4-soft.springaddons.oidc")
public class SpringAddonsOidcProperties {
+ /**
+ * OpenID Providers configuration: JWK set URI, issuer URI, audience, and authorities mapping
+ * configuration for each issuer. A minimum of one issuer is required. Properties defined here
+ * are a replacement for spring.security.oauth2.resourceserver.jwt.* (which will be ignored).
+ * Authorities mapping defined there is used by both client and resource server filter-chains.
+ */
+ private List ops = List.of();
+
+ /**
+ * Auto-configuration for an OAuth2 client (secured with session, not access token)
+ * Security(Web)FilterChain with @Order(Ordered.LOWEST_PRECEDENCE - 1). Typical use-cases are
+ * spring-cloud-gateway used as BFF and applications with Thymeleaf or another server-side
+ * rendering framework. Default configuration includes: enabled sessions, CSRF protection,
+ * "oauth2Login", "logout". securityMatchers must be set for this filter-chain @Bean and its
+ * dependencies to be defined. Properties defined here are a complement for
+ * spring.security.oauth2.client.* (which are required when enabling spring-addons client
+ * filter-chain).
+ */
+ @NestedConfigurationProperty
+ private SpringAddonsOidcClientProperties client = new SpringAddonsOidcClientProperties();
+
+ /**
+ * Auto-configuration for an OAuth2 resource server Security(Web)FilterChain with
+ * @Order(LOWEST_PRECEDENCE). Typical use case is a REST API secured with access tokens.
+ * Default configuration is as follow: no securityMatcher to process all the requests that were
+ * not intercepted by higher @Order Security(Web)FilterChains, no session, disabled CSRF
+ * protection, and 401 to unauthorized requests.
+ */
+ @NestedConfigurationProperty
+ private SpringAddonsOidcResourceServerProperties resourceserver =
+ new SpringAddonsOidcResourceServerProperties();
+
+ private List cors = List.of();
+
+ /**
+ * OpenID Providers configuration. A minimum of one issuer is required. Properties defined here
+ * are a replacement for spring.security.oauth2.resourceserver.jwt.* (which will be ignored).
+ * Authorities mapping defined here is used by both client and resource server filter-chains.
+ *
+ * @author Jerome Wacongne ch4mp@c4-soft.com
+ */
+ @Data
+ static public class OpenidProviderProperties {
/**
- * OpenID Providers configuration: JWK set URI, issuer URI, audience, and authorities mapping configuration for each issuer. A minimum of one issuer is
- * required. Properties defined here are a replacement for spring.security.oauth2.resourceserver.jwt.* (which will be ignored). Authorities mapping
- * defined there is used by both client and resource server filter-chains.
+ *
+ * Must be exactly the same as in access tokens (even trailing slash, if any, is important). In
+ * case of doubt, open one of your access tokens with a tool like
+ * https://jwt.io.
+ *
*/
- private List ops = List.of();
+ private URI iss;
/**
- * Auto-configuration for an OAuth2 client (secured with session, not access token) Security(Web)FilterChain with @Order(Ordered.LOWEST_PRECEDENCE - 1).
- * Typical use-cases are spring-cloud-gateway used as BFF and applications with Thymeleaf or another server-side rendering framework. Default configuration
- * includes: enabled sessions, CSRF protection, "oauth2Login", "logout". securityMatchers must be set for this filter-chain @Bean and its dependencies
- * to be defined. Properties defined here are a complement for spring.security.oauth2.client.* (which are required when enabling spring-addons client
- * filter-chain).
+ * Can be omitted if OpenID configuration can be retrieved from
+ * ${iss}/.well-known/openid-configuration
*/
- @NestedConfigurationProperty
- private SpringAddonsOidcClientProperties client = new SpringAddonsOidcClientProperties();
+ private URI jwkSetUri;
/**
- * Auto-configuration for an OAuth2 resource server Security(Web)FilterChain with @Order(LOWEST_PRECEDENCE). Typical use case is a REST API secured with
- * access tokens. Default configuration is as follow: no securityMatcher to process all the requests that were not intercepted by higher @Order
- * Security(Web)FilterChains, no session, disabled CSRF protection, and 401 to unauthorized requests.
+ * Can be omitted. Will insert an audience validator if not null or empty
*/
- @NestedConfigurationProperty
- private SpringAddonsOidcResourceServerProperties resourceserver = new SpringAddonsOidcResourceServerProperties();
+ private String aud;
- private List cors = List.of();
+ /**
+ * Authorities mapping configuration, per claim
+ */
+ private List authorities = List.of();
/**
- * OpenID Providers configuration. A minimum of one issuer is required. Properties defined here are a replacement for
- * spring.security.oauth2.resourceserver.jwt.* (which will be ignored). Authorities mapping defined here is used by both client and resource server
- * filter-chains.
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
+ * JSON path for the claim to use as "name" source
*/
+ private String usernameClaim = StandardClaimNames.SUB;
+
@Data
- @ConfigurationProperties
- static public class OpenidProviderProperties {
- /**
- *
- * Must be exactly the same as in access tokens (even trailing slash, if any, is important). In case of doubt, open one of your access tokens with a
- * tool like https://jwt.io.
- *
- */
- private URI iss;
-
- /**
- * Can be omitted if OpenID configuration can be retrieved from ${iss}/.well-known/openid-configuration
- */
- private URI jwkSetUri;
-
- /**
- * Can be omitted. Will insert an audience validator if not null or empty
- */
- private String aud;
-
- /**
- * Authorities mapping configuration, per claim
- */
- private List authorities = List.of();
-
- /**
- * JSON path for the claim to use as "name" source
- */
- private String usernameClaim = StandardClaimNames.SUB;
-
- @Data
- @ConfigurationProperties
- public static class SimpleAuthoritiesMappingProperties {
- /**
- * JSON path of the claim(s) to map with this properties
- */
- private String path = "$.realm_access.roles";
-
- /**
- * What to prefix authorities with (for instance "ROLE_" or "SCOPE_")
- */
- private String prefix = "";
-
- /**
- * Whether to transform authorities to uppercase, lowercase, or to leave it unchanged
- */
- private Case caze = Case.UNCHANGED;
-
- public static enum Case {
- UNCHANGED,
- UPPER,
- LOWER
- }
-
- }
+ public static class SimpleAuthoritiesMappingProperties {
+ /**
+ * JSON path of the claim(s) to map with this properties
+ */
+ private String path = "$.realm_access.roles";
+
+ /**
+ * What to prefix authorities with (for instance "ROLE_" or "SCOPE_")
+ */
+ private String prefix = "";
+
+ /**
+ * Whether to transform authorities to uppercase, lowercase, or to leave it unchanged
+ */
+ private Case caze = Case.UNCHANGED;
+
+ public static enum Case {
+ UNCHANGED, UPPER, LOWER
+ }
+
}
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcResourceServerProperties.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcResourceServerProperties.java
index 6827b318b..610abb54d 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcResourceServerProperties.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcResourceServerProperties.java
@@ -1,48 +1,47 @@
package com.c4_soft.springaddons.security.oidc.starter.properties;
import java.util.List;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
import lombok.Data;
/**
- * Auto-configuration for an OAuth2 resource server Security(Web)FilterChain with @Order(LOWEST_PRECEDENCE). Typical use case is a REST API secured with
- * access tokens. Default configuration is as follow: no securityMatcher to process all the requests that were not intercepted by higher @Order
- * Security(Web)FilterChains, no session, disabled CSRF protection, and 401 to unauthorized requests.
+ * Auto-configuration for an OAuth2 resource server Security(Web)FilterChain with
+ * @Order(LOWEST_PRECEDENCE). Typical use case is a REST API secured with access tokens. Default
+ * configuration is as follow: no securityMatcher to process all the requests that were not
+ * intercepted by higher @Order Security(Web)FilterChains, no session, disabled CSRF protection,
+ * and 401 to unauthorized requests.
*
* @author Jerome Wacongne ch4mp@c4-soft.com
*/
@Data
-@ConfigurationProperties
public class SpringAddonsOidcResourceServerProperties {
- /**
- * Resource server SecurityFilterChain bean and all its dependencies are instantiated only if true.
- */
- private boolean enabled = true;
-
- /**
- * Path matchers for the routes accessible to anonymous requests
- */
- private List permitAll = List.of();
-
- /**
- * Whether to disable sessions. It should remain true.
- */
- private boolean statlessSessions = true;
-
- /**
- * CSRF protection configuration for the auto-configured client filter-chain
- */
- private Csrf csrf = Csrf.DISABLE;
-
- /**
- * Fine grained CORS configuration
- *
- * @deprecated use com.c4-soft.springaddons.oidc.cors instead
- */
- @Deprecated(forRemoval = true)
- private List cors = List.of();
+ /**
+ * Resource server SecurityFilterChain bean and all its dependencies are instantiated only if
+ * true.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Path matchers for the routes accessible to anonymous requests
+ */
+ private List permitAll = List.of();
+
+ /**
+ * Whether to disable sessions. It should remain true.
+ */
+ private boolean statlessSessions = true;
+
+ /**
+ * CSRF protection configuration for the auto-configured client filter-chain
+ */
+ private Csrf csrf = Csrf.DISABLE;
+
+ /**
+ * Fine grained CORS configuration
+ *
+ * @deprecated use com.c4-soft.springaddons.oidc.cors instead
+ */
+ @Deprecated(forRemoval = true)
+ private List cors = List.of();
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/HasTokenEdpointParametersPropertiesCondition.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/HasTokenEdpointParametersPropertiesCondition.java
index 511d8e0ab..322f4c5b4 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/HasTokenEdpointParametersPropertiesCondition.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/HasTokenEdpointParametersPropertiesCondition.java
@@ -2,7 +2,7 @@
public class HasTokenEdpointParametersPropertiesCondition extends HasPropertyPrefixCondition {
- public HasTokenEdpointParametersPropertiesCondition() {
- super("com.c4-soft.springaddons.oidc.client.token-request-params");
- }
+ public HasTokenEdpointParametersPropertiesCondition() {
+ super("com.c4-soft.springaddons.oidc.client.token-request-params");
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultAuthenticationEntryPointCondition.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultAuthenticationEntryPointCondition.java
new file mode 100644
index 000000000..7df6c7b01
--- /dev/null
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultAuthenticationEntryPointCondition.java
@@ -0,0 +1,21 @@
+package com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
+
+public class DefaultAuthenticationEntryPointCondition extends NoneNestedConditions {
+
+ public DefaultAuthenticationEntryPointCondition() {
+ super(ConfigurationPhase.REGISTER_BEAN);
+ }
+
+ @ConditionalOnBean(AuthenticationEntryPoint.class)
+ static class AuthenticationEntryPointCondition {
+ }
+
+ @ConditionalOnBean(ServerAuthenticationEntryPoint.class)
+ static class ServerAuthenticationEntryPointCondition {
+ }
+}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultAuthenticationSuccessHandlerCondition.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultAuthenticationSuccessHandlerCondition.java
index 34c04f0a0..a4fe0c30d 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultAuthenticationSuccessHandlerCondition.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultAuthenticationSuccessHandlerCondition.java
@@ -3,6 +3,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
public class DefaultAuthenticationSuccessHandlerCondition extends NoneNestedConditions {
@@ -14,7 +15,7 @@ public DefaultAuthenticationSuccessHandlerCondition() {
static class AuthenticationSuccessHandlerProvidedCondition {
}
- @ConditionalOnBean(AuthenticationSuccessHandler.class)
+ @ConditionalOnBean(ServerAuthenticationSuccessHandler.class)
static class ServerAuthenticationSuccessHandlerProvidedCondition {
}
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultOidcBackChannelLogoutHandlerCondition.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultOidcBackChannelLogoutHandlerCondition.java
new file mode 100644
index 000000000..d096bae10
--- /dev/null
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultOidcBackChannelLogoutHandlerCondition.java
@@ -0,0 +1,27 @@
+package com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean;
+
+import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcBackChannelLogoutHandler;
+import org.springframework.security.config.web.server.OidcBackChannelServerLogoutHandler;
+
+public class DefaultOidcBackChannelLogoutHandlerCondition extends AllNestedConditions {
+
+ public DefaultOidcBackChannelLogoutHandlerCondition() {
+ super(ConfigurationPhase.REGISTER_BEAN);
+ }
+
+ @ConditionalOnProperty(name = "com.c4-soft.springaddons.oidc.client.back-channel-logout.enabled")
+ static class BackChannelLogoutEnabledCondition {
+ }
+
+ @ConditionalOnMissingBean(OidcBackChannelLogoutHandler.class)
+ static class NoOidcBackChannelLogoutHandlerCondition {
+ }
+
+ @ConditionalOnMissingBean(OidcBackChannelServerLogoutHandler.class)
+ static class NoOidcBackChannelServerLogoutHandlerCondition {
+ }
+
+}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultOidcSessionRegistryCondition.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultOidcSessionRegistryCondition.java
new file mode 100644
index 000000000..538d26f74
--- /dev/null
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultOidcSessionRegistryCondition.java
@@ -0,0 +1,24 @@
+package com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean;
+
+import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
+import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
+
+public class DefaultOidcSessionRegistryCondition extends AllNestedConditions {
+
+ public DefaultOidcSessionRegistryCondition() {
+ super(ConfigurationPhase.REGISTER_BEAN);
+ }
+
+ @ConditionalOnProperty(name = "com.c4-soft.springaddons.oidc.client.back-channel-logout.enabled")
+ static class BackChannelLogoutEnabledCondition {}
+
+ @ConditionalOnMissingBean(OidcSessionRegistry.class)
+ static class NoOidcSessionRegistryCondition {}
+
+ @ConditionalOnMissingBean(ReactiveOidcSessionRegistry.class)
+ static class NoReactiveOidcSessionRegistryCondition {}
+
+}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java
index 54dd95101..6d358f39b 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java
@@ -1,16 +1,18 @@
package com.c4_soft.springaddons.security.oidc.starter.reactive;
import static org.springframework.security.config.Customizer.withDefaults;
-
+import java.net.URI;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
-
+import java.util.stream.Collectors;
import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
import org.springframework.security.config.web.server.ServerHttpSecurity;
-import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
-import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
+import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.CsrfToken;
@@ -20,176 +22,193 @@
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.server.ServerWebExchange;
-
import com.c4_soft.springaddons.security.oidc.starter.properties.CorsProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.Csrf;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
+import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties.OpenidProviderProperties;
import com.c4_soft.springaddons.security.oidc.starter.reactive.client.ClientAuthorizeExchangeSpecPostProcessor;
import com.c4_soft.springaddons.security.oidc.starter.reactive.client.ClientReactiveHttpSecurityPostProcessor;
import com.c4_soft.springaddons.security.oidc.starter.reactive.resourceserver.ResourceServerAuthorizeExchangeSpecPostProcessor;
import com.c4_soft.springaddons.security.oidc.starter.reactive.resourceserver.ResourceServerReactiveHttpSecurityPostProcessor;
-
import reactor.core.publisher.Mono;
public class ReactiveConfigurationSupport {
- public static ServerHttpSecurity configureResourceServer(
- ServerHttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ServerAuthenticationEntryPoint authenticationEntryPoint,
- Optional accessDeniedHandler,
- ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
- ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor) {
-
- ReactiveConfigurationSupport
- .configureState(http, addonsProperties.getResourceserver().isStatlessSessions(), addonsProperties.getResourceserver().getCsrf());
-
- // FIXME: use only the new CORS properties at next major release
- final var corsProps = new ArrayList<>(addonsProperties.getCors());
- final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
- final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
- corsProps.addAll(deprecatedClientCorsProps);
- corsProps.addAll(deprecatedResourceServerCorsProps);
- ReactiveConfigurationSupport.configureAccess(http, addonsProperties.getResourceserver().getPermitAll(), corsProps);
-
- http.exceptionHandling(handling -> {
- handling.authenticationEntryPoint(authenticationEntryPoint);
- accessDeniedHandler.ifPresent(handling::accessDeniedHandler);
- });
-
- if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
- http.redirectToHttps(withDefaults());
- }
-
- http.authorizeExchange(registry -> authorizePostProcessor.authorizeHttpRequests(registry));
- httpPostProcessor.process(http);
-
- return http;
+ public static ServerHttpSecurity configureResourceServer(ServerHttpSecurity http,
+ ServerProperties serverProperties, SpringAddonsOidcProperties addonsProperties,
+ ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
+ ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor) {
+
+ http.exceptionHandling(exceptions -> {
+ final var issuers = addonsProperties.getOps().stream().map(OpenidProviderProperties::getIss)
+ .filter(iss -> iss != null).map(URI::toString)
+ .collect(Collectors.joining(",", "\"", "\""));
+ exceptions
+ .authenticationEntryPoint((ServerWebExchange exchange, AuthenticationException ex) -> {
+ var response = exchange.getResponse();
+ response.setStatusCode(HttpStatus.UNAUTHORIZED);
+ response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE,
+ "OAuth realm=%s".formatted(issuers));
+ var dataBufferFactory = response.bufferFactory();
+ var buffer = dataBufferFactory.wrap(ex.getMessage().getBytes(Charset.defaultCharset()));
+ return response.writeWith(Mono.just(buffer))
+ .doOnError(error -> DataBufferUtils.release(buffer));
+ });
+ });
+
+ ReactiveConfigurationSupport.configureState(http,
+ addonsProperties.getResourceserver().isStatlessSessions(),
+ addonsProperties.getResourceserver().getCsrf());
+
+ // FIXME: use only the new CORS properties at next major release
+ final var corsProps = new ArrayList<>(addonsProperties.getCors());
+ final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
+ final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
+ corsProps.addAll(deprecatedClientCorsProps);
+ corsProps.addAll(deprecatedResourceServerCorsProps);
+ ReactiveConfigurationSupport.configureAccess(http,
+ addonsProperties.getResourceserver().getPermitAll(), corsProps);
+
+ if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
+ http.redirectToHttps(withDefaults());
}
- public static ServerHttpSecurity configureClient(
- ServerHttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ClientAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
- ClientReactiveHttpSecurityPostProcessor httpPostProcessor) {
+ http.authorizeExchange(registry -> authorizePostProcessor.authorizeHttpRequests(registry));
+ httpPostProcessor.process(http);
- ReactiveConfigurationSupport.configureState(http, false, addonsProperties.getClient().getCsrf());
+ return http;
+ }
- // FIXME: use only the new CORS properties at next major release
- final var corsProps = new ArrayList<>(addonsProperties.getCors());
- final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
- final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
- corsProps.addAll(deprecatedClientCorsProps);
- corsProps.addAll(deprecatedResourceServerCorsProps);
- ReactiveConfigurationSupport.configureAccess(http, addonsProperties.getClient().getPermitAll(), corsProps);
+ public static ServerHttpSecurity configureClient(ServerHttpSecurity http,
+ ServerProperties serverProperties, SpringAddonsOidcProperties addonsProperties,
+ ClientAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
+ ClientReactiveHttpSecurityPostProcessor httpPostProcessor) {
- if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
- http.redirectToHttps(withDefaults());
- }
+ ReactiveConfigurationSupport.configureState(http, false,
+ addonsProperties.getClient().getCsrf());
- http.authorizeExchange(registry -> authorizePostProcessor.authorizeHttpRequests(registry));
- httpPostProcessor.process(http);
+ // FIXME: use only the new CORS properties at next major release
+ final var corsProps = new ArrayList<>(addonsProperties.getCors());
+ final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
+ final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
+ corsProps.addAll(deprecatedClientCorsProps);
+ corsProps.addAll(deprecatedResourceServerCorsProps);
+ ReactiveConfigurationSupport.configureAccess(http, addonsProperties.getClient().getPermitAll(),
+ corsProps);
- return http;
+ if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
+ http.redirectToHttps(withDefaults());
}
- public static ServerHttpSecurity configureAccess(ServerHttpSecurity http, List permitAll, List corsProperties) {
- final var permittedCorsOptions = corsProperties
- .stream()
- .filter(cors -> (cors.getAllowedMethods().contains("*") || cors.getAllowedMethods().contains("OPTIONS")) && !cors.isDisableAnonymousOptions())
- .map(CorsProperties::getPath)
- .toList();
+ http.authorizeExchange(registry -> authorizePostProcessor.authorizeHttpRequests(registry));
+ httpPostProcessor.process(http);
+
+ return http;
+ }
- if (permitAll.size() > 0 || permittedCorsOptions.size() > 0) {
- http.anonymous(withDefaults());
- }
+ public static ServerHttpSecurity configureAccess(ServerHttpSecurity http, List permitAll,
+ List corsProperties) {
+ final var permittedCorsOptions = corsProperties.stream()
+ .filter(cors -> (cors.getAllowedMethods().contains("*")
+ || cors.getAllowedMethods().contains("OPTIONS")) && !cors.isDisableAnonymousOptions())
+ .map(CorsProperties::getPath).toList();
- if (permitAll.size() > 0) {
- http.authorizeExchange(authorizeExchange -> authorizeExchange.pathMatchers(permitAll.toArray(new String[] {})).permitAll());
- }
+ if (permitAll.size() > 0 || permittedCorsOptions.size() > 0) {
+ http.anonymous(withDefaults());
+ }
- if (permittedCorsOptions.size() > 0) {
- http
- .authorizeExchange(
- authorizeExchange -> authorizeExchange.pathMatchers(HttpMethod.OPTIONS, permittedCorsOptions.toArray(new String[] {})).permitAll());
- }
+ if (permitAll.size() > 0) {
+ http.authorizeExchange(authorizeExchange -> authorizeExchange
+ .pathMatchers(permitAll.toArray(new String[] {})).permitAll());
+ }
- return http;
+ if (permittedCorsOptions.size() > 0) {
+ http.authorizeExchange(authorizeExchange -> authorizeExchange
+ .pathMatchers(HttpMethod.OPTIONS, permittedCorsOptions.toArray(new String[] {}))
+ .permitAll());
}
- public static CorsWebFilter getCorsFilterBean(List corsProperties) {
- final var source = new UrlBasedCorsConfigurationSource();
- for (final var corsProps : corsProperties) {
- final var configuration = new CorsConfiguration();
- configuration.setAllowCredentials(corsProps.getAllowCredentials());
- configuration.setAllowedHeaders(corsProps.getAllowedHeaders());
- configuration.setAllowedMethods(corsProps.getAllowedMethods());
- configuration.setAllowedOriginPatterns(corsProps.getAllowedOriginPatterns());
- configuration.setExposedHeaders(corsProps.getExposedHeaders());
- configuration.setMaxAge(corsProps.getMaxAge());
- source.registerCorsConfiguration(corsProps.getPath(), configuration);
- }
- return new CorsWebFilter(source);
+ return http;
+ }
+
+ public static CorsWebFilter getCorsFilterBean(List corsProperties) {
+ final var source = new UrlBasedCorsConfigurationSource();
+ for (final var corsProps : corsProperties) {
+ final var configuration = new CorsConfiguration();
+ configuration.setAllowCredentials(corsProps.getAllowCredentials());
+ configuration.setAllowedHeaders(corsProps.getAllowedHeaders());
+ configuration.setAllowedMethods(corsProps.getAllowedMethods());
+ configuration.setAllowedOriginPatterns(corsProps.getAllowedOriginPatterns());
+ configuration.setExposedHeaders(corsProps.getExposedHeaders());
+ configuration.setMaxAge(corsProps.getMaxAge());
+ source.registerCorsConfiguration(corsProps.getPath(), configuration);
+ }
+ return new CorsWebFilter(source);
+ }
+
+ public static ServerHttpSecurity configureState(ServerHttpSecurity http, boolean isStatless,
+ Csrf csrfEnum) {
+
+ if (isStatless) {
+ http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
}
- public static ServerHttpSecurity configureState(ServerHttpSecurity http, boolean isStatless, Csrf csrfEnum) {
-
- if (isStatless) {
- http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
- }
-
- http.csrf(csrf -> {
- switch (csrfEnum) {
- case DISABLE:
- csrf.disable();
- break;
- case DEFAULT:
- if (isStatless) {
- csrf.disable();
- } else {
- withDefaults();
- }
- break;
- case SESSION:
- withDefaults();
- break;
- case COOKIE_ACCESSIBLE_FROM_JS:
- // adapted from https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa
- csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler());
- break;
- }
- });
-
- return http;
+ http.csrf(csrf -> {
+ switch (csrfEnum) {
+ case DISABLE:
+ csrf.disable();
+ break;
+ case DEFAULT:
+ if (isStatless) {
+ csrf.disable();
+ } else {
+ withDefaults();
+ }
+ break;
+ case SESSION:
+ withDefaults();
+ break;
+ case COOKIE_ACCESSIBLE_FROM_JS:
+ // adapted from
+ // https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa
+ csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
+ .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler());
+ break;
+ }
+ });
+
+ return http;
+ }
+
+ /**
+ * Adapted from
+ * https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa
+ */
+ static final class SpaCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {
+ private final ServerCsrfTokenRequestAttributeHandler delegate =
+ new XorServerCsrfTokenRequestAttributeHandler();
+
+ @Override
+ public void handle(ServerWebExchange exchange, Mono csrfToken) {
+ /*
+ * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the
+ * CsrfToken when it is rendered in the response body.
+ */
+ this.delegate.handle(exchange, csrfToken);
}
- /**
- * Adapted from https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa
- */
- static final class SpaCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {
- private final ServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();
-
- @Override
- public void handle(ServerWebExchange exchange, Mono csrfToken) {
- /*
- * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body.
- */
- this.delegate.handle(exchange, csrfToken);
- }
-
- @Override
- public Mono resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
- /*
- * If the request contains a X-XSRF-TOKEN header, use it. This applies when a single-page application includes the header value automatically,
- * which was obtained via a cookie containing the raw CsrfToken. In all other cases (e.g. if the request contains a request parameter), use
- * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies when a server-side rendered form includes the _csrf request parameter
- * as a hidden input.
- */
- return Mono
- .justOrEmpty(exchange.getRequest().getHeaders().getFirst(csrfToken.getHeaderName()))
- .switchIfEmpty(this.delegate.resolveCsrfTokenValue(exchange, csrfToken));
- }
+ @Override
+ public Mono resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
+ /*
+ * If the request contains a X-XSRF-TOKEN header, use it. This applies when a single-page
+ * application includes the header value automatically, which was obtained via a cookie
+ * containing the raw CsrfToken. In all other cases (e.g. if the request contains a request
+ * parameter), use XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
+ * when a server-side rendered form includes the _csrf request parameter as a hidden input.
+ */
+ return Mono
+ .justOrEmpty(exchange.getRequest().getHeaders().getFirst(csrfToken.getHeaderName()))
+ .switchIfEmpty(this.delegate.resolveCsrfTokenValue(exchange, csrfToken));
}
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientWithLoginBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientWithLoginBeans.java
index 93bb41fbe..a9a49027c 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientWithLoginBeans.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientWithLoginBeans.java
@@ -2,7 +2,6 @@
import java.util.ArrayList;
import java.util.Optional;
-
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -13,14 +12,16 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
-import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.OidcBackChannelServerLogoutHandler;
import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
+import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.ServerRedirectStrategy;
-import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
@@ -31,146 +32,157 @@
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.server.WebFilter;
-import org.springframework.web.util.UriComponentsBuilder;
-
import com.c4_soft.springaddons.security.oidc.starter.ClaimSetAuthoritiesConverter;
import com.c4_soft.springaddons.security.oidc.starter.ConfigurableClaimSetAuthoritiesConverter;
import com.c4_soft.springaddons.security.oidc.starter.LogoutRequestUriBuilder;
import com.c4_soft.springaddons.security.oidc.starter.SpringAddonsOAuth2LogoutRequestUriBuilder;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.CookieCsrfCondition;
+import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationEntryPointCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationFailureHandlerCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationSuccessHandlerCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultCorsWebFilterCondition;
+import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultOidcBackChannelLogoutHandlerCondition;
+import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultOidcSessionRegistryCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientWithLoginCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsNotServlet;
import com.c4_soft.springaddons.security.oidc.starter.reactive.ReactiveConfigurationSupport;
import com.c4_soft.springaddons.security.oidc.starter.reactive.ReactiveSpringAddonsOidcBeans;
-
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
/**
* The following {@link ConditionalOnMissingBean @ConditionalOnMissingBeans} are auto-configured
*
- * - springAddonsClientFilterChain: a {@link SecurityWebFilterChain}. Instantiated only if "com.c4-soft.springaddons.oidc.client.security-matchers" property
- * has at least one entry. If defined, it is with a high precedence, to ensure that all routes defined in this security matcher property are intercepted by this
- * filter-chain.
- * - logoutRequestUriBuilder: builder for RP-Initiated Logout queries, taking
- * configuration from properties for OIDC providers which do not strictly comply with the spec: logout URI not provided by OIDC conf or non standard parameter
- * names (Auth0 and Cognito are samples of such OPs)
- * - logoutSuccessHandler: a {@link ServerLogoutSuccessHandler}. Default instance is a {@link SpringAddonsServerLogoutSuccessHandler} which logs a user out
- * from the last authorization server he logged on
- * - authoritiesConverter: an {@link ClaimSetAuthoritiesConverter}. Default instance is a {@link ConfigurableClaimSetAuthoritiesConverter} which reads
- * spring-addons {@link SpringAddonsOidcProperties}
- * - csrfCookieWebFilter: a {@link WebFilter} to set the CSRF cookie if "com.c4-soft.springaddons.oidc.client.csrf" is set to cookie
- * - clientAuthorizePostProcessor: a {@link ClientAuthorizeExchangeSpecPostProcessor} post processor to fine tune access control from java configuration. It
- * applies to all routes not listed in "permit-all" property configuration. Default requires users to be authenticated.
- * - clientHttpPostProcessor: a {@link ClientReactiveHttpSecurityPostProcessor} to override anything from above auto-configuration. It is called just before
- * the security filter-chain is returned. Default is a no-op.
- * - authorizationRequestResolver: a {@link ServerOAuth2AuthorizationRequestResolver} to add custom parameters (from application properties) to authorization
- * code request
+ * - springAddonsClientFilterChain: a {@link SecurityWebFilterChain}. Instantiated only if
+ * "com.c4-soft.springaddons.oidc.client.security-matchers" property has at least one entry. If
+ * defined, it is with a high precedence, to ensure that all routes defined in this security matcher
+ * property are intercepted by this filter-chain.
+ * - logoutRequestUriBuilder: builder for
+ * RP-Initiated Logout
+ * queries, taking configuration from properties for OIDC providers which do not strictly comply
+ * with the spec: logout URI not provided by OIDC conf or non standard parameter names (Auth0 and
+ * Cognito are samples of such OPs)
+ * - logoutSuccessHandler: a {@link ServerLogoutSuccessHandler}. Default instance is a
+ * {@link SpringAddonsServerLogoutSuccessHandler} which logs a user out from the last authorization
+ * server he logged on
+ * - authoritiesConverter: an {@link ClaimSetAuthoritiesConverter}. Default instance is a
+ * {@link ConfigurableClaimSetAuthoritiesConverter} which reads spring-addons
+ * {@link SpringAddonsOidcProperties}
+ * - csrfCookieWebFilter: a {@link WebFilter} to set the CSRF cookie if
+ * "com.c4-soft.springaddons.oidc.client.csrf" is set to cookie
+ * - clientAuthorizePostProcessor: a {@link ClientAuthorizeExchangeSpecPostProcessor} post
+ * processor to fine tune access control from java configuration. It applies to all routes not
+ * listed in "permit-all" property configuration. Default requires users to be authenticated.
+ * - clientHttpPostProcessor: a {@link ClientReactiveHttpSecurityPostProcessor} to override
+ * anything from above auto-configuration. It is called just before the security filter-chain is
+ * returned. Default is a no-op.
+ * - authorizationRequestResolver: a {@link ServerOAuth2AuthorizationRequestResolver} to add
+ * custom parameters (from application properties) to authorization code request
*
*
* @author Jerome Wacongne ch4mp@c4-soft.com
*/
-@Conditional({ IsClientWithLoginCondition.class, IsNotServlet.class })
+@Conditional({IsClientWithLoginCondition.class, IsNotServlet.class})
@EnableWebFluxSecurity
@AutoConfiguration
@ImportAutoConfiguration(ReactiveSpringAddonsOidcBeans.class)
@Slf4j
public class ReactiveSpringAddonsOidcClientWithLoginBeans {
- /**
- *
- * Instantiated only if "com.c4-soft.springaddons.oidc.client.security-matchers" property has at least one entry. If defined, it is with higher precedence
- * than resource server one.
- *
- * It defines:
- *
- * - If the path to login page was provided in conf, a @Controller must be provided to handle it. Otherwise Spring Boot default generated one is
- * used
- * - logout (using {@link SpringAddonsServerLogoutSuccessHandler} by default)
- * - forces SSL usage if it is enabled
properties
- * - CSRF protection as defined in spring-addons client properties (enabled by default in this filter-chain).
- * - allow access to unauthorized requests to path matchers listed in spring-security client "permit-all" property
- * - as usual, apply {@link ClientAuthorizeExchangeSpecPostProcessor} for access control configuration from Java conf and
- * {@link ClientReactiveHttpSecurityPostProcessor} to override anything from the auto-configuration listed above
- *
- *
- * @param http the security filter-chain builder to configure
- * @param serverProperties Spring Boot standard server properties
- * @param authorizationRequestResolver the authorization request resolver to use. By default {@link ServerOAuth2AuthorizationRequestResolver} (adds
- * authorization request parameters defined in properties and builds absolutes callback URI). By default, a
- * {@link SpringAddonsServerOAuth2AuthorizationRequestResolver} is used
- * @param preAuthorizationCodeRedirectStrategy the redirection strategy to use for authorization-code request
- * @param authenticationSuccessHandler the authentication success handler to use. By default, a {@link SpringAddonsOauth2ServerAuthenticationSuccessHandler}
- * is used.
- * @param authenticationFailureHandler the authentication failure handler to use. By default, a {@link SpringAddonsOauth2ServerAuthenticationFailureHandler}
- * is used.
- * @param logoutSuccessHandler Defaulted to {@link SpringAddonsServerLogoutSuccessHandler} which can handle "almost" RP Initiated Logout conformant OPs
- * (like Auth0 and Cognito)
- * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties spring-addons client properties}
- * @param authorizePostProcessor post process authorization after "permit-all" configuration was applied (default is "isAuthenticated()" to everything that
- * was not matched)
- * @param httpPostProcessor post process the "http" builder just before it is returned (enables to override anything from the auto-configuration)
- * spring-addons client properties}
- * @param oidcLogoutCustomizer a configurer for Spring Security Back-Channel Logout implementation
- * @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2 clients
- * @throws Exception in case of miss-configuration
- */
- @Order(Ordered.LOWEST_PRECEDENCE - 1)
- @Bean
- SecurityWebFilterChain clientFilterChain(
- ServerHttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver,
- PreAuthorizationCodeServerRedirectStrategy preAuthorizationCodeRedirectStrategy,
- Optional authenticationSuccessHandler,
- Optional authenticationFailureHandler,
- ServerLogoutSuccessHandler logoutSuccessHandler,
- ClientAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
- ClientReactiveHttpSecurityPostProcessor httpPostProcessor,
- Optional logoutHandler,
- Customizer oidcLogoutCustomizer)
- throws Exception {
+ /**
+ *
+ * Instantiated only if "com.c4-soft.springaddons.oidc.client.security-matchers" property has at
+ * least one entry. If defined, it is with higher precedence than resource server one.
+ *
+ * It defines:
+ *
+ * - If the path to login page was provided in conf, a @Controller must be provided to
+ * handle it. Otherwise Spring Boot default generated one is used
+ * - logout (using {@link SpringAddonsServerLogoutSuccessHandler} by default)
+ * - forces SSL usage if it is enabled
properties
+ * - CSRF protection as defined in spring-addons client properties (enabled by default in
+ * this filter-chain).
+ * - allow access to unauthorized requests to path matchers listed in spring-security
+ * client "permit-all" property
+ * - as usual, apply {@link ClientAuthorizeExchangeSpecPostProcessor} for access control
+ * configuration from Java conf and {@link ClientReactiveHttpSecurityPostProcessor} to override
+ * anything from the auto-configuration listed above
+ *
+ *
+ * @param http the security filter-chain builder to configure
+ * @param serverProperties Spring Boot standard server properties
+ * @param authorizationRequestResolver the authorization request resolver to use. By default
+ * {@link ServerOAuth2AuthorizationRequestResolver} (adds authorization request parameters
+ * defined in properties and builds absolutes callback URI). By default, a
+ * {@link SpringAddonsServerOAuth2AuthorizationRequestResolver} is used
+ * @param preAuthorizationCodeRedirectStrategy the redirection strategy to use for
+ * authorization-code request
+ * @param authenticationSuccessHandler the authentication success handler to use. By default, a
+ * {@link SpringAddonsOauth2ServerAuthenticationSuccessHandler} is used.
+ * @param authenticationFailureHandler the authentication failure handler to use. By default, a
+ * {@link SpringAddonsOauth2ServerAuthenticationFailureHandler} is used.
+ * @param logoutSuccessHandler Defaulted to {@link SpringAddonsServerLogoutSuccessHandler} which
+ * can handle "almost" RP Initiated Logout conformant OPs (like Auth0 and Cognito)
+ * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties spring-addons client
+ * properties}
+ * @param authorizePostProcessor post process authorization after "permit-all" configuration was
+ * applied (default is "isAuthenticated()" to everything that was not matched)
+ * @param httpPostProcessor post process the "http" builder just before it is returned (enables to
+ * override anything from the auto-configuration) spring-addons client properties}
+ * @param oidcBackChannelLogoutHandler if present, Back-Channel Logout is enabled. A default
+ * {@link OidcBackChannelServerLogoutHandler} is provided if
+ * com.c4-soft.springaddons.oidc.client.back-channel-logout.enabled is true
+ * @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2
+ * clients
+ * @throws Exception in case of miss-configuration
+ */
+ @Order(Ordered.LOWEST_PRECEDENCE - 1)
+ @Bean
+ SecurityWebFilterChain clientFilterChain(ServerHttpSecurity http,
+ ServerProperties serverProperties, SpringAddonsOidcProperties addonsProperties,
+ ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver,
+ PreAuthorizationCodeServerRedirectStrategy preAuthorizationCodeRedirectStrategy,
+ ServerAuthenticationEntryPoint authenticationEntryPoint,
+ ServerAuthenticationSuccessHandler authenticationSuccessHandler,
+ ServerAuthenticationFailureHandler authenticationFailureHandler,
+ ServerLogoutSuccessHandler logoutSuccessHandler,
+ ClientAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
+ ClientReactiveHttpSecurityPostProcessor httpPostProcessor,
+ Optional logoutHandler,
+ Optional oidcBackChannelLogoutHandler) throws Exception {
- final var clientRoutes = addonsProperties
- .getClient()
- .getSecurityMatchers()
- .stream()
- .map(PathPatternParserServerWebExchangeMatcher::new)
- .map(ServerWebExchangeMatcher.class::cast)
- .toList();
- log.info("Applying client OAuth2 configuration for: {}", addonsProperties.getClient().getSecurityMatchers());
- http.securityMatcher(new OrServerWebExchangeMatcher(clientRoutes));
+ final var clientRoutes = addonsProperties.getClient().getSecurityMatchers().stream()
+ .map(PathPatternParserServerWebExchangeMatcher::new)
+ .map(ServerWebExchangeMatcher.class::cast).toList();
+ log.info("Applying client OAuth2 configuration for: {}",
+ addonsProperties.getClient().getSecurityMatchers());
+ http.securityMatcher(new OrServerWebExchangeMatcher(clientRoutes));
- // @formatter:off
- addonsProperties.getClient().getLoginPath().ifPresent(loginPath -> {
- http.exceptionHandling(exceptionHandling -> exceptionHandling
- .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint(UriComponentsBuilder.fromUri(addonsProperties.getClient().getClientUri()).path(loginPath).build().toString())));
+ // @formatter:off
+ http.exceptionHandling(exceptions -> {
+ exceptions.authenticationEntryPoint(authenticationEntryPoint);
});
http.oauth2Login(oauth2 -> {
oauth2.authorizationRequestResolver(authorizationRequestResolver);
oauth2.authorizationRedirectStrategy(preAuthorizationCodeRedirectStrategy);
- authenticationSuccessHandler.ifPresent(oauth2::authenticationSuccessHandler);
- authenticationFailureHandler.ifPresent(oauth2::authenticationFailureHandler);
+ oauth2.authenticationSuccessHandler(authenticationSuccessHandler);
+ oauth2.authenticationFailureHandler(authenticationFailureHandler);
});
http.logout((logout) -> {
- logoutHandler.ifPresent(logout::logoutHandler);
+ logoutHandler.ifPresent(handler -> {
+ if(!(handler instanceof OidcBackChannelServerLogoutHandler)) {
+ logout.logoutHandler(handler);
+ }
+ });
logout.logoutSuccessHandler(logoutSuccessHandler);
});
- if(addonsProperties.getClient().getBackChannelLogout().isEnabled()) {
- http.oidcLogout((logout) -> {
- logout.backChannel(bc -> {
- addonsProperties.getClient().getBackChannelLogout().getInternalLogoutUri().ifPresent(bc::logoutUri);
- });
- });
- }
+ if (oidcBackChannelLogoutHandler.isPresent()) {
+ http.oidcLogout(ol -> ol.backChannel(bc -> bc.logoutHandler(oidcBackChannelLogoutHandler.get())));
+ }
ReactiveConfigurationSupport.configureClient(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
@@ -267,6 +279,12 @@ PreAuthorizationCodeServerRedirectStrategy preAuthorizationCodeRedirectStrategy(
addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode());
}
+ @Conditional(DefaultAuthenticationEntryPointCondition.class)
+ @Bean
+ ServerAuthenticationEntryPoint authenticationEntryPoint(SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsServerAuthenticationEntryPoint(addonsProperties.getClient());
+ }
+
@Conditional(DefaultAuthenticationSuccessHandlerCondition.class)
@Bean
ServerAuthenticationSuccessHandler authenticationSuccessHandler(SpringAddonsOidcProperties addonsProperties) {
@@ -285,13 +303,6 @@ public static class SpringAddonsPreAuthorizationCodeServerRedirectStrategy exten
public SpringAddonsPreAuthorizationCodeServerRedirectStrategy(HttpStatus defaultStatus) {
super(defaultStatus);
}
-
- }
-
- @ConditionalOnMissingBean
- @Bean
- Customizer oidcLogoutSpec() {
- return Customizer.withDefaults();
}
/**
@@ -306,4 +317,19 @@ CorsWebFilter corsFilter(SpringAddonsOidcProperties addonsProperties) {
return ReactiveConfigurationSupport.getCorsFilterBean(corsProps);
}
+
+ @Conditional(DefaultOidcSessionRegistryCondition.class)
+ @Bean
+ ReactiveOidcSessionRegistry oidcSessionRegistry() {
+ return new InMemoryReactiveOidcSessionRegistry();
+ }
+
+ @Conditional(DefaultOidcBackChannelLogoutHandlerCondition.class)
+ @Bean
+ OidcBackChannelServerLogoutHandler oidcBackChannelLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry, SpringAddonsOidcProperties addonsProperties) {
+ OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(sessionRegistry);
+ addonsProperties.getClient().getBackChannelLogout().getInternalLogoutUri().ifPresent(logoutHandler::setLogoutUri);
+ addonsProperties.getClient().getBackChannelLogout().getCookieName().ifPresent(logoutHandler::setSessionCookieName);
+ return logoutHandler;
+ }
}
\ No newline at end of file
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationFailureHandler.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationFailureHandler.java
index 76685fe69..4a21dede4 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationFailureHandler.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationFailureHandler.java
@@ -2,6 +2,8 @@
import java.net.URI;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
@@ -11,6 +13,8 @@
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
+import lombok.extern.slf4j.Slf4j;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
@@ -21,23 +25,36 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsServerOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
+@Slf4j
public class SpringAddonsOauth2ServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
private final URI defaultRedirectUri;
- private final SpringAddonsOauth2ServerRedirectStrategy redirectStrategy;
+ private final HttpStatus postAuthorizationFailureStatus;
public SpringAddonsOauth2ServerAuthenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) {
this.defaultRedirectUri = addonsProperties.getClient().getLoginErrorRedirectPath().orElse(URI.create("/"));
- this.redirectStrategy = new SpringAddonsOauth2ServerRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode());
+ this.postAuthorizationFailureStatus = addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationFailure();
}
@Override
public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
return webFilterExchange.getExchange().getSession().flatMap(session -> {
- final var uri = UriComponentsBuilder.fromUri(
+ final var location = UriComponentsBuilder.fromUri(
session.getAttributeOrDefault(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE, defaultRedirectUri))
.queryParam(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, HtmlUtils.htmlEscape(exception.getMessage()))
- .build().toUri();
- return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), uri);
+ .build().toUri().toString();
+
+ final var response = webFilterExchange.getExchange().getResponse();
+ response.setStatusCode(postAuthorizationFailureStatus);
+ response.getHeaders().add(HttpHeaders.LOCATION, location);
+ response.getHeaders().add(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, exception.getMessage());
+
+ log.debug("Login failure. Status: {}, location: {}, message: {}", postAuthorizationFailureStatus, location, exception.getMessage());
+
+ if (postAuthorizationFailureStatus.is4xxClientError() || postAuthorizationFailureStatus.is5xxServerError()) {
+ final var buffer = response.bufferFactory().wrap(exception.getMessage().getBytes());
+ return response.writeWith(Flux.just(buffer));
+ }
+ return response.setComplete();
});
}
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationSuccessHandler.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationSuccessHandler.java
index 6e3e93e8d..995c83bec 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationSuccessHandler.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerAuthenticationSuccessHandler.java
@@ -9,6 +9,7 @@
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
+import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
/**
@@ -19,6 +20,7 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsServerOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
+@Slf4j
public class SpringAddonsOauth2ServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private final URI defaultRedirectUri;
private final SpringAddonsOauth2ServerRedirectStrategy redirectStrategy;
@@ -33,6 +35,8 @@ public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, A
return webFilterExchange.getExchange().getSession().flatMap(session -> {
final var uri =
session.getAttributeOrDefault(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE, defaultRedirectUri);
+
+ log.debug("Login success. Status: {}, location: {}", redirectStrategy.getDefaultStatus(), uri.toString());
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), uri);
});
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerRedirectStrategy.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerRedirectStrategy.java
index ff024d2f7..15a5852b0 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerRedirectStrategy.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsOauth2ServerRedirectStrategy.java
@@ -13,43 +13,40 @@
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;
/**
- * A redirect strategy that might not actually redirect: the HTTP status is taken from com.c4-soft.springaddons.oidc.client.oauth2-redirect-status property.
- * User-agents will auto redirect only if the status is in 3xx range. This gives single page and mobile applications a chance to intercept the redirection and
- * choose to follow the redirection (or not), with which agent and potentially by clearing some headers.
+ * A redirect strategy that might not actually redirect: the HTTP status is taken from
+ * com.c4-soft.springaddons.oidc.client.oauth2-redirect-status property. User-agents will auto redirect only if the status is in 3xx range.
+ * This gives single page and mobile applications a chance to intercept the redirection and choose to follow the redirection (or not), with
+ * which agent and potentially by clearing some headers.
*
* @author Jerome Wacongne ch4mp@c4-soft.com
*/
@RequiredArgsConstructor
public class SpringAddonsOauth2ServerRedirectStrategy implements ServerRedirectStrategy {
- private final HttpStatus defaultStatus;
-
- @Override
- public Mono sendRedirect(ServerWebExchange exchange, URI location) {
- return Mono.fromRunnable(() -> {
- ServerHttpResponse response = exchange.getResponse();
- final var status = Optional
- .ofNullable(exchange.getRequest().getHeaders().get(SpringAddonsOidcClientProperties.RESPONSE_STATUS_HEADER))
- .map(List::stream)
- .orElse(Stream.empty())
- .filter(StringUtils::hasLength)
- .findAny()
- .map(statusStr -> {
- try {
- final var statusCode = Integer.parseInt(statusStr);
- return HttpStatus.valueOf(statusCode);
- } catch (NumberFormatException e) {
- return HttpStatus.valueOf(statusStr.toUpperCase());
- }
- })
- .orElse(defaultStatus);
- response.setStatusCode(status);
-
- response.getHeaders().setLocation(location);
- });
- }
+ @Getter
+ private final HttpStatus defaultStatus;
+
+ @Override
+ public Mono sendRedirect(ServerWebExchange exchange, URI location) {
+ return Mono.fromRunnable(() -> {
+ ServerHttpResponse response = exchange.getResponse();
+ final var status = Optional.ofNullable(exchange.getRequest().getHeaders().get(SpringAddonsOidcClientProperties.RESPONSE_STATUS_HEADER))
+ .map(List::stream).orElse(Stream.empty()).filter(StringUtils::hasLength).findAny().map(statusStr -> {
+ try {
+ final var statusCode = Integer.parseInt(statusStr);
+ return HttpStatus.valueOf(statusCode);
+ } catch (NumberFormatException e) {
+ return HttpStatus.valueOf(statusStr.toUpperCase());
+ }
+ }).orElse(defaultStatus);
+ response.setStatusCode(status);
+
+ response.getHeaders().setLocation(location);
+ });
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerAuthenticationEntryPoint.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerAuthenticationEntryPoint.java
new file mode 100644
index 000000000..ec4e9bab3
--- /dev/null
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerAuthenticationEntryPoint.java
@@ -0,0 +1,47 @@
+package com.c4_soft.springaddons.security.oidc.starter.reactive.client;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
+
+import lombok.extern.slf4j.Slf4j;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class SpringAddonsServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
+ private final SpringAddonsOidcClientProperties clientProperties;
+
+ public SpringAddonsServerAuthenticationEntryPoint(SpringAddonsOidcClientProperties addonsProperties) {
+ this.clientProperties = addonsProperties;
+ }
+
+ @Override
+ public Mono commence(ServerWebExchange exchange, AuthenticationException ex) {
+ final var location = clientProperties
+ .getLoginUri()
+ .orElse(
+ UriComponentsBuilder.fromUri(clientProperties.getClientUri()).pathSegment(clientProperties.getClientUri().getPath(), "/login").build().toUri())
+ .toString();
+ log.debug("Status: {}, location: {}", clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().value(), location);
+
+ final var response = exchange.getResponse();
+ response.setStatusCode(clientProperties.getOauth2Redirections().getAuthenticationEntryPoint());
+ response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, "OAuth realm=%s".formatted(location));
+ response.getHeaders().add(HttpHeaders.LOCATION, location.toString());
+
+ if (clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().is4xxClientError() || clientProperties
+ .getOauth2Redirections()
+ .getAuthenticationEntryPoint()
+ .is5xxServerError()) {
+ final var buffer = response.bufferFactory().wrap("Unauthorized. Please authenticate at %s".formatted(location.toString()).getBytes());
+ return response.writeWith(Flux.just(buffer));
+ }
+
+ return response.setComplete();
+ }
+}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveJWTClaimsSetAuthenticationManager.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveJWTClaimsSetAuthenticationManager.java
index b8826d742..f17ed318a 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveJWTClaimsSetAuthenticationManager.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveJWTClaimsSetAuthenticationManager.java
@@ -38,7 +38,7 @@
* configuration properties could not be resolved from the JWT claims.
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
public class ReactiveJWTClaimsSetAuthenticationManager implements ReactiveAuthenticationManager {
@@ -81,7 +81,7 @@ public Mono authenticate(Authentication authentication) throws A
* Provider configuration properties could not be resolved from the JWT claims.
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
@RequiredArgsConstructor
public static class ReactiveJWTClaimsSetAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver {
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java
index 38ac02c56..8f9bece9a 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java
@@ -1,12 +1,10 @@
package com.c4_soft.springaddons.security.oidc.starter.reactive.resourceserver;
-import java.nio.charset.Charset;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Map;
-import java.util.Optional;
import java.util.Date;
+import java.util.Map;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
@@ -18,15 +16,11 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.converter.Converter;
-import org.springframework.core.io.buffer.DataBufferUtils;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
@@ -37,11 +31,7 @@
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
-import org.springframework.security.web.AuthenticationEntryPoint;
-import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.server.SecurityWebFilterChain;
-import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
-import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.server.ServerWebExchange;
@@ -68,22 +58,22 @@
/**
*
* Usage
- * If not using spring-boot, @Import or @ComponentScan this class. All beans defined here are @ConditionalOnMissingBean => just define your own
- * @Beans to override.
+ * If not using spring-boot, @Import or @ComponentScan this class. All beans defined here are @ConditionalOnMissingBean =>
+ * just define your own @Beans to override.
*
*
* Provided @Beans
*
*
- * - SecurityWebFilterChain: applies CORS, CSRF, anonymous, sessionCreationPolicy, SSL redirect and 401 instead of redirect to login properties as
- * defined in {@link SpringAddonsOidcProperties}
- * - AuthorizeExchangeSpecPostProcessor. Override if you need fined grained HTTP security (more than authenticated() to all routes but the ones defined
- * as permitAll() in {@link SpringAddonsOidcProperties}
+ * - SecurityWebFilterChain: applies CORS, CSRF, anonymous, sessionCreationPolicy, SSL redirect and 401 instead of redirect to
+ * login properties as defined in {@link SpringAddonsOidcProperties}
+ * - AuthorizeExchangeSpecPostProcessor. Override if you need fined grained HTTP security (more than authenticated() to all routes
+ * but the ones defined as permitAll() in {@link SpringAddonsOidcProperties}
* - Jwt2AuthoritiesConverter: responsible for converting the JWT into Collection<? extends GrantedAuthority>
- * - ReactiveJwt2OpenidClaimSetConverter<T extends Map<String, Object> & Serializable>: responsible for converting the JWT into a
- * claim-set of your choice (OpenID or not)
- * - ReactiveJwt2AuthenticationConverter<OAuthentication<T extends OpenidClaimSet>>: responsible for converting the JWT into an
- * Authentication (uses both beans above)
+ * - ReactiveJwt2OpenidClaimSetConverter<T extends Map<String, Object> & Serializable>: responsible for converting
+ * the JWT into a claim-set of your choice (OpenID or not)
+ * - ReactiveJwt2AuthenticationConverter<OAuthentication<T extends OpenidClaimSet>>: responsible for converting the JWT
+ * into an Authentication (uses both beans above)
* - ReactiveAuthenticationManagerResolver: required to be able to define more than one token issuer until
* https://github.com/spring-projects/spring-boot/issues/30108 is solved
*
@@ -96,277 +86,234 @@
@ImportAutoConfiguration(ReactiveSpringAddonsOidcBeans.class)
public class ReactiveSpringAddonsOidcResourceServerBeans {
- /**
- *
- * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no security matcher and
- * an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers.
- *
- *
- * You should consider to set security matcher to all other {@link SecurityWebFilterChain} beans and provide a
- * {@link ResourceServerReactiveHttpSecurityPostProcessor} bean to override anything from this bean
- *
- * .
- *
- * @param http HTTP security to configure
- * @param serverProperties Spring "server" configuration properties
- * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
- * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
- * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
- * @param authenticationManagerResolver Converts successful JWT decoding result into an {@link Authentication}
- * @param authenticationEntryPoint The {@link AuthenticationEntryPoint} to use (defaults returns 401)
- * @param accessDeniedHandler An optional {@link AccessDeniedHandler} to use instead of Boot default one
- * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with JWT decoder(matches all unmatched routes with lowest precedence)
- */
- @Conditional(IsJwtDecoderResourceServerCondition.class)
- @Order(Ordered.LOWEST_PRECEDENCE)
- @Bean
- SecurityWebFilterChain springAddonsJwtResourceServerSecurityFilterChain(
- ServerHttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
- ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor,
- ReactiveAuthenticationManagerResolver authenticationManagerResolver,
- ServerAuthenticationEntryPoint authenticationEntryPoint,
- Optional accessDeniedHandler) {
- http.oauth2ResourceServer(server -> server.authenticationManagerResolver(authenticationManagerResolver));
-
- ReactiveConfigurationSupport
- .configureResourceServer(
- http,
- serverProperties,
- addonsProperties,
- authenticationEntryPoint,
- accessDeniedHandler,
- authorizePostProcessor,
- httpPostProcessor);
+ /**
+ *
+ * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no
+ * security matcher and an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers.
+ *
+ *
+ * You should consider to set security matcher to all other {@link SecurityWebFilterChain} beans and provide a
+ * {@link ResourceServerReactiveHttpSecurityPostProcessor} bean to override anything from this bean
+ *
+ * .
+ *
+ * @param http HTTP security to configure
+ * @param serverProperties Spring "server" configuration properties
+ * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
+ * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
+ * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
+ * @param authenticationManagerResolver Converts successful JWT decoding result into an {@link Authentication}
+ * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with JWT decoder(matches all
+ * unmatched routes with lowest precedence)
+ */
+ @Conditional(IsJwtDecoderResourceServerCondition.class)
+ @Order(Ordered.LOWEST_PRECEDENCE)
+ @Bean
+ SecurityWebFilterChain springAddonsJwtResourceServerSecurityFilterChain(
+ ServerHttpSecurity http,
+ ServerProperties serverProperties,
+ SpringAddonsOidcProperties addonsProperties,
+ ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
+ ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor,
+ ReactiveAuthenticationManagerResolver authenticationManagerResolver) {
+ http.oauth2ResourceServer(server -> {
+ server.authenticationManagerResolver(authenticationManagerResolver);
+ });
- return http.build();
- }
+ ReactiveConfigurationSupport.configureResourceServer(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
- /**
- *
- * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no security matcher and
- * an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers.
- *
- *
- * You should consider to set security matcher to all other {@link SecurityWebFilterChain} beans and provide a
- * {@link ResourceServerReactiveHttpSecurityPostProcessor} bean to override anything from this bean
- *
- * .
- *
- * @param http HTTP security to configure
- * @param serverProperties Spring "server" configuration properties
- * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
- * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
- * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
- * @param introspectionAuthenticationConverter Converts successful introspection result into an {@link Authentication}
- * @param authenticationEntryPoint The {@link AuthenticationEntryPoint} to use (defaults returns 401)
- * @param accessDeniedHandler An optional {@link AccessDeniedHandler} to use instead of Boot default one
- * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with access-token introspection (matches all unmatched routes with lowest
- * precedence)
- */
- @Conditional(IsIntrospectingResourceServerCondition.class)
- @Order(Ordered.LOWEST_PRECEDENCE)
- @Bean
- SecurityWebFilterChain springAddonsIntrospectingResourceServerSecurityFilterChain(
- ServerHttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
- ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor,
- ReactiveOpaqueTokenAuthenticationConverter introspectionAuthenticationConverter,
- ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector,
- ServerAuthenticationEntryPoint authenticationEntryPoint,
- Optional accessDeniedHandler) {
- http.oauth2ResourceServer(server -> server.opaqueToken(ot -> {
- ot.introspector(opaqueTokenIntrospector);
- ot.authenticationConverter(introspectionAuthenticationConverter);
- }));
+ return http.build();
+ }
- ReactiveConfigurationSupport
- .configureResourceServer(
- http,
- serverProperties,
- addonsProperties,
- authenticationEntryPoint,
- accessDeniedHandler,
- authorizePostProcessor,
- httpPostProcessor);
+ /**
+ *
+ * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no
+ * security matcher and an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers.
+ *
+ *
+ * You should consider to set security matcher to all other {@link SecurityWebFilterChain} beans and provide a
+ * {@link ResourceServerReactiveHttpSecurityPostProcessor} bean to override anything from this bean
+ *
+ * .
+ *
+ * @param http HTTP security to configure
+ * @param serverProperties Spring "server" configuration properties
+ * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
+ * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
+ * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
+ * @param introspectionAuthenticationConverter Converts successful introspection result into an {@link Authentication}
+ * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with access-token
+ * introspection (matches all unmatched routes with lowest precedence)
+ */
+ @Conditional(IsIntrospectingResourceServerCondition.class)
+ @Order(Ordered.LOWEST_PRECEDENCE)
+ @Bean
+ SecurityWebFilterChain springAddonsIntrospectingResourceServerSecurityFilterChain(
+ ServerHttpSecurity http,
+ ServerProperties serverProperties,
+ SpringAddonsOidcProperties addonsProperties,
+ ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
+ ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor,
+ ReactiveOpaqueTokenAuthenticationConverter introspectionAuthenticationConverter,
+ ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector) {
+ http.oauth2ResourceServer(server -> server.opaqueToken(ot -> {
+ ot.introspector(opaqueTokenIntrospector);
+ ot.authenticationConverter(introspectionAuthenticationConverter);
+ }));
- return http.build();
- }
+ ReactiveConfigurationSupport.configureResourceServer(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
- /**
- * Hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
- *
- * @return a hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
- */
- @ConditionalOnMissingBean
- @Bean
- ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor() {
- return (ServerHttpSecurity.AuthorizeExchangeSpec spec) -> spec.anyExchange().authenticated();
- }
+ return http.build();
+ }
- /**
- * Hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that you can modify anything
- *
- * @return a hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that you can modify
- * anything
- */
- @ConditionalOnMissingBean
- @Bean
- ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor() {
- return serverHttpSecurity -> serverHttpSecurity;
- }
+ /**
+ * Hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
+ *
+ * @return a hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor() {
+ return (ServerHttpSecurity.AuthorizeExchangeSpec spec) -> spec.anyExchange().authenticated();
+ }
- @ConditionalOnMissingBean
- @Bean
- SpringAddonsReactiveJwtDecoderFactory springAddonsJwtDecoderFactory() {
- return new DefaultSpringAddonsReactiveJwtDecoderFactory();
- }
+ /**
+ * Hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that you can
+ * modify anything
+ *
+ * @return a hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that
+ * you can modify anything
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ ResourceServerReactiveHttpSecurityPostProcessor httpPostProcessor() {
+ return serverHttpSecurity -> serverHttpSecurity;
+ }
- /**
- * Provides with multi-tenancy: builds a ReactiveAuthenticationManagerResolver per provided OIDC issuer URI
- *
- * @param opPropertiesResolver "com.c4-soft.springaddons.oidc" configuration properties
- * @param jwtDecoderFactory something to build a JWT decoder from OpenID Provider configuration properties
- * @param jwtAuthenticationConverter converts from a {@link Jwt} to an {@link Authentication} implementation
- * @return Multi-tenant {@link ReactiveAuthenticationManagerResolver} (one for each configured issuer)
- */
- @Conditional(DefaultAuthenticationManagerResolverCondition.class)
- @Bean
- ReactiveAuthenticationManagerResolver authenticationManagerResolver(
- OpenidProviderPropertiesResolver opPropertiesResolver,
- SpringAddonsReactiveJwtDecoderFactory jwtDecoderFactory,
- Converter> jwtAuthenticationConverter) {
- return new SpringAddonsReactiveJwtAuthenticationManagerResolver(opPropertiesResolver, jwtDecoderFactory, jwtAuthenticationConverter);
- }
+ @ConditionalOnMissingBean
+ @Bean
+ SpringAddonsReactiveJwtDecoderFactory springAddonsJwtDecoderFactory() {
+ return new DefaultSpringAddonsReactiveJwtDecoderFactory();
+ }
- /**
- * Bean to switch from default behavior of redirecting unauthorized users to login (302) to returning 401 (unauthorized)
- *
- * @return a bean to switch from default behavior of redirecting unauthorized users to login (302) to returning 401 (unauthorized)
- */
- @ConditionalOnMissingBean
- @Bean
- ServerAuthenticationEntryPoint authenticationEntryPoint() {
- return (ServerWebExchange exchange, AuthenticationException ex) -> exchange.getPrincipal().flatMap(principal -> {
- var response = exchange.getResponse();
- response.setStatusCode(HttpStatus.UNAUTHORIZED);
- response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\"");
- var dataBufferFactory = response.bufferFactory();
- var buffer = dataBufferFactory.wrap(ex.getMessage().getBytes(Charset.defaultCharset()));
- return response.writeWith(Mono.just(buffer)).doOnError(error -> DataBufferUtils.release(buffer));
- });
- }
+ /**
+ * Provides with multi-tenancy: builds a ReactiveAuthenticationManagerResolver per provided OIDC issuer URI
+ *
+ * @param opPropertiesResolver "com.c4-soft.springaddons.oidc" configuration properties
+ * @param jwtDecoderFactory something to build a JWT decoder from OpenID Provider configuration properties
+ * @param jwtAuthenticationConverter converts from a {@link Jwt} to an {@link Authentication} implementation
+ * @return Multi-tenant {@link ReactiveAuthenticationManagerResolver} (one for each configured issuer)
+ */
+ @Conditional(DefaultAuthenticationManagerResolverCondition.class)
+ @Bean
+ ReactiveAuthenticationManagerResolver authenticationManagerResolver(
+ OpenidProviderPropertiesResolver opPropertiesResolver,
+ SpringAddonsReactiveJwtDecoderFactory jwtDecoderFactory,
+ Converter> jwtAuthenticationConverter) {
+ return new SpringAddonsReactiveJwtAuthenticationManagerResolver(opPropertiesResolver, jwtDecoderFactory, jwtAuthenticationConverter);
+ }
- /**
- * https://docs.spring.io/spring-security/reference/5.8/migration/reactive.html#_i_am_using_angularjs_or_another_javascript_framework
- */
- @Conditional(CookieCsrfCondition.class)
- @ConditionalOnMissingBean(name = "csrfCookieWebFilter")
- @Bean
- WebFilter csrfCookieWebFilter() {
- return (exchange, chain) -> {
- Mono csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
- return csrfToken.doOnSuccess(token -> {}).then(chain.filter(exchange));
- };
- }
+ /**
+ * https://docs.spring.io/spring-security/reference/5.8/migration/reactive.html#_i_am_using_angularjs_or_another_javascript_framework
+ */
+ @Conditional(CookieCsrfCondition.class)
+ @ConditionalOnMissingBean(name = "csrfCookieWebFilter")
+ @Bean
+ WebFilter csrfCookieWebFilter() {
+ return (exchange, chain) -> {
+ Mono csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
+ return csrfToken.doOnSuccess(token -> {
+ }).then(chain.filter(exchange));
+ };
+ }
- /**
- * Converter bean from {@link Jwt} to {@link AbstractAuthenticationToken}
- *
- * @param authoritiesConverter converts access-token claims into Spring authorities
- * @param opPropertiesResolver "com.c4-soft.springaddons.oidc" configuration properties
- * @return a converter from {@link Jwt} to {@link AbstractAuthenticationToken}
- */
- @Conditional(DefaultJwtAbstractAuthenticationTokenConverterCondition.class)
- @Bean
- ReactiveJwtAbstractAuthenticationTokenConverter jwtAuthenticationConverter(
- Converter, Collection extends GrantedAuthority>> authoritiesConverter,
- OpenidProviderPropertiesResolver opPropertiesResolver) {
- return jwt -> Mono
- .just(
- new JwtAuthenticationToken(
- jwt,
- authoritiesConverter.convert(jwt.getClaims()),
- new OpenidClaimSet(
- jwt.getClaims(),
- opPropertiesResolver
- .resolve(jwt.getClaims())
- .orElseThrow(() -> new NotAConfiguredOpenidProviderException(jwt.getClaims()))
- .getUsernameClaim()).getName()));
- }
+ /**
+ * Converter bean from {@link Jwt} to {@link AbstractAuthenticationToken}
+ *
+ * @param authoritiesConverter converts access-token claims into Spring authorities
+ * @param opPropertiesResolver "com.c4-soft.springaddons.oidc" configuration properties
+ * @return a converter from {@link Jwt} to {@link AbstractAuthenticationToken}
+ */
+ @Conditional(DefaultJwtAbstractAuthenticationTokenConverterCondition.class)
+ @Bean
+ ReactiveJwtAbstractAuthenticationTokenConverter jwtAuthenticationConverter(
+ Converter, Collection extends GrantedAuthority>> authoritiesConverter,
+ OpenidProviderPropertiesResolver opPropertiesResolver) {
+ return jwt -> Mono.just(
+ new JwtAuthenticationToken(
+ jwt,
+ authoritiesConverter.convert(jwt.getClaims()),
+ new OpenidClaimSet(
+ jwt.getClaims(),
+ opPropertiesResolver.resolve(jwt.getClaims()).orElseThrow(() -> new NotAConfiguredOpenidProviderException(jwt.getClaims()))
+ .getUsernameClaim()).getName()));
+ }
- /**
- * Converter bean from successful introspection result to {@link Authentication} instance
- *
- * @param authoritiesConverter converts access-token claims into Spring authorities
- * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
- * @param resourceServerProperties Spring Boot standard resource server configuration properties
- * @return a converter from successful introspection result to {@link Authentication} instance
- */
- @Conditional(DefaultOpaqueTokenAuthenticationConverterCondition.class)
- @Bean
- @SuppressWarnings("unchecked")
- ReactiveOpaqueTokenAuthenticationConverter introspectionAuthenticationConverter(
- Converter, Collection extends GrantedAuthority>> authoritiesConverter,
- SpringAddonsOidcProperties addonsProperties,
- OAuth2ResourceServerProperties resourceServerProperties) {
- return (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> Mono
- .just(
- new BearerTokenAuthentication(
- new OAuth2IntrospectionAuthenticatedPrincipal(
- new OpenidClaimSet(
- authenticatedPrincipal.getAttributes(),
- addonsProperties
- .getOps()
- .stream()
- .filter(issProps -> resourceServerProperties.getOpaquetoken().getIntrospectionUri().contains(issProps.getIss().toString()))
- .findAny()
- .orElse(addonsProperties.getOps().get(0))
- .getUsernameClaim()).getName(),
- authenticatedPrincipal.getAttributes(),
- (Collection) authenticatedPrincipal.getAuthorities()),
- new OAuth2AccessToken(
- OAuth2AccessToken.TokenType.BEARER,
- introspectedToken,
- toInstant(authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT)),
- toInstant(authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP))),
- authoritiesConverter.convert(authenticatedPrincipal.getAttributes())));
- }
+ /**
+ * Converter bean from successful introspection result to {@link Authentication} instance
+ *
+ * @param authoritiesConverter converts access-token claims into Spring authorities
+ * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
+ * @param resourceServerProperties Spring Boot standard resource server configuration properties
+ * @return a converter from successful introspection result to {@link Authentication} instance
+ */
+ @Conditional(DefaultOpaqueTokenAuthenticationConverterCondition.class)
+ @Bean
+ @SuppressWarnings("unchecked")
+ ReactiveOpaqueTokenAuthenticationConverter introspectionAuthenticationConverter(
+ Converter, Collection extends GrantedAuthority>> authoritiesConverter,
+ SpringAddonsOidcProperties addonsProperties,
+ OAuth2ResourceServerProperties resourceServerProperties) {
+ return (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> Mono.just(
+ new BearerTokenAuthentication(
+ new OAuth2IntrospectionAuthenticatedPrincipal(
+ new OpenidClaimSet(
+ authenticatedPrincipal.getAttributes(),
+ addonsProperties.getOps().stream()
+ .filter(
+ issProps -> resourceServerProperties.getOpaquetoken().getIntrospectionUri()
+ .contains(issProps.getIss().toString()))
+ .findAny().orElse(addonsProperties.getOps().get(0)).getUsernameClaim()).getName(),
+ authenticatedPrincipal.getAttributes(),
+ (Collection) authenticatedPrincipal.getAuthorities()),
+ new OAuth2AccessToken(
+ OAuth2AccessToken.TokenType.BEARER,
+ introspectedToken,
+ toInstant(authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT)),
+ toInstant(authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP))),
+ authoritiesConverter.convert(authenticatedPrincipal.getAttributes())));
+ }
- /**
- * FIXME: use only the new CORS properties at next major release
- */
- @Conditional(DefaultCorsWebFilterCondition.class)
- @Bean
- CorsWebFilter corsFilter(SpringAddonsOidcProperties addonsProperties) {
- final var corsProps = new ArrayList<>(addonsProperties.getCors());
- final var deprecatedClientCorsProps = addonsProperties.getResourceserver().getCors();
- corsProps.addAll(deprecatedClientCorsProps);
+ /**
+ * FIXME: use only the new CORS properties at next major release
+ */
+ @Conditional(DefaultCorsWebFilterCondition.class)
+ @Bean
+ CorsWebFilter corsFilter(SpringAddonsOidcProperties addonsProperties) {
+ final var corsProps = new ArrayList<>(addonsProperties.getCors());
+ final var deprecatedClientCorsProps = addonsProperties.getResourceserver().getCors();
+ corsProps.addAll(deprecatedClientCorsProps);
- return ReactiveConfigurationSupport.getCorsFilterBean(corsProps);
- }
+ return ReactiveConfigurationSupport.getCorsFilterBean(corsProps);
+ }
- private static final Instant toInstant(Object claim) {
- if (claim == null) {
- return null;
- }
- if (claim instanceof Instant i) {
- return i;
- }
- if (claim instanceof Date d) {
- return d.toInstant();
- }
- if (claim instanceof Integer i) {
- return Instant.ofEpochSecond((i).longValue());
- } else if (claim instanceof Long l) {
- return Instant.ofEpochSecond(l);
- } else {
- return null;
- }
- }
+ private static final Instant toInstant(Object claim) {
+ if (claim == null) {
+ return null;
+ }
+ if (claim instanceof Instant i) {
+ return i;
+ }
+ if (claim instanceof Date d) {
+ return d.toInstant();
+ }
+ if (claim instanceof Integer i) {
+ return Instant.ofEpochSecond((i).longValue());
+ } else if (claim instanceof Long l) {
+ return Instant.ofEpochSecond(l);
+ } else {
+ return null;
+ }
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/SpringAddonsReactiveJwtAuthenticationManagerResolver.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/SpringAddonsReactiveJwtAuthenticationManagerResolver.java
index c35bbf64b..ec99b7b3c 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/SpringAddonsReactiveJwtAuthenticationManagerResolver.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/SpringAddonsReactiveJwtAuthenticationManagerResolver.java
@@ -23,7 +23,7 @@
* configuration properties could not be resolved from the JWT claims.
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
public class SpringAddonsReactiveJwtAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver {
private final ReactiveJWTClaimsSetAuthenticationManager authenticationManager;
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/ServletConfigurationSupport.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/ServletConfigurationSupport.java
index 9131e6f28..c270f0ce7 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/ServletConfigurationSupport.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/ServletConfigurationSupport.java
@@ -1,13 +1,15 @@
package com.c4_soft.springaddons.security.oidc.starter.synchronised;
import static org.springframework.security.config.Customizer.withDefaults;
-
import java.io.IOException;
+import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
-
+import java.util.stream.Collectors;
import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -23,15 +25,14 @@
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.OncePerRequestFilter;
-
import com.c4_soft.springaddons.security.oidc.starter.properties.CorsProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.Csrf;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
+import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties.OpenidProviderProperties;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.client.ClientExpressionInterceptUrlRegistryPostProcessor;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.client.ClientSynchronizedHttpSecurityPostProcessor;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerSynchronizedHttpSecurityPostProcessor;
-
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -39,185 +40,192 @@
public class ServletConfigurationSupport {
- public static HttpSecurity configureResourceServer(
- HttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
- ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor)
- throws Exception {
-
- ServletConfigurationSupport
- .configureState(http, addonsProperties.getResourceserver().isStatlessSessions(), addonsProperties.getResourceserver().getCsrf());
-
- // FIXME: use only the new CORS properties at next major release
- final var corsProps = new ArrayList<>(addonsProperties.getCors());
- final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
- final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
- corsProps.addAll(deprecatedClientCorsProps);
- corsProps.addAll(deprecatedResourceServerCorsProps);
- ServletConfigurationSupport.configureAccess(http, addonsProperties.getResourceserver().getPermitAll(), corsProps, authorizePostProcessor);
-
- if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
- http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
- }
-
- return httpPostProcessor.process(http);
+ public static HttpSecurity configureResourceServer(HttpSecurity http,
+ ServerProperties serverProperties, SpringAddonsOidcProperties addonsProperties,
+ ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
+ ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor) throws Exception {
+
+ http.exceptionHandling(exceptions -> {
+ final var issuers = addonsProperties.getOps().stream().map(OpenidProviderProperties::getIss)
+ .filter(iss -> iss != null).map(URI::toString)
+ .collect(Collectors.joining(",", "\"", "\""));
+ exceptions.authenticationEntryPoint((request, response, authException) -> {
+ response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "OAuth realm=%s".formatted(issuers));
+ response.sendError(HttpStatus.UNAUTHORIZED.value(),
+ HttpStatus.UNAUTHORIZED.getReasonPhrase());
+ });
+ });
+
+ ServletConfigurationSupport.configureState(http,
+ addonsProperties.getResourceserver().isStatlessSessions(),
+ addonsProperties.getResourceserver().getCsrf());
+
+ // FIXME: use only the new CORS properties at next major release
+ final var corsProps = new ArrayList<>(addonsProperties.getCors());
+ final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
+ final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
+ corsProps.addAll(deprecatedClientCorsProps);
+ corsProps.addAll(deprecatedResourceServerCorsProps);
+ ServletConfigurationSupport.configureAccess(http,
+ addonsProperties.getResourceserver().getPermitAll(), corsProps, authorizePostProcessor);
+
+ if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
+ http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
+ }
+
+ return httpPostProcessor.process(http);
+ }
+
+ public static HttpSecurity configureClient(HttpSecurity http, ServerProperties serverProperties,
+ SpringAddonsOidcProperties addonsProperties,
+ ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
+ ClientSynchronizedHttpSecurityPostProcessor httpPostProcessor) throws Exception {
+
+ ServletConfigurationSupport.configureState(http, false, addonsProperties.getClient().getCsrf());
+
+ // FIXME: use only the new CORS properties at next major release
+ final var corsProps = new ArrayList<>(addonsProperties.getCors());
+ final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
+ final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
+ corsProps.addAll(deprecatedClientCorsProps);
+ corsProps.addAll(deprecatedResourceServerCorsProps);
+ ServletConfigurationSupport.configureAccess(http, addonsProperties.getClient().getPermitAll(),
+ corsProps, authorizePostProcessor);
+
+ if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
+ http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
+ }
+
+ return httpPostProcessor.process(http);
+ }
+
+ public static HttpSecurity configureAccess(HttpSecurity http, List permitAll,
+ List corsProperties,
+ ExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor) throws Exception {
+ final var permittedCorsOptions = corsProperties.stream()
+ .filter(cors -> (cors.getAllowedMethods().contains("*")
+ || cors.getAllowedMethods().contains("OPTIONS")) && !cors.isDisableAnonymousOptions())
+ .map(CorsProperties::getPath).toList();
+
+ if (permitAll.size() > 0 || permittedCorsOptions.size() > 0) {
+ http.anonymous(withDefaults());
}
- public static HttpSecurity configureClient(
- HttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
- ClientSynchronizedHttpSecurityPostProcessor httpPostProcessor)
- throws Exception {
-
- ServletConfigurationSupport.configureState(http, false, addonsProperties.getClient().getCsrf());
-
- // FIXME: use only the new CORS properties at next major release
- final var corsProps = new ArrayList<>(addonsProperties.getCors());
- final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
- final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
- corsProps.addAll(deprecatedClientCorsProps);
- corsProps.addAll(deprecatedResourceServerCorsProps);
- ServletConfigurationSupport.configureAccess(http, addonsProperties.getClient().getPermitAll(), corsProps, authorizePostProcessor);
-
- if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
- http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
- }
-
- return httpPostProcessor.process(http);
+ if (permitAll.size() > 0) {
+ http.authorizeHttpRequests(registry -> registry.requestMatchers(
+ permitAll.stream().map(AntPathRequestMatcher::new).toArray(AntPathRequestMatcher[]::new))
+ .permitAll());
}
- public static HttpSecurity configureAccess(
- HttpSecurity http,
- List permitAll,
- List corsProperties,
- ExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor)
- throws Exception {
- final var permittedCorsOptions = corsProperties
- .stream()
- .filter(cors -> (cors.getAllowedMethods().contains("*") || cors.getAllowedMethods().contains("OPTIONS")) && !cors.isDisableAnonymousOptions())
- .map(CorsProperties::getPath)
- .toList();
-
- if (permitAll.size() > 0 || permittedCorsOptions.size() > 0) {
- http.anonymous(withDefaults());
- }
-
- if (permitAll.size() > 0) {
- http
- .authorizeHttpRequests(
- registry -> registry.requestMatchers(permitAll.stream().map(AntPathRequestMatcher::new).toArray(AntPathRequestMatcher[]::new)).permitAll());
- }
-
- if (permittedCorsOptions.size() > 0) {
- http
- .authorizeHttpRequests(
- registry -> registry
- .requestMatchers(
- permittedCorsOptions
- .stream()
- .map(corsPathPattern -> new AntPathRequestMatcher(corsPathPattern, "OPTIONS"))
- .toArray(AntPathRequestMatcher[]::new))
- .permitAll());
- }
-
- return http.authorizeHttpRequests(registry -> authorizePostProcessor.authorizeHttpRequests(registry));
+ if (permittedCorsOptions.size() > 0) {
+ http.authorizeHttpRequests(registry -> registry.requestMatchers(permittedCorsOptions.stream()
+ .map(corsPathPattern -> new AntPathRequestMatcher(corsPathPattern, "OPTIONS"))
+ .toArray(AntPathRequestMatcher[]::new)).permitAll());
}
- public static CorsFilter getCorsFilterBean(List corsProperties) {
- final var source = new UrlBasedCorsConfigurationSource();
- for (final var corsProps : corsProperties) {
- final var configuration = new CorsConfiguration();
- configuration.setAllowCredentials(corsProps.getAllowCredentials());
- configuration.setAllowedHeaders(corsProps.getAllowedHeaders());
- configuration.setAllowedMethods(corsProps.getAllowedMethods());
- configuration.setAllowedOriginPatterns(corsProps.getAllowedOriginPatterns());
- configuration.setExposedHeaders(corsProps.getExposedHeaders());
- configuration.setMaxAge(corsProps.getMaxAge());
- source.registerCorsConfiguration(corsProps.getPath(), configuration);
- }
- return new CorsFilter(source);
+ return http
+ .authorizeHttpRequests(registry -> authorizePostProcessor.authorizeHttpRequests(registry));
+ }
+
+ public static CorsFilter getCorsFilterBean(List corsProperties) {
+ final var source = new UrlBasedCorsConfigurationSource();
+ for (final var corsProps : corsProperties) {
+ final var configuration = new CorsConfiguration();
+ configuration.setAllowCredentials(corsProps.getAllowCredentials());
+ configuration.setAllowedHeaders(corsProps.getAllowedHeaders());
+ configuration.setAllowedMethods(corsProps.getAllowedMethods());
+ configuration.setAllowedOriginPatterns(corsProps.getAllowedOriginPatterns());
+ configuration.setExposedHeaders(corsProps.getExposedHeaders());
+ configuration.setMaxAge(corsProps.getMaxAge());
+ source.registerCorsConfiguration(corsProps.getPath(), configuration);
}
+ return new CorsFilter(source);
+ }
- public static HttpSecurity configureState(HttpSecurity http, boolean isStatless, Csrf csrfEnum) throws Exception {
-
- if (isStatless) {
- http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
- }
-
- http.csrf(configurer -> {
- switch (csrfEnum) {
- case DISABLE:
- configurer.disable();
- break;
- case DEFAULT:
- if (isStatless) {
- configurer.disable();
- }
- break;
- case SESSION:
- break;
- case COOKIE_ACCESSIBLE_FROM_JS:
- // Taken from
- // https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa-configuration
- configurer.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler());
- http.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
- break;
- }
- });
-
- return http;
+ public static HttpSecurity configureState(HttpSecurity http, boolean isStatless, Csrf csrfEnum)
+ throws Exception {
+
+ if (isStatless) {
+ http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
}
- /**
- * Copied from https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa-configuration
- */
- static final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
- private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
-
- @Override
- public void handle(HttpServletRequest request, HttpServletResponse response, Supplier csrfToken) {
- /*
- * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body.
- */
- this.delegate.handle(request, response, csrfToken);
- }
-
- @Override
- public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
- /*
- * If the request contains a request header, use CsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies when a single-page
- * application includes the header value automatically, which was obtained via a cookie containing the raw CsrfToken.
- */
- final var csrfHeader = request.getHeader(csrfToken.getHeaderName());
- if (StringUtils.hasText(csrfHeader)) {
- return csrfHeader;
- }
- /*
- * In all other cases (e.g. if the request contains a request parameter), use XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This
- * applies when a server-side rendered form includes the _csrf request parameter as a hidden input.
- */
- return this.delegate.resolveCsrfTokenValue(request, csrfToken);
- }
+ http.csrf(configurer -> {
+ switch (csrfEnum) {
+ case DISABLE:
+ configurer.disable();
+ break;
+ case DEFAULT:
+ if (isStatless) {
+ configurer.disable();
+ }
+ break;
+ case SESSION:
+ break;
+ case COOKIE_ACCESSIBLE_FROM_JS:
+ // Taken from
+ // https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa-configuration
+ configurer.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
+ .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler());
+ http.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
+ break;
+ }
+ });
+
+ return http;
+ }
+
+ /**
+ * Copied from
+ * https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa-configuration
+ */
+ static final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
+ private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
+
+ @Override
+ public void handle(HttpServletRequest request, HttpServletResponse response,
+ Supplier csrfToken) {
+ /*
+ * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the
+ * CsrfToken when it is rendered in the response body.
+ */
+ this.delegate.handle(request, response, csrfToken);
}
- /**
- * Copied from https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa-configuration
- */
- static final class CsrfCookieFilter extends OncePerRequestFilter {
-
- @Override
- protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
- throws ServletException,
- IOException {
- CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
- // Render the token value to a cookie by causing the deferred token to be loaded
- csrfToken.getToken();
-
- filterChain.doFilter(request, response);
- }
+ @Override
+ public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
+ /*
+ * If the request contains a request header, use CsrfTokenRequestAttributeHandler to resolve
+ * the CsrfToken. This applies when a single-page application includes the header value
+ * automatically, which was obtained via a cookie containing the raw CsrfToken.
+ */
+ final var csrfHeader = request.getHeader(csrfToken.getHeaderName());
+ if (StringUtils.hasText(csrfHeader)) {
+ return csrfHeader;
+ }
+ /*
+ * In all other cases (e.g. if the request contains a request parameter), use
+ * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies when a
+ * server-side rendered form includes the _csrf request parameter as a hidden input.
+ */
+ return this.delegate.resolveCsrfTokenValue(request, csrfToken);
+ }
+ }
+
+ /**
+ * Copied from
+ * https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa-configuration
+ */
+ static final class CsrfCookieFilter extends OncePerRequestFilter {
+
+ @Override
+ protected void doFilterInternal(@NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
+ throws ServletException, IOException {
+ CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
+ // Render the token value to a cookie by causing the deferred token to be loaded
+ csrfToken.getToken();
+
+ filterChain.doFilter(request, response);
}
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsAuthenticationEntryPoint.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsAuthenticationEntryPoint.java
new file mode 100644
index 000000000..25a831f03
--- /dev/null
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsAuthenticationEntryPoint.java
@@ -0,0 +1,47 @@
+package com.c4_soft.springaddons.security.oidc.starter.synchronised.client;
+
+import java.io.IOException;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class SpringAddonsAuthenticationEntryPoint implements AuthenticationEntryPoint {
+ private final SpringAddonsOidcClientProperties clientProperties;
+
+ public SpringAddonsAuthenticationEntryPoint(SpringAddonsOidcClientProperties addonsProperties) {
+ this.clientProperties = addonsProperties;
+ }
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+ final var location = clientProperties
+ .getLoginUri()
+ .orElse(
+ UriComponentsBuilder.fromUri(clientProperties.getClientUri()).pathSegment(clientProperties.getClientUri().getPath(), "/login").build().toUri())
+ .toString();
+ log.debug("Status: {}, location: {}", clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().value(), location);
+
+ response.setStatus(clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().value());
+ response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "OAuth realm=%s".formatted(location));
+ response.setHeader(HttpHeaders.LOCATION, location.toString());
+
+ if (clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().is4xxClientError() || clientProperties
+ .getOauth2Redirections()
+ .getAuthenticationEntryPoint()
+ .is5xxServerError()) {
+ response.getOutputStream().write("Unauthorized. Please authenticate at %s".formatted(location.toString()).getBytes());
+ }
+
+ response.flushBuffer();
+ }
+}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationFailureHandler.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationFailureHandler.java
index ea5bd8a26..bf40fe3d3 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationFailureHandler.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationFailureHandler.java
@@ -4,6 +4,8 @@
import java.net.URI;
import java.util.Optional;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.util.HtmlUtils;
@@ -15,6 +17,7 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
/**
* An authentication failure handler reading post-login failure URI in session (set by the frontend with a header or request param when
@@ -24,24 +27,36 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
+@Slf4j
public class SpringAddonsOauth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String redirectUri;
- private final SpringAddonsOauth2RedirectStrategy redirectStrategy;
+ private final HttpStatus postAuthorizationFailureStatus;
public SpringAddonsOauth2AuthenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) {
this.redirectUri = addonsProperties.getClient().getLoginErrorRedirectPath().map(URI::toString).orElse("/");
- this.redirectStrategy = new SpringAddonsOauth2RedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode());
+ this.postAuthorizationFailureStatus = addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationFailure();
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException,
ServletException {
- final var uri = UriComponentsBuilder.fromUriString(
+ final var location = UriComponentsBuilder.fromUriString(
Optional.ofNullable(request.getSession().getAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE))
.map(Object::toString).orElse(redirectUri))
.queryParam(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, HtmlUtils.htmlEscape(exception.getMessage())).build()
- .toUri();
- redirectStrategy.sendRedirect(request, response, uri.toString());
+ .toUri().toString();
+
+ log.debug("Authentication failure. Status: {}, location: {}, message: {}", postAuthorizationFailureStatus.value(), location, exception.getMessage());
+
+ response.setStatus(postAuthorizationFailureStatus.value());
+ response.setHeader(HttpHeaders.LOCATION, location);
+ response.setHeader(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, exception.getMessage());
+
+ if (postAuthorizationFailureStatus.is4xxClientError() || postAuthorizationFailureStatus.is5xxServerError()) {
+ response.getOutputStream().write(exception.getMessage().getBytes());
+ }
+
+ response.flushBuffer();
}
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationSuccessHandler.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationSuccessHandler.java
index 2f82b0161..4243941d5 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationSuccessHandler.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2AuthenticationSuccessHandler.java
@@ -13,6 +13,7 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
/**
* An authentication success handler reading post-login success URI in session (set by the frontend with a header or request param when
@@ -22,6 +23,7 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
+@Slf4j
public class SpringAddonsOauth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String redirectUri;
private final SpringAddonsOauth2RedirectStrategy redirectStrategy;
@@ -38,7 +40,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
final var uri =
Optional.ofNullable(request.getSession().getAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE))
.map(Object::toString).orElse(redirectUri);
- redirectStrategy.sendRedirect(request, response, uri);
+ log.debug("Authentication success. Status: {}, location: {}", redirectStrategy.getDefaultStatus(), uri);
+
+ redirectStrategy.sendRedirect(request, response, uri);
}
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2RedirectStrategy.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2RedirectStrategy.java
index f6f36ab79..319e015f8 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2RedirectStrategy.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOauth2RedirectStrategy.java
@@ -10,25 +10,28 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
- * A redirect strategy that might not actually redirect: the HTTP status is taken from com.c4-soft.springaddons.oidc.client.oauth2-redirect-status property.
- * User-agents will auto redirect only if the status is in 3xx range. This gives single page and mobile applications a chance to intercept the redirection and
- * choose to follow the redirection (or not), with which agent and potentially by clearing some headers.
+ * A redirect strategy that might not actually redirect: the HTTP status is taken from
+ * com.c4-soft.springaddons.oidc.client.oauth2-redirect-status property. User-agents will auto redirect only if the status is in 3xx range.
+ * This gives single page and mobile applications a chance to intercept the redirection and choose to follow the redirection (or not), with
+ * which agent and potentially by clearing some headers.
*
* @author Jerome Wacongne ch4mp@c4-soft.com
*/
@RequiredArgsConstructor
public class SpringAddonsOauth2RedirectStrategy implements RedirectStrategy {
- private final HttpStatus defaultStatus;
+ @Getter
+ private final HttpStatus defaultStatus;
- @Override
- public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String location) throws IOException {
- final var requestedStatus = request.getIntHeader(SpringAddonsOidcClientProperties.RESPONSE_STATUS_HEADER);
- response.setStatus(requestedStatus > -1 ? requestedStatus : defaultStatus.value());
+ @Override
+ public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String location) throws IOException {
+ final var requestedStatus = request.getIntHeader(SpringAddonsOidcClientProperties.RESPONSE_STATUS_HEADER);
+ response.setStatus(requestedStatus > -1 ? requestedStatus : defaultStatus.value());
- response.setHeader(HttpHeaders.LOCATION, location);
- }
+ response.setHeader(HttpHeaders.LOCATION, location);
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java
index dadef0a8b..e6a86ec59 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java
@@ -2,7 +2,6 @@
import java.util.ArrayList;
import java.util.Optional;
-
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -14,52 +13,72 @@
import org.springframework.context.annotation.Conditional;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcBackChannelLogoutHandler;
+import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
+import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
+import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.session.InvalidSessionStrategy;
+import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
import org.springframework.web.filter.CorsFilter;
-
+import org.springframework.web.util.UriComponentsBuilder;
import com.c4_soft.springaddons.security.oidc.starter.ClaimSetAuthoritiesConverter;
import com.c4_soft.springaddons.security.oidc.starter.ConfigurableClaimSetAuthoritiesConverter;
import com.c4_soft.springaddons.security.oidc.starter.LogoutRequestUriBuilder;
import com.c4_soft.springaddons.security.oidc.starter.SpringAddonsOAuth2LogoutRequestUriBuilder;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
+import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationEntryPointCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationFailureHandlerCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationSuccessHandlerCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultCorsFilterCondition;
+import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultOidcBackChannelLogoutHandlerCondition;
+import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultOidcSessionRegistryCondition;
import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientWithLoginCondition;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.ServletConfigurationSupport;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.SpringAddonsOidcBeans;
-
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
/**
* The following {@link ConditionalOnMissingBean @ConditionalOnMissingBeans} are auto-configured
*
- * - springAddonsClientFilterChain: a {@link SecurityFilterChain}. Instantiated only if "com.c4-soft.springaddons.oidc.client.security-matchers" property has
- * at least one entry. If defined, it is with highest precedence, to ensure that all routes defined in this security matcher property are intercepted by this
- * filter-chain.
- * - oAuth2AuthorizationRequestResolver: a {@link OAuth2AuthorizationRequestResolver}. Default instance is a
- * {@link SpringAddonsOAuth2AuthorizationRequestResolver} which sets the client hostname in the redirect URI with
- * {@link SpringAddonsOidcClientProperties#clientUri SpringAddonsOidcClientProperties#client-uri}
- * - logoutRequestUriBuilder: builder for RP-Initiated Logout queries, taking
- * configuration from properties for OIDC providers which do not strictly comply with the spec: logout URI not provided by OIDC conf or non standard parameter
- * names (Auth0 and Cognito are samples of such OPs)
- * - logoutSuccessHandler: a {@link LogoutSuccessHandler}. Default instance is a {@link SpringAddonsLogoutSuccessHandler} which logs a user out from the last
- * authorization server he logged on.
- * - authoritiesConverter: an {@link ClaimSetAuthoritiesConverter}. Default instance is a {@link ConfigurableClaimSetAuthoritiesConverter} which reads
- * spring-addons {@link SpringAddonsOidcProperties}
- * - clientAuthorizePostProcessor: a {@link ClientExpressionInterceptUrlRegistryPostProcessor} post processor to fine tune access control from java
- * configuration. It applies to all routes not listed in "permit-all" property configuration. Default requires users to be authenticated.
- * - clientHttpPostProcessor: a {@link ClientSynchronizedHttpSecurityPostProcessor} to override anything from above auto-configuration. It is called just
- * before the security filter-chain is returned. Default is a no-op.
+ * - springAddonsClientFilterChain: a {@link SecurityFilterChain}. Instantiated only if
+ * "com.c4-soft.springaddons.oidc.client.security-matchers" property has at least one entry. If
+ * defined, it is with highest precedence, to ensure that all routes defined in this security
+ * matcher property are intercepted by this filter-chain.
+ * - oAuth2AuthorizationRequestResolver: a {@link OAuth2AuthorizationRequestResolver}. Default
+ * instance is a {@link SpringAddonsOAuth2AuthorizationRequestResolver} which sets the client
+ * hostname in the redirect URI with {@link SpringAddonsOidcClientProperties#clientUri
+ * SpringAddonsOidcClientProperties#client-uri}
+ * - logoutRequestUriBuilder: builder for
+ * RP-Initiated Logout
+ * queries, taking configuration from properties for OIDC providers which do not strictly comply
+ * with the spec: logout URI not provided by OIDC conf or non standard parameter names (Auth0 and
+ * Cognito are samples of such OPs)
+ * - logoutSuccessHandler: a {@link LogoutSuccessHandler}. Default instance is a
+ * {@link SpringAddonsLogoutSuccessHandler} which logs a user out from the last authorization server
+ * he logged on.
+ * - authoritiesConverter: an {@link ClaimSetAuthoritiesConverter}. Default instance is a
+ * {@link ConfigurableClaimSetAuthoritiesConverter} which reads spring-addons
+ * {@link SpringAddonsOidcProperties}
+ * - clientAuthorizePostProcessor: a {@link ClientExpressionInterceptUrlRegistryPostProcessor}
+ * post processor to fine tune access control from java configuration. It applies to all routes not
+ * listed in "permit-all" property configuration. Default requires users to be authenticated.
+ * - clientHttpPostProcessor: a {@link ClientSynchronizedHttpSecurityPostProcessor} to override
+ * anything from above auto-configuration. It is called just before the security filter-chain is
+ * returned. Default is a no-op.
*
*
* @author Jerome Wacongne ch4mp@c4-soft.com
@@ -72,67 +91,92 @@
@Slf4j
public class SpringAddonsOidcClientWithLoginBeans {
- /**
- *
- * Instantiated only if "com.c4-soft.springaddons.oidc.client.security-matchers" property has at least one entry. If defined, it is with higher precedence
- * than resource server one.
- *
- * It defines:
- *
- * - If the path to login page was provided in conf, a @Controller must be provided to handle it. Otherwise Spring Boot default generated one is used
- * (be aware that it does not work when bound to 80 or 8080 with SSL enabled, so, in that case, use another port or define a login path and a controller to
- * handle it)
- * - logout (using {@link SpringAddonsLogoutSuccessHandler} by default)
- * - forces SSL usage if it is enabled
properties
- * - CSRF protection as defined in spring-addons client properties (enabled by default in this filter-chain).
- * - allow access to unauthorized requests to path matchers listed in spring-security client "permit-all" property
- * - as usual, apply {@link ClientExpressionInterceptUrlRegistryPostProcessor} for access control configuration from Java conf and
- * {@link ClientSynchronizedHttpSecurityPostProcessor} to override anything from the auto-configuration listed above
- *
- *
- * @param http the security filter-chain builder to configure
- * @param serverProperties Spring Boot standard server properties
- * @param authorizationRequestResolver the authorization request resolver to use. By default {@link SpringAddonsOAuth2AuthorizationRequestResolver} (adds
- * authorization request parameters defined in properties and builds absolutes callback URI)
- * @param preAuthorizationCodeRedirectStrategy the redirection strategy to use for authorization-code request
- * @param authenticationSuccessHandler the authentication success handler to use. Default is a {@link SpringAddonsOauth2AuthenticationSuccessHandler}
- * @param authenticationFailureHandler the authentication failure handler to use. Default is a {@link SpringAddonsOauth2AuthenticationFailureHandler}
- * @param logoutSuccessHandler Defaulted to {@link SpringAddonsLogoutSuccessHandler} which can handle "almost" RP Initiated Logout conformant OPs (like
- * Auth0 and Cognito). Default is a {@link SpringAddonsLogoutSuccessHandler}
- * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties spring-addons client properties}
- * @param authorizePostProcessor post process authorization after "permit-all" configuration was applied (default is "isAuthenticated()" to everything that
- * was not matched)
- * @param httpPostProcessor post process the "http" builder just before it is returned (enables to override anything from the auto-configuration)
- * spring-addons client properties}
- * @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2 clients
- * @throws Exception in case of miss-configuration
- */
- @Order(Ordered.LOWEST_PRECEDENCE - 1)
- @Bean
- SecurityFilterChain springAddonsClientFilterChain(
- HttpSecurity http,
- ServerProperties serverProperties,
- PreAuthorizationCodeRedirectStrategy preAuthorizationCodeRedirectStrategy,
- OAuth2AuthorizationRequestResolver authorizationRequestResolver,
- Optional authenticationSuccessHandler,
- Optional authenticationFailureHandler,
- LogoutSuccessHandler logoutSuccessHandler,
- SpringAddonsOidcProperties addonsProperties,
- ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
- ClientSynchronizedHttpSecurityPostProcessor httpPostProcessor)
- throws Exception {
- // @formatter:off
+ /**
+ *
+ * Instantiated only if "com.c4-soft.springaddons.oidc.client.security-matchers" property has at
+ * least one entry. If defined, it is with higher precedence than resource server one.
+ *
+ * It defines:
+ *
+ * - If the path to login page was provided in conf, a @Controller must be provided to
+ * handle it. Otherwise Spring Boot default generated one is used (be aware that it does not work
+ * when bound to 80 or 8080 with SSL enabled, so, in that case, use another port or define a login
+ * path and a controller to handle it)
+ * - logout (using {@link SpringAddonsLogoutSuccessHandler} by default)
+ * - forces SSL usage if it is enabled
properties
+ * - CSRF protection as defined in spring-addons client properties (enabled by default in
+ * this filter-chain).
+ * - allow access to unauthorized requests to path matchers listed in spring-security
+ * client "permit-all" property
+ * - as usual, apply {@link ClientExpressionInterceptUrlRegistryPostProcessor} for access
+ * control configuration from Java conf and {@link ClientSynchronizedHttpSecurityPostProcessor} to
+ * override anything from the auto-configuration listed above
+ *
+ *
+ * @param http the security filter-chain builder to configure
+ * @param serverProperties Spring Boot standard server properties
+ * @param authorizationRequestResolver the authorization request resolver to use. By default
+ * {@link SpringAddonsOAuth2AuthorizationRequestResolver} (adds authorization request
+ * parameters defined in properties and builds absolutes callback URI)
+ * @param preAuthorizationCodeRedirectStrategy the redirection strategy to use for
+ * authorization-code request
+ * @param authenticationEntryPoint the {@link AuthenticationEntryPoint} to use. Default is
+ * {@link SpringAddonsAuthenticationEntryPoint}
+ * @param authenticationSuccessHandler the authentication success handler to use. Default is a
+ * {@link SpringAddonsOauth2AuthenticationSuccessHandler}
+ * @param authenticationFailureHandler the authentication failure handler to use. Default is a
+ * {@link SpringAddonsOauth2AuthenticationFailureHandler}
+ * @param invalidSessionStrategy default redirects to login, unless another status is set in
+ * com.c4-soft.springaddons.oidc.client.oauth2-redirections.invalid-session-strategy
+ * @param logoutSuccessHandler Defaulted to {@link SpringAddonsLogoutSuccessHandler} which can
+ * handle "almost" RP Initiated Logout conformant OPs (like Auth0 and Cognito). Default is
+ * a {@link SpringAddonsLogoutSuccessHandler}
+ * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties spring-addons client
+ * properties}
+ * @param authorizePostProcessor post process authorization after "permit-all" configuration was
+ * applied (default is "isAuthenticated()" to everything that was not matched)
+ * @param httpPostProcessor post process the "http" builder just before it is returned (enables to
+ * override anything from the auto-configuration) spring-addons client properties}
+ * @param oidcBackChannelLogoutHandler if present, Back-Channel Logout is enabled. A default
+ * {@link OidcBackChannelLogoutHandler} is provided if
+ * com.c4-soft.springaddons.oidc.client.back-channel-logout.enabled is true
+ * @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2
+ * clients
+ * @throws Exception in case of miss-configuration
+ */
+ @Order(Ordered.LOWEST_PRECEDENCE - 1)
+ @Bean
+ SecurityFilterChain springAddonsClientFilterChain(HttpSecurity http,
+ ServerProperties serverProperties,
+ PreAuthorizationCodeRedirectStrategy preAuthorizationCodeRedirectStrategy,
+ OAuth2AuthorizationRequestResolver authorizationRequestResolver,
+ AuthenticationEntryPoint authenticationEntryPoint,
+ AuthenticationSuccessHandler authenticationSuccessHandler,
+ AuthenticationFailureHandler authenticationFailureHandler,
+ InvalidSessionStrategy invalidSessionStrategy, Optional logoutHandler,
+ LogoutSuccessHandler logoutSuccessHandler, SpringAddonsOidcProperties addonsProperties,
+ ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
+ ClientSynchronizedHttpSecurityPostProcessor httpPostProcessor,
+ Optional oidcBackChannelLogoutHandler) throws Exception {
+ // @formatter:off
log.info("Applying client OAuth2 configuration for: {}", addonsProperties.getClient().getSecurityMatchers());
http.securityMatcher(addonsProperties.getClient().getSecurityMatchers().toArray(new String[] {}));
+ http.sessionManagement(sessions -> {
+ sessions.invalidSessionStrategy(invalidSessionStrategy);
+ });
+
+ http.exceptionHandling(exceptions -> {
+ exceptions.authenticationEntryPoint(authenticationEntryPoint);
+ });
+
http.oauth2Login(login -> {
login.authorizationEndpoint(authorizationEndpoint -> {
authorizationEndpoint.authorizationRedirectStrategy(preAuthorizationCodeRedirectStrategy);
authorizationEndpoint.authorizationRequestResolver(authorizationRequestResolver);
});
- addonsProperties.getClient().getLoginPath().ifPresent(login::loginPage);
- authenticationSuccessHandler.ifPresent(login::successHandler);
- authenticationFailureHandler.ifPresent(login::failureHandler);
+ login.successHandler(authenticationSuccessHandler);
+ login.failureHandler(authenticationFailureHandler);
});
http.logout(logout -> {
@@ -140,127 +184,191 @@ SecurityFilterChain springAddonsClientFilterChain(
});
// @formatter:on
- if (addonsProperties.getClient().getBackChannelLogout().isEnabled()) {
- http.oidcLogout(ol -> {
- ol.backChannel(bc -> {
- addonsProperties.getClient().getBackChannelLogout().getInternalLogoutUri().ifPresent(bc::logoutUri);
- });
- });
- }
+ if (oidcBackChannelLogoutHandler.isPresent()) {
+ http.oidcLogout(
+ ol -> ol.backChannel(bc -> bc.logoutHandler(oidcBackChannelLogoutHandler.get())));
+ }
- ServletConfigurationSupport.configureClient(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
+ ServletConfigurationSupport.configureClient(http, serverProperties, addonsProperties,
+ authorizePostProcessor, httpPostProcessor);
- return http.build();
- }
+ return http.build();
+ }
- /**
- * Use a {@link SpringAddonsOAuth2AuthorizationRequestResolver} which:
- *
- * - takes hostname and port from configuration properties (and works even if SSL is enabled on port 8080)
- * - spport defining additionl authorization request parameters from properties
- *
- *
- * @param bootClientProperties "standard" Spring Boot OAuth2 client properties
- * @param clientRegistrationRepository
- * @param addonsProperties "spring-addons" OAuth2 client properties
- * @return {@link SpringAddonsOAuth2AuthorizationRequestResolver}
- */
- @ConditionalOnMissingBean
- @Bean
- OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver(
- OAuth2ClientProperties bootClientProperties,
- ClientRegistrationRepository clientRegistrationRepository,
- SpringAddonsOidcProperties addonsProperties) {
- return new SpringAddonsOAuth2AuthorizationRequestResolver(bootClientProperties, clientRegistrationRepository, addonsProperties.getClient());
- }
+ /**
+ * Use a {@link SpringAddonsOAuth2AuthorizationRequestResolver} which:
+ *
+ * - takes hostname and port from configuration properties (and works even if SSL is enabled on
+ * port 8080)
+ * - spport defining additionl authorization request parameters from properties
+ *
+ *
+ * @param bootClientProperties "standard" Spring Boot OAuth2 client properties
+ * @param clientRegistrationRepository
+ * @param addonsProperties "spring-addons" OAuth2 client properties
+ * @return {@link SpringAddonsOAuth2AuthorizationRequestResolver}
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver(
+ OAuth2ClientProperties bootClientProperties,
+ ClientRegistrationRepository clientRegistrationRepository,
+ SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsOAuth2AuthorizationRequestResolver(bootClientProperties,
+ clientRegistrationRepository, addonsProperties.getClient());
+ }
- /**
- * Build logout request for RP-Initiated Logout. It works with most OIDC
- * provider: those complying with the spec (Keycloak for instance), off course, but also those which are close enough to it (Auth0, Cognito, ...)
- *
- * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties} to pick logout configuration for divergence to the standard (logout URI not provided
- * in .well-known/openid-configuration and non-conform parameter names)
- * @return {@link SpringAddonsOAuth2LogoutRequestUriBuilder]
- */
- @ConditionalOnMissingBean
- @Bean
- LogoutRequestUriBuilder logoutRequestUriBuilder(SpringAddonsOidcProperties addonsProperties) {
- return new SpringAddonsOAuth2LogoutRequestUriBuilder(addonsProperties.getClient());
- }
+ /**
+ * Build logout request for
+ * RP-Initiated
+ * Logout. It works with most OIDC provider: those complying with the spec (Keycloak for
+ * instance), off course, but also those which are close enough to it (Auth0, Cognito, ...)
+ *
+ * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties} to pick logout configuration
+ * for divergence to the standard (logout URI not provided in
+ * .well-known/openid-configuration and non-conform parameter names)
+ * @return {@link SpringAddonsOAuth2LogoutRequestUriBuilder]
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ LogoutRequestUriBuilder logoutRequestUriBuilder(SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsOAuth2LogoutRequestUriBuilder(addonsProperties.getClient());
+ }
- /**
- * Single tenant logout handler for OIDC provider complying to RP-Initiated
- * Logout (or approximately complying to it like Auth0 or Cognito)
- *
- * @param logoutRequestUriBuilder delegate doing the smart job
- * @param clientRegistrationRepository
- * @param addonsProperties
- * @return {@link SpringAddonsLogoutSuccessHandler}
- */
- @ConditionalOnMissingBean
- @Bean
- LogoutSuccessHandler logoutSuccessHandler(
- LogoutRequestUriBuilder logoutRequestUriBuilder,
- ClientRegistrationRepository clientRegistrationRepository,
- SpringAddonsOidcProperties addonsProperties) {
- return new SpringAddonsLogoutSuccessHandler(logoutRequestUriBuilder, clientRegistrationRepository, addonsProperties);
- }
+ /**
+ * Single tenant logout handler for OIDC provider complying to
+ * RP-Initiated Logout
+ * (or approximately complying to it like Auth0 or Cognito)
+ *
+ * @param logoutRequestUriBuilder delegate doing the smart job
+ * @param clientRegistrationRepository
+ * @param addonsProperties
+ * @return {@link SpringAddonsLogoutSuccessHandler}
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ LogoutSuccessHandler logoutSuccessHandler(LogoutRequestUriBuilder logoutRequestUriBuilder,
+ ClientRegistrationRepository clientRegistrationRepository,
+ SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsLogoutSuccessHandler(logoutRequestUriBuilder,
+ clientRegistrationRepository, addonsProperties);
+ }
- /**
- * @return a Post processor for access control in Java configuration which requires users to be authenticated. It is called after "permit-all" configuration
- * property was applied.
- */
- @ConditionalOnMissingBean
- @Bean
- ClientExpressionInterceptUrlRegistryPostProcessor clientAuthorizePostProcessor() {
- return registry -> registry.anyRequest().authenticated();
- }
+ /**
+ * @return a Post processor for access control in Java configuration which requires users to be
+ * authenticated. It is called after "permit-all" configuration property was applied.
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ ClientExpressionInterceptUrlRegistryPostProcessor clientAuthorizePostProcessor() {
+ return registry -> registry.anyRequest().authenticated();
+ }
- /**
- * @return a no-op post processor
- */
- @ConditionalOnMissingBean
- @Bean
- ClientSynchronizedHttpSecurityPostProcessor clientHttpPostProcessor() {
- return http -> http;
- }
+ /**
+ * @return a no-op post processor
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ ClientSynchronizedHttpSecurityPostProcessor clientHttpPostProcessor() {
+ return http -> http;
+ }
- @ConditionalOnMissingBean
- @Bean
- PreAuthorizationCodeRedirectStrategy authorizationCodeRedirectStrategy(SpringAddonsOidcProperties addonsProperties) {
- return new SpringAddonsPreAuthorizationCodeRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode());
- }
+ @ConditionalOnMissingBean
+ @Bean
+ PreAuthorizationCodeRedirectStrategy authorizationCodeRedirectStrategy(
+ SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsPreAuthorizationCodeRedirectStrategy(
+ addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode());
+ }
- public static class SpringAddonsPreAuthorizationCodeRedirectStrategy extends SpringAddonsOauth2RedirectStrategy
- implements
- PreAuthorizationCodeRedirectStrategy {
- public SpringAddonsPreAuthorizationCodeRedirectStrategy(HttpStatus defaultStatus) {
- super(defaultStatus);
- }
+ public static class SpringAddonsPreAuthorizationCodeRedirectStrategy
+ extends SpringAddonsOauth2RedirectStrategy implements PreAuthorizationCodeRedirectStrategy {
+ public SpringAddonsPreAuthorizationCodeRedirectStrategy(HttpStatus defaultStatus) {
+ super(defaultStatus);
}
+ }
- @Conditional(DefaultAuthenticationSuccessHandlerCondition.class)
- @Bean
- AuthenticationSuccessHandler authenticationSuccessHandler(SpringAddonsOidcProperties addonsProperties) {
- return new SpringAddonsOauth2AuthenticationSuccessHandler(addonsProperties);
- }
+ @ConditionalOnMissingBean(InvalidSessionStrategy.class)
+ @Bean
+ InvalidSessionStrategy invalidSessionStrategy(SpringAddonsOidcProperties addonsProperties) {
+ final var location = addonsProperties.getClient().getLoginUri()
+ .orElse(UriComponentsBuilder.fromUri(addonsProperties.getClient().getClientUri())
+ .pathSegment(addonsProperties.getClient().getClientUri().getPath(), "/login").build()
+ .toUri())
+ .toString();
+ log.debug("Invalid session. Returning %d and request authentication at %s".formatted(
+ addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy().value(),
+ location));
- @Conditional(DefaultAuthenticationFailureHandlerCondition.class)
- @Bean
- AuthenticationFailureHandler authenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) {
- return new SpringAddonsOauth2AuthenticationFailureHandler(addonsProperties);
+ if (addonsProperties.getClient().getOauth2Redirections()
+ .getInvalidSessionStrategy() == HttpStatus.FOUND) {
+ return new SimpleRedirectInvalidSessionStrategy(location);
}
- /**
- * FIXME: use only the new CORS properties at next major release
- */
- @Conditional(DefaultCorsFilterCondition.class)
- @Bean
- CorsFilter corsFilter(SpringAddonsOidcProperties addonsProperties) {
- final var corsProps = new ArrayList<>(addonsProperties.getCors());
- final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
- corsProps.addAll(deprecatedClientCorsProps);
+ return (HttpServletRequest request, HttpServletResponse response) -> {
+ response.setStatus(
+ addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy().value());
+ response.setHeader(HttpHeaders.LOCATION, location);
+ if (addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy()
+ .is4xxClientError()
+ || addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy()
+ .is5xxServerError()) {
+ response.getOutputStream()
+ .write("Invalid session. Please authenticate at %s".formatted(location).getBytes());
+ }
+ response.flushBuffer();
+ };
+ }
+
+ @Conditional(DefaultAuthenticationEntryPointCondition.class)
+ @Bean
+ AuthenticationEntryPoint authenticationEntryPoint(SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsAuthenticationEntryPoint(addonsProperties.getClient());
+ }
+
+ @Conditional(DefaultAuthenticationSuccessHandlerCondition.class)
+ @Bean
+ AuthenticationSuccessHandler authenticationSuccessHandler(
+ SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsOauth2AuthenticationSuccessHandler(addonsProperties);
+ }
+
+ @Conditional(DefaultAuthenticationFailureHandlerCondition.class)
+ @Bean
+ AuthenticationFailureHandler authenticationFailureHandler(
+ SpringAddonsOidcProperties addonsProperties) {
+ return new SpringAddonsOauth2AuthenticationFailureHandler(addonsProperties);
+ }
+
+ /**
+ * FIXME: use only the new CORS properties at next major release
+ */
+ @Conditional(DefaultCorsFilterCondition.class)
+ @Bean
+ CorsFilter corsFilter(SpringAddonsOidcProperties addonsProperties) {
+ final var corsProps = new ArrayList<>(addonsProperties.getCors());
+ final var deprecatedClientCorsProps = addonsProperties.getClient().getCors();
+ corsProps.addAll(deprecatedClientCorsProps);
+
+ return ServletConfigurationSupport.getCorsFilterBean(corsProps);
+ }
+
+ @Conditional(DefaultOidcSessionRegistryCondition.class)
+ @Bean
+ OidcSessionRegistry oidcSessionRegistry() {
+ return new InMemoryOidcSessionRegistry();
+ }
+
+ @Conditional(DefaultOidcBackChannelLogoutHandlerCondition.class)
+ @Bean
+ OidcBackChannelLogoutHandler oidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry,
+ SpringAddonsOidcProperties addonsProperties) {
+ OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry);
+ addonsProperties.getClient().getBackChannelLogout().getInternalLogoutUri()
+ .ifPresent(logoutHandler::setLogoutUri);
+ addonsProperties.getClient().getBackChannelLogout().getCookieName()
+ .ifPresent(logoutHandler::setSessionCookieName);
+ return logoutHandler;
+ }
- return ServletConfigurationSupport.getCorsFilterBean(corsProps);
- }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/JWTClaimsSetAuthenticationManager.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/JWTClaimsSetAuthenticationManager.java
index dbb71a585..a04a10e80 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/JWTClaimsSetAuthenticationManager.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/JWTClaimsSetAuthenticationManager.java
@@ -35,7 +35,7 @@
* properties could not be resolved from the JWT claims.
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
public class JWTClaimsSetAuthenticationManager implements AuthenticationManager {
@@ -77,7 +77,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
* properties could not be resolved from the JWT claims.
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
@RequiredArgsConstructor
public static class JWTClaimsSetAuthenticationManagerResolver implements AuthenticationManagerResolver {
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsJwtAuthenticationManagerResolver.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsJwtAuthenticationManagerResolver.java
index f088a5b5d..c0c42de5a 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsJwtAuthenticationManagerResolver.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsJwtAuthenticationManagerResolver.java
@@ -22,7 +22,7 @@
* properties could not be resolved from the JWT claims.
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
public class SpringAddonsJwtAuthenticationManagerResolver implements AuthenticationManagerResolver {
private final JWTClaimsSetAuthenticationManager authenticationManager;
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsOidcResourceServerBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsOidcResourceServerBeans.java
index 795c3510f..6ed9e5341 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsOidcResourceServerBeans.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/resourceserver/SpringAddonsOidcResourceServerBeans.java
@@ -1,12 +1,11 @@
package com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver;
-import java.sql.Date;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Date;
import java.util.Map;
-import java.util.Optional;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
@@ -20,8 +19,6 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.converter.Converter;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -39,9 +36,7 @@
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
-import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.filter.CorsFilter;
@@ -64,28 +59,29 @@
/**
*
* Usage
- * If not using spring-boot, @Import or @ComponentScan this class. All beans defined here are @ConditionalOnMissingBean => just define your own
- * @Beans to override.
+ * If not using spring-boot, @Import or @ComponentScan this class. All beans defined here are @ConditionalOnMissingBean =>
+ * just define your own @Beans to override.
*
*
* Provided @Beans
*
*
- * - springAddonsResourceServerSecurityFilterChain: applies CORS, CSRF, anonymous, sessionCreationPolicy, SSL, redirect and 401 instead of redirect to login
- * as defined in springAddonsResourceServerSecurityFilterChain: applies CORS, CSRF, anonymous, sessionCreationPolicy, SSL, redirect and 401 instead of
+ * redirect to login as defined in SpringAddonsSecurityProperties
- * - authorizePostProcessor: a bean of type {@link ResourceServerExpressionInterceptUrlRegistryPostProcessor} to fine tune access control from java
- * configuration. It applies to all routes not listed in "permit-all" property configuration. Default requires users to be authenticated. This is a bean to
- * provide in your application configuration if you prefer to define fine-grained access control rules with Java configuration rather than methods
- * security.
- * - httpPostProcessor: a bean of type {@link ResourceServerSynchronizedHttpSecurityPostProcessor} to override anything from above auto-configuration. It is
- * called just before the security filter-chain is returned. Default is a no-op.
- * - jwtAuthenticationConverter: a converter from a {@link Jwt} to something inheriting from {@link AbstractAuthenticationToken}. The default instantiate a
- * {@link JwtAuthenticationToken} with username and authorities as configured for the issuer of thi token. The easiest to override the type of
- * {@link AbstractAuthenticationToken}, is to provide with an Converter<Jwt, ? extends AbstractAuthenticationToken> bean.
+ * - authorizePostProcessor: a bean of type {@link ResourceServerExpressionInterceptUrlRegistryPostProcessor} to fine tune access control
+ * from java configuration. It applies to all routes not listed in "permit-all" property configuration. Default requires users to be
+ * authenticated. This is a bean to provide in your application configuration if you prefer to define fine-grained access control rules
+ * with Java configuration rather than methods security.
+ * - httpPostProcessor: a bean of type {@link ResourceServerSynchronizedHttpSecurityPostProcessor} to override anything from above
+ * auto-configuration. It is called just before the security filter-chain is returned. Default is a no-op.
+ * - jwtAuthenticationConverter: a converter from a {@link Jwt} to something inheriting from {@link AbstractAuthenticationToken}. The
+ * default instantiate a {@link JwtAuthenticationToken} with username and authorities as configured for the issuer of thi token. The easiest
+ * to override the type of {@link AbstractAuthenticationToken}, is to provide with an Converter<Jwt, ? extends
+ * AbstractAuthenticationToken> bean.
* - authenticationManagerResolver: to accept authorities from more than one issuer, the recommended way is to provide an
- * {@link AuthenticationManagerResolver} supporting it. Default keeps a {@link JwtAuthenticationProvider} with its own {@link JwtDecoder}
- * for each issuer.
+ * {@link AuthenticationManagerResolver} supporting it. Default keeps a {@link JwtAuthenticationProvider} with its own
+ * {@link JwtDecoder} for each issuer.
*
*
* @author Jerome Wacongne ch4mp@c4-soft.com
@@ -96,230 +92,213 @@
@AutoConfiguration
@ImportAutoConfiguration(SpringAddonsOidcBeans.class)
public class SpringAddonsOidcResourceServerBeans {
- /**
- *
- * Configures a SecurityFilterChain for a resource server with JwtDecoder with @Order(LOWEST_PRECEDENCE). Defining a {@link SecurityWebFilterChain} bean
- * with no security matcher and an order higher than LOWEST_PRECEDENCE will hide this filter-chain an disable most of spring-addons auto-configuration for
- * OpenID resource-servers.
- *
- *
- * @param http HTTP security to configure
- * @param serverProperties Spring "server" configuration properties
- * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
- * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
- * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
- * @param authenticationManagerResolver Converts successful JWT decoding result into an {@link Authentication}
- * @return A {@link SecurityWebFilterChain} for servlet resource-servers with JWT decoder
- */
- @Conditional(IsJwtDecoderResourceServerCondition.class)
- @Order(Ordered.LOWEST_PRECEDENCE)
- @Bean
- SecurityFilterChain springAddonsJwtResourceServerSecurityFilterChain(
- HttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
- ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor,
- AuthenticationManagerResolver authenticationManagerResolver,
- AuthenticationEntryPoint authenticationEntryPoint,
- Optional accessDeniedHandler)
- throws Exception {
- http.oauth2ResourceServer(oauth2 -> {
- oauth2.authenticationManagerResolver(authenticationManagerResolver);
- oauth2.authenticationEntryPoint(authenticationEntryPoint);
- accessDeniedHandler.ifPresent(oauth2::accessDeniedHandler);
- });
+ /**
+ *
+ * Configures a SecurityFilterChain for a resource server with JwtDecoder with @Order(LOWEST_PRECEDENCE). Defining a
+ * {@link SecurityWebFilterChain} bean with no security matcher and an order higher than LOWEST_PRECEDENCE will hide this filter-chain an
+ * disable most of spring-addons auto-configuration for OpenID resource-servers.
+ *
+ *
+ * @param http HTTP security to configure
+ * @param serverProperties Spring "server" configuration properties
+ * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
+ * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
+ * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
+ * @param authenticationManagerResolver Converts successful JWT decoding result into an {@link Authentication}
+ * @return A {@link SecurityWebFilterChain} for servlet resource-servers with JWT decoder
+ */
+ @Conditional(IsJwtDecoderResourceServerCondition.class)
+ @Order(Ordered.LOWEST_PRECEDENCE)
+ @Bean
+ SecurityFilterChain springAddonsJwtResourceServerSecurityFilterChain(
+ HttpSecurity http,
+ ServerProperties serverProperties,
+ SpringAddonsOidcProperties addonsProperties,
+ ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
+ ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor,
+ AuthenticationManagerResolver authenticationManagerResolver)
+ throws Exception {
+ http.oauth2ResourceServer(oauth2 -> {
+ oauth2.authenticationManagerResolver(authenticationManagerResolver);
+ });
- ServletConfigurationSupport.configureResourceServer(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
+ ServletConfigurationSupport.configureResourceServer(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
- return http.build();
- }
+ return http.build();
+ }
- /**
- *
- * Configures a SecurityFilterChain for a resource server with JwtDecoder with @Order(LOWEST_PRECEDENCE). Defining a {@link SecurityWebFilterChain} bean
- * with no security matcher and an order higher than LOWEST_PRECEDENCE will hide this filter-chain an disable most of spring-addons auto-configuration for
- * OpenID resource-servers.
- *
- *
- * @param http HTTP security to configure
- * @param serverProperties Spring "server" configuration properties
- * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
- * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
- * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
- * @param introspectionAuthenticationConverter Converts successful introspection result into an {@link Authentication}
- * @param opaqueTokenIntrospector the instrospector to use
- * @return A {@link SecurityWebFilterChain} for servlet resource-servers with access token introspection
- */
- @Conditional(IsIntrospectingResourceServerCondition.class)
- @Order(Ordered.LOWEST_PRECEDENCE)
- @Bean
- SecurityFilterChain springAddonsIntrospectingResourceServerSecurityFilterChain(
- HttpSecurity http,
- ServerProperties serverProperties,
- SpringAddonsOidcProperties addonsProperties,
- ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
- ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor,
- OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter,
- OpaqueTokenIntrospector opaqueTokenIntrospector,
- AuthenticationEntryPoint authenticationEntryPoint,
- Optional accessDeniedHandler)
- throws Exception {
- http.oauth2ResourceServer(server -> server.opaqueToken(ot -> {
- ot.introspector(opaqueTokenIntrospector);
- ot.authenticationConverter(introspectionAuthenticationConverter);
- server.authenticationEntryPoint(authenticationEntryPoint);
- accessDeniedHandler.ifPresent(server::accessDeniedHandler);
- }));
+ /**
+ *
+ * Configures a SecurityFilterChain for a resource server with JwtDecoder with @Order(LOWEST_PRECEDENCE). Defining a
+ * {@link SecurityWebFilterChain} bean with no security matcher and an order higher than LOWEST_PRECEDENCE will hide this filter-chain an
+ * disable most of spring-addons auto-configuration for OpenID resource-servers.
+ *
+ *
+ * @param http HTTP security to configure
+ * @param serverProperties Spring "server" configuration properties
+ * @param addonsProperties "com.c4-soft.springaddons.oidc" configuration properties
+ * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all"
+ * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration
+ * @param introspectionAuthenticationConverter Converts successful introspection result into an {@link Authentication}
+ * @param opaqueTokenIntrospector the instrospector to use
+ * @return A {@link SecurityWebFilterChain} for servlet resource-servers with access token
+ * introspection
+ */
+ @Conditional(IsIntrospectingResourceServerCondition.class)
+ @Order(Ordered.LOWEST_PRECEDENCE)
+ @Bean
+ SecurityFilterChain springAddonsIntrospectingResourceServerSecurityFilterChain(
+ HttpSecurity http,
+ ServerProperties serverProperties,
+ SpringAddonsOidcProperties addonsProperties,
+ ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
+ ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor,
+ OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter,
+ OpaqueTokenIntrospector opaqueTokenIntrospector)
+ throws Exception {
+ http.oauth2ResourceServer(server -> server.opaqueToken(ot -> {
+ ot.introspector(opaqueTokenIntrospector);
+ ot.authenticationConverter(introspectionAuthenticationConverter);
+ }));
- ServletConfigurationSupport.configureResourceServer(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
+ ServletConfigurationSupport.configureResourceServer(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor);
- return http.build();
- }
+ return http.build();
+ }
- /**
- * hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
- *
- * @return a hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
- */
- @ConditionalOnMissingBean
- @Bean
- ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() {
- return registry -> registry.anyRequest().authenticated();
- }
+ /**
+ * hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
+ *
+ * @return a hook to override security rules for all path that are not listed in "permit-all". Default is isAuthenticated().
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() {
+ return registry -> registry.anyRequest().authenticated();
+ }
- /**
- * Hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that you can modify anything
- *
- * @return a hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that you can modify
- * anything
- */
- @ConditionalOnMissingBean
- @Bean
- ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor() {
- return httpSecurity -> httpSecurity;
- }
+ /**
+ * Hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that you can
+ * modify anything
+ *
+ * @return a hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that
+ * you can modify anything
+ */
+ @ConditionalOnMissingBean
+ @Bean
+ ResourceServerSynchronizedHttpSecurityPostProcessor httpPostProcessor() {
+ return httpSecurity -> httpSecurity;
+ }
- @ConditionalOnMissingBean
- @Bean
- SpringAddonsJwtDecoderFactory springAddonsJwtDecoderFactory() {
- return new DefaultSpringAddonsJwtDecoderFactory();
- }
+ @ConditionalOnMissingBean
+ @Bean
+ SpringAddonsJwtDecoderFactory springAddonsJwtDecoderFactory() {
+ return new DefaultSpringAddonsJwtDecoderFactory();
+ }
- /**
- * Provides with multi-tenancy: builds a AuthenticationManagerResolver per provided OIDC issuer URI
- *
- * @param opPropertiesResolver a resolver for OpenID Provider configuration properties
- * @param jwtDecoderFactory something to build a JWT decoder from OpenID Provider configuration properties
- * @param jwtAuthenticationConverter converts from a {@link Jwt} to an {@link Authentication} implementation
- * @return Multi-tenant {@link AuthenticationManagerResolver} (one for each configured issuer)
- */
- @Conditional(DefaultAuthenticationManagerResolverCondition.class)
- @Bean
- AuthenticationManagerResolver authenticationManagerResolver(
- OpenidProviderPropertiesResolver opPropertiesResolver,
- SpringAddonsJwtDecoderFactory jwtDecoderFactory,
- Converter jwtAuthenticationConverter) {
- return new SpringAddonsJwtAuthenticationManagerResolver(opPropertiesResolver, jwtDecoderFactory, jwtAuthenticationConverter);
- }
+ /**
+ * Provides with multi-tenancy: builds a AuthenticationManagerResolver per provided OIDC issuer URI
+ *
+ * @param opPropertiesResolver a resolver for OpenID Provider configuration properties
+ * @param jwtDecoderFactory something to build a JWT decoder from OpenID Provider configuration properties
+ * @param jwtAuthenticationConverter converts from a {@link Jwt} to an {@link Authentication} implementation
+ * @return Multi-tenant {@link AuthenticationManagerResolver} (one for each configured
+ * issuer)
+ */
+ @Conditional(DefaultAuthenticationManagerResolverCondition.class)
+ @Bean
+ AuthenticationManagerResolver authenticationManagerResolver(
+ OpenidProviderPropertiesResolver opPropertiesResolver,
+ SpringAddonsJwtDecoderFactory jwtDecoderFactory,
+ Converter jwtAuthenticationConverter) {
+ return new SpringAddonsJwtAuthenticationManagerResolver(opPropertiesResolver, jwtDecoderFactory, jwtAuthenticationConverter);
+ }
- @ConditionalOnMissingBean
- @Bean
- AuthenticationEntryPoint authenticationEntryPoint() {
- return (request, response, authException) -> {
- response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\"");
- response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
- };
- }
+ /**
+ * Converter bean from {@link Jwt} to {@link AbstractAuthenticationToken}
+ *
+ * @param authoritiesConverter converts access-token claims into Spring authorities
+ * @param opPropertiesResolver spring-addons configuration properties
+ * @return a converter from {@link Jwt} to {@link AbstractAuthenticationToken}
+ */
+ @Conditional(DefaultJwtAbstractAuthenticationTokenConverterCondition.class)
+ @Bean
+ JwtAbstractAuthenticationTokenConverter jwtAuthenticationConverter(
+ Converter, Collection extends GrantedAuthority>> authoritiesConverter,
+ OpenidProviderPropertiesResolver opPropertiesResolver) {
+ return jwt -> new JwtAuthenticationToken(
+ jwt,
+ authoritiesConverter.convert(jwt.getClaims()),
+ new OpenidClaimSet(
+ jwt.getClaims(),
+ opPropertiesResolver.resolve(jwt.getClaims()).orElseThrow(() -> new NotAConfiguredOpenidProviderException(jwt.getClaims()))
+ .getUsernameClaim()).getName());
+ }
- /**
- * Converter bean from {@link Jwt} to {@link AbstractAuthenticationToken}
- *
- * @param authoritiesConverter converts access-token claims into Spring authorities
- * @param opPropertiesResolver spring-addons configuration properties
- * @return a converter from {@link Jwt} to {@link AbstractAuthenticationToken}
- */
- @Conditional(DefaultJwtAbstractAuthenticationTokenConverterCondition.class)
- @Bean
- JwtAbstractAuthenticationTokenConverter jwtAuthenticationConverter(
- Converter, Collection extends GrantedAuthority>> authoritiesConverter,
- OpenidProviderPropertiesResolver opPropertiesResolver) {
- return jwt -> new JwtAuthenticationToken(
- jwt,
- authoritiesConverter.convert(jwt.getClaims()),
- new OpenidClaimSet(
- jwt.getClaims(),
- opPropertiesResolver.resolve(jwt.getClaims()).orElseThrow(() -> new NotAConfiguredOpenidProviderException(jwt.getClaims())).getUsernameClaim())
- .getName());
- }
+ /**
+ * Converter bean from successful introspection result to an {@link Authentication} instance
+ *
+ * @param authoritiesConverter converts access-token claims into Spring authorities
+ * @param addonsProperties spring-addons configuration properties
+ * @param resourceServerProperties Spring Boot standard resource server configuration properties
+ * @return a converter from successful introspection result to an {@link Authentication} instance
+ */
+ @Conditional(DefaultOpaqueTokenAuthenticationConverterCondition.class)
+ @Bean
+ @SuppressWarnings("unchecked")
+ OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter(
+ Converter, Collection extends GrantedAuthority>> authoritiesConverter,
+ SpringAddonsOidcProperties addonsProperties,
+ OAuth2ResourceServerProperties resourceServerProperties) {
+ return (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> {
+ final var iatClaim = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
+ final var expClaim = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
+ return new BearerTokenAuthentication(
+ new OAuth2IntrospectionAuthenticatedPrincipal(
+ new OpenidClaimSet(
+ authenticatedPrincipal.getAttributes(),
+ addonsProperties.getOps().stream()
+ .filter(
+ openidProvider -> resourceServerProperties.getOpaquetoken().getIntrospectionUri()
+ .contains(openidProvider.getIss().toString()))
+ .findAny().orElse(addonsProperties.getOps().get(0)).getUsernameClaim()).getName(),
+ authenticatedPrincipal.getAttributes(),
+ (Collection) authenticatedPrincipal.getAuthorities()),
+ new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken, toInstant(iatClaim), toInstant(expClaim)),
+ authoritiesConverter.convert(authenticatedPrincipal.getAttributes()));
+ };
+ }
- /**
- * Converter bean from successful introspection result to an {@link Authentication} instance
- *
- * @param authoritiesConverter converts access-token claims into Spring authorities
- * @param addonsProperties spring-addons configuration properties
- * @param resourceServerProperties Spring Boot standard resource server configuration properties
- * @return a converter from successful introspection result to an {@link Authentication} instance
- */
- @Conditional(DefaultOpaqueTokenAuthenticationConverterCondition.class)
- @Bean
- @SuppressWarnings("unchecked")
- OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter(
- Converter, Collection extends GrantedAuthority>> authoritiesConverter,
- SpringAddonsOidcProperties addonsProperties,
- OAuth2ResourceServerProperties resourceServerProperties) {
- return (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> {
- final var iatClaim = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT);
- final var expClaim = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
- return new BearerTokenAuthentication(
- new OAuth2IntrospectionAuthenticatedPrincipal(
- new OpenidClaimSet(
- authenticatedPrincipal.getAttributes(),
- addonsProperties
- .getOps()
- .stream()
- .filter(
- openidProvider -> resourceServerProperties.getOpaquetoken().getIntrospectionUri().contains(openidProvider.getIss().toString()))
- .findAny()
- .orElse(addonsProperties.getOps().get(0))
- .getUsernameClaim()).getName(),
- authenticatedPrincipal.getAttributes(),
- (Collection) authenticatedPrincipal.getAuthorities()),
- new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken, toInstant(iatClaim), toInstant(expClaim)),
- authoritiesConverter.convert(authenticatedPrincipal.getAttributes()));
- };
- }
+ /**
+ * FIXME: use only the new CORS properties at next major release
+ */
+ @Conditional(DefaultCorsFilterCondition.class)
+ @Bean
+ CorsFilter corsFilter(SpringAddonsOidcProperties addonsProperties) {
+ final var corsProps = new ArrayList<>(addonsProperties.getCors());
+ final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
+ corsProps.addAll(deprecatedResourceServerCorsProps);
- /**
- * FIXME: use only the new CORS properties at next major release
- */
- @Conditional(DefaultCorsFilterCondition.class)
- @Bean
- CorsFilter corsFilter(SpringAddonsOidcProperties addonsProperties) {
- final var corsProps = new ArrayList<>(addonsProperties.getCors());
- final var deprecatedResourceServerCorsProps = addonsProperties.getResourceserver().getCors();
- corsProps.addAll(deprecatedResourceServerCorsProps);
+ return ServletConfigurationSupport.getCorsFilterBean(corsProps);
+ }
- return ServletConfigurationSupport.getCorsFilterBean(corsProps);
- }
-
- private static final Instant toInstant(Object claim) {
- if (claim == null) {
- return null;
- }
- if (claim instanceof Instant i) {
- return i;
- }
- if (claim instanceof Date d) {
- return d.toInstant();
- }
- if (claim instanceof Integer i) {
- return Instant.ofEpochSecond((i).longValue());
- } else if (claim instanceof Long l) {
- return Instant.ofEpochSecond(l);
- } else {
- return null;
- }
- }
+ private static final Instant toInstant(Object claim) {
+ if (claim == null) {
+ return null;
+ }
+ if (claim instanceof Instant i) {
+ return i;
+ }
+ if (claim instanceof Date d) {
+ return d.toInstant();
+ }
+ if (claim instanceof Integer i) {
+ return Instant.ofEpochSecond((i).longValue());
+ } else if (claim instanceof Long l) {
+ return Instant.ofEpochSecond(l);
+ } else {
+ return null;
+ }
+ }
}
diff --git a/spring-addons-starter-openapi/pom.xml b/spring-addons-starter-openapi/pom.xml
index b635df342..be3d72015 100644
--- a/spring-addons-starter-openapi/pom.xml
+++ b/spring-addons-starter-openapi/pom.xml
@@ -3,7 +3,7 @@
com.c4-soft.springaddons
spring-addons
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
spring-addons-starter-openapi
diff --git a/starters/spring-addons-starters-recaptcha/README.md b/spring-addons-starter-recaptcha/README.md
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/README.md
rename to spring-addons-starter-recaptcha/README.md
diff --git a/spring-addons-starter-recaptcha/pom.xml b/spring-addons-starter-recaptcha/pom.xml
new file mode 100644
index 000000000..6eab2e9c9
--- /dev/null
+++ b/spring-addons-starter-recaptcha/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+
+ com.c4-soft.springaddons
+ spring-addons
+ 8.0.0-RC2-SNAPSHOT
+ ..
+
+ spring-addons-starter-recaptcha
+
+ https://github.com/ch4mpy/spring-addons/
+
+ scm:git:git://github.com/ch4mpy/spring-addons.git
+ scm:git:git@github.com:ch4mpy/spring-addons.git
+ https://github.com/ch4mpy/spring-addons
+ spring-addons-7.8.8
+
+
+
+
+ org.springframework
+ spring-web
+
+
+ org.springframework.boot
+ spring-boot
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+ com.c4-soft.springaddons
+ spring-addons-starter-rest
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
diff --git a/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaSettings.java b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaSettings.java
new file mode 100644
index 000000000..3089932f7
--- /dev/null
+++ b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaSettings.java
@@ -0,0 +1,26 @@
+package com.c4_soft.springaddons.starter.recaptcha;
+
+import java.net.URL;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+import org.springframework.stereotype.Component;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientHttpRequestFactoryProperties;
+import lombok.Data;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "com.c4-soft.springaddons.recaptcha")
+public class C4ReCaptchaSettings {
+
+ private String secretKey;
+
+ @Value("${siteverify-url:https://www.google.com/recaptcha/api/siteverify}")
+ private URL siteverifyUrl;
+
+ private double v3Threshold = .5;
+
+ @NestedConfigurationProperty
+ private ClientHttpRequestFactoryProperties http;
+
+}
diff --git a/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaValidationService.java b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaValidationService.java
new file mode 100644
index 000000000..705e20b21
--- /dev/null
+++ b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaValidationService.java
@@ -0,0 +1,83 @@
+package com.c4_soft.springaddons.starter.recaptcha;
+
+import java.util.stream.Collectors;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.web.client.RestClient;
+import com.c4_soft.springaddons.rest.SpringAddonsClientHttpRequestFactory;
+import com.c4_soft.springaddons.rest.SystemProxyProperties;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Usage:
+ *
+ *
+ * if (Boolean.FALSE.equals(captcha.checkV2(reCaptcha).block())) {
+ * throw new RuntimeException("Are you a robot?");
+ * }
+ *
+ *
+ * @author Jérôme Wacongne ch4mp@c4-soft.com
+ */
+@Service
+@Slf4j
+public class C4ReCaptchaValidationService {
+
+ private final RestClient client;
+ private final String googleRecaptchaSecret;
+ private final double v3Threshold;
+
+ public C4ReCaptchaValidationService(C4ReCaptchaSettings settings,
+ SystemProxyProperties systemProxyProperties) {
+ final var clientBuilder = RestClient.builder();
+ clientBuilder.requestFactory(
+ new SpringAddonsClientHttpRequestFactory(systemProxyProperties, settings.getHttp()));
+ clientBuilder.baseUrl(settings.getSiteverifyUrl().toString());
+ this.client = clientBuilder.build();
+ this.googleRecaptchaSecret = settings.getSecretKey();
+ this.v3Threshold = settings.getV3Threshold();
+ }
+
+ /**
+ * Checks a reCaptcha V2 challenge response
+ *
+ * @param response answer provided by the client
+ * @return true / false
+ */
+ public Boolean checkV2(String response) {
+ final var dto = response(response, V2ValidationResponseDto.class);
+ log.debug("reCaptcha result : {}", dto);
+ return dto.isSuccess();
+ }
+
+ /**
+ * Checks a reCaptcha V3 challenge response
+ *
+ * @param response answer provided by the client
+ * @return a score between 0 and 1
+ * @throws ReCaptchaValidationException if response wasn't a valid reCAPTCHA token for your site
+ * or score is below configured threshold
+ */
+ public Double checkV3(String response) throws ReCaptchaValidationException {
+ final var dto = response(response, V3ValidationResponseDto.class);
+ log.debug("reCaptcha result : {}", dto);
+ if (!dto.isSuccess()) {
+ throw new ReCaptchaValidationException(String.format("Failed to validate reCaptcha: %s %s",
+ response, dto.getErrorCodes().stream().collect(Collectors.joining("[", ", ", "]"))));
+ }
+ if (dto.getScore() < v3Threshold) {
+ throw new ReCaptchaValidationException(
+ String.format("Failed to validate reCaptcha: %s. Score is %f", response, dto.getScore()));
+ }
+ return dto.getScore();
+ }
+
+ private T response(String response, Class dtoType) {
+ final var formData = new LinkedMultiValueMap<>();
+ formData.add("secret", googleRecaptchaSecret);
+ formData.add("response", response);
+ return client.post().contentType(MediaType.APPLICATION_FORM_URLENCODED).body(formData)
+ .retrieve().toEntity(dtoType).getBody();
+ }
+}
diff --git a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/ReCaptchaValidationException.java b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/ReCaptchaValidationException.java
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/ReCaptchaValidationException.java
rename to spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/ReCaptchaValidationException.java
diff --git a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/SpringBootAutoConfiguration.java b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/SpringBootAutoConfiguration.java
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/SpringBootAutoConfiguration.java
rename to spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/SpringBootAutoConfiguration.java
diff --git a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V2ValidationResponseDto.java b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V2ValidationResponseDto.java
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V2ValidationResponseDto.java
rename to spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V2ValidationResponseDto.java
diff --git a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V3ValidationResponseDto.java b/spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V3ValidationResponseDto.java
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V3ValidationResponseDto.java
rename to spring-addons-starter-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/V3ValidationResponseDto.java
diff --git a/starters/spring-addons-starters-recaptcha/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-addons-starter-recaptcha/src/main/resources/META-INF/additional-spring-configuration-metadata.json
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/src/main/resources/META-INF/additional-spring-configuration-metadata.json
rename to spring-addons-starter-recaptcha/src/main/resources/META-INF/additional-spring-configuration-metadata.json
diff --git a/starters/spring-addons-starters-recaptcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-addons-starter-recaptcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
rename to spring-addons-starter-recaptcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
diff --git a/starters/spring-addons-starters-recaptcha/src/test/resources/application.properties b/spring-addons-starter-recaptcha/src/test/resources/application.properties
similarity index 100%
rename from starters/spring-addons-starters-recaptcha/src/test/resources/application.properties
rename to spring-addons-starter-recaptcha/src/test/resources/application.properties
diff --git a/spring-addons-starter-rest/README.md b/spring-addons-starter-rest/README.md
index 0d858ba11..f7f1b411f 100644
--- a/spring-addons-starter-rest/README.md
+++ b/spring-addons-starter-rest/README.md
@@ -1,16 +1,19 @@
# Auto-configure `RestClient` or `WebClient` beans
-This starter aims at auto-configuring `RestClient` and `WebClient`. For now, it supports:
+This starter aims at auto-configuring `RestClient` and `WebClient` using application properties:
- base URL
- `Basic` or OAuth2 `Bearer` authorization; for the latter, using either a client registration or forwarding the access token in the security context of a resource server.
- proxy settings with consideration of `HTTP_PROXY` and `NO_PROXY` environment variables. Finer-grained configuration or overrides can be achieved with custom properties.
- connection and read timeouts
-- instantiate `RestClient` in servlets and `WebClient` in WebFlux apps. Any client can be switched to `WebClient` in servlets.
-- client bean names are by default the camelCase transformation of the key in the application properties map, with the `Builder` suffix when `expose-builder` is true. It can be set to anything else in properties.
-When more is needed than what can be auto-configured, it is possible to have `RestClient.Builder` or `WebClient.Builder` exposed as beans instead of the already built instances.
+Instantiated REST clients are `WebClient` in WebFlux apps and `RestClient` in servlets, but any client can be switched to `WebClient` in servlets.
-## Usage since `8.0.0-RC1`
-### Dependency
+Exposed bean names are by default the `camelCase` transformation of the `kebab-case` key in the application properties map, with the `Builder` suffix when `expose-builder` is `true`. It can be set to anything else in properties.
+
+When more is needed than the provided auto-configuration, it is possible to expose `RestClient.Builder` or `WebClient.Builder` instead of the already built instances.
+
+There is no adherence to other `spring-addons` starters (`spring-addons-starter-rest` can be used without `spring-addons-starter-oidc`).
+
+## Dependency
```xml
com.c4-soft.springaddons
@@ -32,18 +35,9 @@ com:
oauth2:
forward-bearer: true
```
-The `keycloakAdminClient` bean can be autowired in any `@Component` or `@Configuration`. For instance when generating an `@HttpExchange` proxy:
-```java
-@Configuration
-public class RestConfiguration {
- @Bean
- KeycloakAdminApi keycloakAdminApi(RestClient keycloakAdminClient) throws Exception {
- return new RestClientHttpExchangeProxyFactoryBean<>(KeycloakAdminApi.class, keycloakAdminClient).getObject();
- }
-}
-```
+This exposes a pre-configured bean named `keycloakAdminClient`. The default type of this bean is `RestClient` in a servlet app and `WebClient` in a Webflux one.
-### Advanced configuration sample for 3 different clients
+## Advanced configuration samples
```yaml
com:
c4-soft:
@@ -52,8 +46,10 @@ com:
client:
machin-client:
base-url: http://localhost:${machin-api-port}
- expose-builder: true
+ # expose a WebClient instead of a RestClient in a servlet app
type: WEB_CLIENT
+ # expose the WebClient.Builder instead of an already built WebClient
+ expose-builder: true
http:
chunk-size: 1000
connect-timeout-millis: 1000
@@ -69,21 +65,25 @@ com:
protocol: http
authorization:
oauth2:
+ # authorize outgoing requests with the Bearer token in the security (possible only in a resource server app)
forward-bearer: true
bidule-client:
base-url: http://localhost:${bidule-api-port}
authorization:
oauth2:
+ # authorize outgoing requests with a Bearer obtained using an OAuth2 client registration
oauth2-registration-id: bidule-registration
http:
proxy:
- # Use HTTP_PROXY and NO_PROXY environment variables
+ # use HTTP_PROXY and NO_PROXY environment variables and add proxy authentication
username: spring-backend
password: secret
chose-client:
base-url: http://localhost:${chose-api-port}
+ # change the bean name to "chose" (default would have bean "choseClient" because of the "chose-client" ID, or "choseClientBuilder" if expose-builder was true)
bean-name: chose
authorization:
+ # authorize outgoing requests with Basic auth
basic:
username: spring-backend
password: secret
@@ -102,3 +102,19 @@ public class RestConfiguration {
}
}
```
+
+## Exposing a generated `@HttpExchange` proxy as a `@Bean`
+Once the REST clients configured, we may use it to generate `@HttpExchange` implementations:
+```java
+@Configuration
+public class RestConfiguration {
+ /**
+ * @param machinClient might be auto-configured by spring-addons-starter-rest or a hand-crafted bean
+ * @return a generated implementation of the {@link MachinApi} {@link HttpExchange @HttpExchange}, exposed as a bean named "machinApi".
+ */
+ @Bean
+ MachinApi machinApi(RestClient machinClient) throws Exception {
+ return new RestClientHttpExchangeProxyFactoryBean<>(MachinApi.class, machinClient).getObject();
+ }
+}
+```
\ No newline at end of file
diff --git a/spring-addons-starter-rest/pom.xml b/spring-addons-starter-rest/pom.xml
index 418d3b14d..75608cd88 100644
--- a/spring-addons-starter-rest/pom.xml
+++ b/spring-addons-starter-rest/pom.xml
@@ -5,7 +5,7 @@
com.c4-soft.springaddons
spring-addons
- 7.8.13-SNAPSHOT
+ 8.0.0-RC2-SNAPSHOT
..
spring-addons-starter-rest
@@ -44,6 +44,7 @@
org.springframework.security
spring-security-oauth2-client
+ true
org.springframework.security
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AbstractSpringAddonsWebClientSupport.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AbstractSpringAddonsWebClientSupport.java
deleted file mode 100644
index 3f98f5d28..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AbstractSpringAddonsWebClientSupport.java
+++ /dev/null
@@ -1,185 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.net.URL;
-import java.util.Map;
-import java.util.Optional;
-
-import org.springframework.http.client.reactive.ReactorClientHttpConnector;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
-import org.springframework.web.reactive.function.client.ExchangeFunction;
-import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.reactive.function.client.WebClient.Builder;
-import org.springframework.web.reactive.function.client.support.WebClientAdapter;
-import org.springframework.web.service.annotation.HttpExchange;
-import org.springframework.web.service.invoker.HttpServiceProxyFactory;
-
-import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.AuthorizationProperties;
-
-import reactor.netty.http.client.HttpClient;
-import reactor.netty.transport.ProxyProvider;
-
-/**
- * @author Jerome Wacongne chl4mp@c4-soft.com
- */
-public abstract class AbstractSpringAddonsWebClientSupport {
-
- private final ProxySupport proxySupport;
-
- private final Map restClientProperties;
-
- /**
- * A {@link BearerProvider} to get the Bearer from the request security context
- */
- private final BearerProvider forwardingBearerProvider;
-
- public AbstractSpringAddonsWebClientSupport(
- SystemProxyProperties systemProxyProperties,
- SpringAddonsRestProperties addonsRestProperties,
- BearerProvider forwardingBearerProvider) {
- super();
- this.proxySupport = new ProxySupport(systemProxyProperties, addonsRestProperties);
- this.restClientProperties = addonsRestProperties.getClient();
- this.forwardingBearerProvider = forwardingBearerProvider;
- }
-
- public WebClient.Builder client() {
- final var clientBuilder = WebClient.builder();
-
- httpConnector(proxySupport).ifPresent(clientBuilder::clientConnector);
-
- return clientBuilder;
- }
-
- /**
- * @param clientName key in "com.c4-soft.springaddons.rest.client" entries of {@link SpringAddonsRestProperties}
- * @return A {@link WebClient} Builder pre-configured with a base-URI and (optionally) with a Bearer Authorization
- */
- public WebClient.Builder client(String clientName) {
- final var clientProps = Optional.ofNullable(restClientProperties.get(clientName)).orElseThrow(() -> new RestConfigurationNotFoundException(clientName));
-
- final var clientBuilder = client();
-
- clientProps.getBaseUrl().map(URL::toString).ifPresent(clientBuilder::baseUrl);
-
- authorize(clientBuilder, clientProps.getAuthorization(), clientName);
-
- return clientBuilder;
- }
-
- /**
- * Uses the provided {@link WebClient} to proxy the httpServiceClass
- *
- * @param
- * @param client
- * @param httpServiceClass class of the #64;Service (with {@link HttpExchange} methods) to proxy with a {@link WebClient}
- * @return a #64;Service proxy with a {@link WebClient}
- */
- public T service(WebClient client, Class httpServiceClass) {
- return HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)).build().createClient(httpServiceClass);
- }
-
- /**
- * Builds a {@link WebClient} with just the provided spring-addons {@link SpringAddonsRestProperties} and uses it to proxy the
- * httpServiceClass.
- *
- * @param
- * @param httpServiceClass class of the #64;Service (with {@link HttpExchange} methods) to proxy with a {@link WebClient}
- * @param clientName key in "rest" entries of spring-addons client properties
- * @return a #64;Service proxy with a {@link WebClient}
- */
- public T service(String clientName, Class httpServiceClass) {
- return this.service(this.client(clientName).build(), httpServiceClass);
- }
-
- protected Optional httpConnector(ProxySupport proxySupport) {
- return proxySupport.getHostname().map(proxyHost -> {
- return new ReactorClientHttpConnector(
- HttpClient.create().proxy(
- proxy -> proxy.type(protocoleToProxyType(proxySupport.getProtocol())).host(proxyHost).port(proxySupport.getPort())
- .username(proxySupport.getUsername()).password(username -> proxySupport.getPassword())
- .nonProxyHosts(proxySupport.getNoProxy()).connectTimeoutMillis(proxySupport.getConnectTimeoutMillis())));
-
- });
- }
-
- protected void authorize(Builder clientBuilder, AuthorizationProperties authProps, String clientName) {
- if (authProps.getOauth2().isConfigured() && authProps.getBasic().isConfigured()) {
- throw new RestMisconfigurationConfigurationException(
- "REST authorization configuration for %s can be made for either OAuth2 or Basic, but not both at a time".formatted(clientName));
- }
- if (authProps.getOauth2().isConfigured()) {
- oauth2(clientBuilder, authProps.getOauth2(), clientName);
- }
- if (authProps.getBasic().isConfigured()) {
- basic(clientBuilder, authProps.getBasic(), clientName);
- }
- }
-
- protected void oauth2(Builder clientBuilder, AuthorizationProperties.OAuth2Properties oauth2Props, String clientName) {
- if (!oauth2Props.isConfValid()) {
- throw new RestMisconfigurationConfigurationException(
- "REST OAuth2 authorization configuration for %s can be made for either a registration-id or resource server Bearer forwarding, but not both at a time"
- .formatted(clientName));
- }
- oauth2Props.getOauth2RegistrationId().map(this::oauth2RegistrationFilter).ifPresent(clientBuilder::filter);
- if (oauth2Props.isForwardBearer()) {
- clientBuilder.filter((ClientRequest request, ExchangeFunction next) -> {
- final var bearer = forwardingBearerProvider.getBearer();
- if (bearer.isEmpty()) {
- return next.exchange(request);
- }
- final var modified = ClientRequest.from(request);
- modified.headers(headers -> headers.setBearerAuth(bearer.get()));
- return next.exchange(modified.build());
- });
- }
- }
-
- protected abstract ExchangeFilterFunction oauth2RegistrationFilter(String registrationId);
-
- protected void basic(Builder clientBuilder, AuthorizationProperties.BasicAuthProperties authProps, String clientName) {
- if (authProps.getEncodedCredentials().isPresent()) {
- if (authProps.getUsername().isPresent() || authProps.getPassword().isPresent() || authProps.getCharset().isPresent()) {
- throw new RestMisconfigurationConfigurationException(
- "REST Basic authorization for %s is misconfigured: when encoded-credentials is provided, username, password and charset must be absent."
- .formatted(clientName));
- }
- } else {
- if (authProps.getUsername().isEmpty() || authProps.getPassword().isEmpty()) {
- throw new RestMisconfigurationConfigurationException(
- "REST Basic authorization for %s is misconfigured: when encoded-credentials is empty, username & password are required."
- .formatted(clientName));
- }
- }
- clientBuilder.filter((ClientRequest request, ExchangeFunction next) -> {
- if (authProps.getEncodedCredentials().isEmpty() && authProps.getUsername().isEmpty()) {
- return next.exchange(request);
- }
- final var modified = ClientRequest.from(request);
- if (authProps.getEncodedCredentials().isPresent()) {
- modified.headers(headers -> headers.setBasicAuth(authProps.getEncodedCredentials().get()));
- } else if (authProps.getCharset().isPresent()) {
- modified.headers(headers -> headers.setBasicAuth(authProps.getUsername().get(), authProps.getPassword().get(), authProps.getCharset().get()));
- } else {
- modified.headers(headers -> headers.setBasicAuth(authProps.getUsername().get(), authProps.getPassword().get()));
- }
- return next.exchange(modified.build());
-
- });
- }
-
- static ProxyProvider.Proxy protocoleToProxyType(String protocol) {
- if (protocol == null) {
- return null;
- }
- final var lower = protocol.toLowerCase();
- if (lower.startsWith("http")) {
- return ProxyProvider.Proxy.HTTP;
- }
- if (lower.startsWith("socks4")) {
- return ProxyProvider.Proxy.SOCKS4;
- }
- return ProxyProvider.Proxy.SOCKS5;
- }
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AbstractWebClientBuilderFactoryBean.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AbstractWebClientBuilderFactoryBean.java
new file mode 100644
index 000000000..79941d6e3
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AbstractWebClientBuilderFactoryBean.java
@@ -0,0 +1,175 @@
+package com.c4_soft.springaddons.rest;
+
+import java.net.URL;
+import java.time.Duration;
+import java.util.Optional;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.lang.Nullable;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.ExchangeFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClient.Builder;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.AuthorizationProperties;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientHttpRequestFactoryProperties;
+import io.netty.channel.ChannelOption;
+import lombok.Setter;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.transport.ProxyProvider;
+
+/**
+ * An abstraction of servlet and server (webflux) {@link FactoryBean} for {@link WebClient.Builder
+ * WebClient Builder}.
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+@Setter
+public abstract class AbstractWebClientBuilderFactoryBean
+ implements FactoryBean {
+ private String clientId;
+ private SystemProxyProperties systemProxyProperties = new SystemProxyProperties();
+ private SpringAddonsRestProperties restProperties = new SpringAddonsRestProperties();
+
+
+ @Override
+ @Nullable
+ public WebClient.Builder getObject() throws Exception {
+ final var builder = WebClient.builder();
+ final var clientProps = Optional.ofNullable(restProperties.getClient().get(clientId))
+ .orElseThrow(() -> new RestConfigurationNotFoundException(clientId));
+
+ builder.clientConnector(clientConnector(systemProxyProperties, clientProps.getHttp()));
+
+ clientProps.getBaseUrl().map(URL::toString).ifPresent(builder::baseUrl);
+
+ setAuthorizationHeader(builder, clientProps.getAuthorization(), clientId);
+
+ return builder;
+ }
+
+ @Override
+ @Nullable
+ public Class> getObjectType() {
+ return WebClient.Builder.class;
+ }
+
+ public static ReactorClientHttpConnector clientConnector(
+ SystemProxyProperties systemProxyProperties,
+ ClientHttpRequestFactoryProperties addonsProperties) {
+
+ final var client = HttpClient.create();
+
+ addonsProperties.getConnectTimeoutMillis()
+ .ifPresent(timeout -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout));
+ addonsProperties.getReadTimeoutMillis()
+ .ifPresent(timeout -> client.responseTimeout(Duration.ofMillis(timeout)));
+
+ final var proxySupport = new ProxySupport(systemProxyProperties, addonsProperties.getProxy());
+ if (proxySupport.isEnabled()) {
+ client.proxy(proxy -> proxy.type(protocoleToProxyType(proxySupport.getProtocol()))
+ .host(proxySupport.getHostname().get()).port(proxySupport.getPort())
+ .username(proxySupport.getUsername()).password(username -> proxySupport.getPassword())
+ .nonProxyHosts(proxySupport.getNoProxy())
+ .connectTimeoutMillis(proxySupport.getConnectTimeoutMillis()));
+ }
+
+ return new ReactorClientHttpConnector(HttpClient.create());
+ }
+
+ static Optional httpConnector(ProxySupport proxySupport) {
+ return proxySupport.getHostname().map(proxyHost -> {
+ return new ReactorClientHttpConnector(HttpClient.create()
+ .proxy(proxy -> proxy.type(protocoleToProxyType(proxySupport.getProtocol()))
+ .host(proxyHost).port(proxySupport.getPort()).username(proxySupport.getUsername())
+ .password(username -> proxySupport.getPassword())
+ .nonProxyHosts(proxySupport.getNoProxy())
+ .connectTimeoutMillis(proxySupport.getConnectTimeoutMillis())));
+
+ });
+ }
+
+ static ProxyProvider.Proxy protocoleToProxyType(String protocol) {
+ if (protocol == null) {
+ return null;
+ }
+ final var lower = protocol.toLowerCase();
+ if (lower.startsWith("http")) {
+ return ProxyProvider.Proxy.HTTP;
+ }
+ if (lower.startsWith("socks4")) {
+ return ProxyProvider.Proxy.SOCKS4;
+ }
+ return ProxyProvider.Proxy.SOCKS5;
+ }
+
+ protected void setAuthorizationHeader(WebClient.Builder clientBuilder,
+ AuthorizationProperties authProps, String clientId) {
+ if (authProps.getOauth2().isConfigured() && authProps.getBasic().isConfigured()) {
+ throw new RestMisconfigurationException(
+ "REST authorization configuration for %s can be made for either OAuth2 or Basic, but not both at a time"
+ .formatted(clientId));
+ }
+ if (authProps.getOauth2().isConfigured()) {
+ setBearerAuthorizationHeader(clientBuilder, authProps.getOauth2(), clientId);
+ } else if (authProps.getBasic().isConfigured()) {
+ setBasicAuthorizationHeader(clientBuilder, authProps.getBasic(), clientId);
+ }
+ }
+
+ protected void setBearerAuthorizationHeader(WebClient.Builder clientBuilder,
+ AuthorizationProperties.OAuth2Properties oauth2Props, String clientId) {
+ if (!oauth2Props.isConfValid()) {
+ throw new RestMisconfigurationException(
+ "REST OAuth2 authorization configuration for %s can be made for either a registration-id or resource server Bearer forwarding, but not both at a time"
+ .formatted(clientId));
+ }
+ if (oauth2Props.getOauth2RegistrationId().isPresent()) {
+ clientBuilder
+ .filter(registrationExchangeFilterFunction(oauth2Props.getOauth2RegistrationId().get()));
+ } else if (oauth2Props.isForwardBearer()) {
+ clientBuilder.filter(forwardingBearerExchangeFilterFunction());
+ }
+ }
+
+ protected abstract ExchangeFilterFunction registrationExchangeFilterFunction(
+ String Oauth2RegistrationId);
+
+ protected abstract ExchangeFilterFunction forwardingBearerExchangeFilterFunction();
+
+ protected void setBasicAuthorizationHeader(Builder clientBuilder,
+ AuthorizationProperties.BasicAuthProperties authProps, String clientName) {
+ if (authProps.getEncodedCredentials().isPresent()) {
+ if (authProps.getUsername().isPresent() || authProps.getPassword().isPresent()
+ || authProps.getCharset().isPresent()) {
+ throw new RestMisconfigurationException(
+ "REST Basic authorization for %s is misconfigured: when encoded-credentials is provided, username, password and charset must be absent."
+ .formatted(clientName));
+ }
+ } else {
+ if (authProps.getUsername().isEmpty() || authProps.getPassword().isEmpty()) {
+ throw new RestMisconfigurationException(
+ "REST Basic authorization for %s is misconfigured: when encoded-credentials is empty, username & password are required."
+ .formatted(clientName));
+ }
+ }
+ clientBuilder.filter((ClientRequest request, ExchangeFunction next) -> {
+ if (authProps.getEncodedCredentials().isEmpty() && authProps.getUsername().isEmpty()) {
+ return next.exchange(request);
+ }
+ final var modified = ClientRequest.from(request);
+ if (authProps.getEncodedCredentials().isPresent()) {
+ modified.headers(headers -> headers.setBasicAuth(authProps.getEncodedCredentials().get()));
+ } else if (authProps.getCharset().isPresent()) {
+ modified.headers(headers -> headers.setBasicAuth(authProps.getUsername().get(),
+ authProps.getPassword().get(), authProps.getCharset().get()));
+ } else {
+ modified.headers(headers -> headers.setBasicAuth(authProps.getUsername().get(),
+ authProps.getPassword().get()));
+ }
+ return next.exchange(modified.build());
+
+ });
+ }
+
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AuthorizedClientBearerProvider.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AuthorizedClientBearerProvider.java
deleted file mode 100644
index 9e48630b7..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/AuthorizedClientBearerProvider.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.util.List;
-import java.util.Optional;
-
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.security.authentication.AnonymousAuthenticationToken;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
-import org.springframework.security.oauth2.core.OAuth2AccessToken;
-
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * Used by a {@link ClientHttpRequestInterceptor} to add a Bearer Authorization header (if the {@link OAuth2AuthorizedClientManager} provides one for the
- * configured registration ID).
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class AuthorizedClientBearerProvider implements BearerProvider {
- private static final AnonymousAuthenticationToken ANONYMOUS = new AnonymousAuthenticationToken(
- "anonymous",
- "anonymous",
- List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")));
-
- private final OAuth2AuthorizedClientManager authorizedClientManager;
- private final String registrationId;
-
- @Override
- public Optional getBearer() {
- final var authentication = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).orElse(ANONYMOUS);
- OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(registrationId).principal(authentication).build();
- final var authorizedClient = Optional.ofNullable(authorizedClientManager.authorize(authorizeRequest));
- final var token = authorizedClient.map(OAuth2AuthorizedClient::getAccessToken);
- return token.map(OAuth2AccessToken::getTokenValue);
- }
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/BearerProvider.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/BearerProvider.java
deleted file mode 100644
index 19a093954..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/BearerProvider.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.util.Optional;
-
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-
-/**
- * Used by a {@link ClientHttpRequestInterceptor} to add a Bearer Authorization header
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
-public interface BearerProvider {
- Optional getBearer();
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/BearerTokenAuthenticationInterceptor.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/BearerTokenAuthenticationInterceptor.java
deleted file mode 100644
index b8bc7d945..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/BearerTokenAuthenticationInterceptor.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.io.IOException;
-
-import org.springframework.http.HttpRequest;
-import org.springframework.http.client.ClientHttpRequestExecution;
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.lang.NonNull;
-
-import lombok.Data;
-
-/**
- * A {@link ClientHttpRequestInterceptor} adding a Bearer Authorization header (if the {@link BearerProvider} provides one).
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
-@Data
-public class BearerTokenAuthenticationInterceptor implements ClientHttpRequestInterceptor {
- private final BearerProvider bearerProvider;
-
- @Override
- public @NonNull ClientHttpResponse intercept(@NonNull HttpRequest request, @NonNull byte[] body, @NonNull ClientHttpRequestExecution execution)
- throws IOException {
- bearerProvider.getBearer().ifPresent(bearer -> {
- request.getHeaders().setBearerAuth(bearer);
- });
- return execution.execute(request, body);
- }
-}
\ No newline at end of file
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/DefaultBearerProvider.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/DefaultBearerProvider.java
deleted file mode 100644
index afbcfa86b..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/DefaultBearerProvider.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.util.Optional;
-
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
-import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
-
-public class DefaultBearerProvider implements BearerProvider {
-
- @Override
- public Optional getBearer() {
- final var authentication = SecurityContextHolder.getContext().getAuthentication();
- if (authentication instanceof JwtAuthenticationToken jwt) {
- return Optional.of(jwt.getToken().getTokenValue());
- }
- if (authentication instanceof BearerTokenAuthentication opaque) {
- return Optional.of(opaque.getToken().getTokenValue());
- }
- return Optional.empty();
- }
-
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/HttpExchangeProxyFactoryBean.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/HttpExchangeProxyFactoryBean.java
new file mode 100644
index 000000000..83e4dae34
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/HttpExchangeProxyFactoryBean.java
@@ -0,0 +1,70 @@
+package com.c4_soft.springaddons.rest;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.lang.Nullable;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.client.support.RestClientAdapter;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.support.WebClientAdapter;
+import org.springframework.web.service.annotation.HttpExchange;
+import org.springframework.web.service.invoker.HttpExchangeAdapter;
+import org.springframework.web.service.invoker.HttpServiceProxyFactory;
+import lombok.NoArgsConstructor;
+
+/**
+ * Bean factory using {@link HttpServiceProxyFactory} to generate an instance of a
+ * {@link HttpExchange @HttpExchange} interface
+ *
+ * @param {@link HttpExchange @HttpExchange} interface to implement
+ * @see WebClientHttpExchangeProxyFactoryBean WebClientHttpExchangeProxyFactoryBean when RestClient
+ * is not on the class-path
+ * @see RestClientHttpExchangeProxyFactoryBean RestClientHttpExchangeProxyFactoryBean when WebClient
+ * is not on the class-path
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+@NoArgsConstructor
+public class HttpExchangeProxyFactoryBean implements FactoryBean {
+ private Class httpExchangeClass;
+ private HttpExchangeAdapter adapter;
+
+ public HttpExchangeProxyFactoryBean(Class httpExchangeClass, RestClient client) {
+ this.httpExchangeClass = httpExchangeClass;
+ this.setClient(client);
+ }
+
+ public HttpExchangeProxyFactoryBean(Class httpExchangeClass, WebClient client) {
+ this.httpExchangeClass = httpExchangeClass;
+ this.setClient(client);
+ }
+
+ @Override
+ @Nullable
+ public T getObject() throws Exception {
+ if (adapter == null || getObjectType() == null) {
+ throw new RestMisconfigurationException(
+ "Both of a REST client (RestClient or WebClient) and the @HttpExchange interface to implement must be configured on the HttpExchangeProxyFactoryBean before calling the getObject() method.");
+ }
+ return HttpServiceProxyFactory.builderFor(adapter).build().createClient(getObjectType());
+ }
+
+ @Override
+ public Class getObjectType() {
+ return httpExchangeClass;
+ }
+
+ public HttpExchangeProxyFactoryBean setHttpExchangeClass(Class httpExchangeClass) {
+ this.httpExchangeClass = httpExchangeClass;
+ return this;
+ }
+
+ public HttpExchangeProxyFactoryBean setClient(RestClient client) {
+ this.adapter = RestClientAdapter.create(client);
+ return this;
+ }
+
+ public HttpExchangeProxyFactoryBean setClient(WebClient client) {
+ this.adapter = WebClientAdapter.create(client);
+ return this;
+ }
+
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/IsServletWithWebClientCondition.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/IsServletWithWebClientCondition.java
index cbb56ae47..2eb039e60 100644
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/IsServletWithWebClientCondition.java
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/IsServletWithWebClientCondition.java
@@ -6,16 +6,24 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.web.reactive.function.client.WebClient;
+/**
+ * A conditon to apply @Configuration only if an application is a servlet and if
+ * {@link WebClient} is on the class-path
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
public class IsServletWithWebClientCondition extends AllNestedConditions {
- IsServletWithWebClientCondition() {
- super(ConfigurationPhase.REGISTER_BEAN);
- }
+ IsServletWithWebClientCondition() {
+ super(ConfigurationPhase.PARSE_CONFIGURATION);
+ }
- @ConditionalOnWebApplication(type = Type.SERVLET)
- static class IsServlet {}
+ @ConditionalOnWebApplication(type = Type.SERVLET)
+ static class IsServlet {
+ }
- @ConditionalOnClass(WebClient.class)
- static class IsWebClientOnClasspath {}
+ @ConditionalOnClass(WebClient.class)
+ static class IsWebClientOnClasspath {
+ }
}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ProxySupport.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ProxySupport.java
index 68d3994e2..cc59b9dbb 100644
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ProxySupport.java
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ProxySupport.java
@@ -4,96 +4,105 @@
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
-
import org.springframework.util.StringUtils;
-
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientHttpRequestFactoryProperties.ProxyProperties;
import lombok.RequiredArgsConstructor;
+/**
+ * Used when configuring a {@link RestClient} or {@link WebClient} instance to authenticate on an
+ * HTTP or SOCKS proxy.
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
@RequiredArgsConstructor
public class ProxySupport {
- private final SystemProxyProperties systemProxyProperties;
- private final SpringAddonsRestProperties restProperties;
-
- public boolean isEnabled() {
- return restProperties.getProxy().isEnabled() && getHostname().isPresent();
- }
-
- public Optional getHostname() {
- if (!restProperties.getProxy().isEnabled()) {
- return Optional.empty();
- }
- return restProperties.getProxy().getHost().or(() -> systemProxyProperties.getHttpProxy().map(URL::getHost));
- }
-
- public String getProtocol() {
- if (!restProperties.getProxy().isEnabled()) {
- return null;
- }
- return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getProtocol())
- .orElse(systemProxyProperties.getHttpProxy().map(URL::getProtocol).orElse(null));
- }
-
- public int getPort() {
- return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getPort())
- .orElse(systemProxyProperties.getHttpProxy().map(URL::getPort).orElse(restProperties.getProxy().getPort()));
- }
-
- public String getUsername() {
- if (!restProperties.getProxy().isEnabled()) {
- return null;
- }
- return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getUsername())
- .orElse(systemProxyProperties.getHttpProxy().map(URL::getUserInfo).map(ProxySupport::getUserinfoName).orElse(null));
- }
-
- public String getPassword() {
- if (!restProperties.getProxy().isEnabled()) {
- return null;
- }
- return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getPassword())
- .orElse(systemProxyProperties.getHttpProxy().map(URL::getUserInfo).map(ProxySupport::getUserinfoPassword).orElse(null));
- }
-
- public String getNoProxy() {
- if (!restProperties.getProxy().isEnabled()) {
- return null;
- }
- return Optional.ofNullable(restProperties.getProxy().getNonProxyHostsPattern()).filter(StringUtils::hasText)
- .orElse(getNonProxyHostsPattern(systemProxyProperties.getNoProxy()));
- }
-
- public int getConnectTimeoutMillis() {
- return restProperties.getProxy().getConnectTimeoutMillis();
- }
-
- public SystemProxyProperties getSystemProperties() {
- return systemProxyProperties;
- }
-
- public SpringAddonsRestProperties.ProxyProperties getAddonsProperties() {
- return restProperties.getProxy();
- }
-
- static String getUserinfoName(String userinfo) {
- if (userinfo == null) {
- return null;
- }
- return userinfo.split(":")[0];
- }
-
- static String getUserinfoPassword(String userinfo) {
- if (userinfo == null) {
- return null;
- }
- final var splits = userinfo.split(":");
- return splits.length < 2 ? null : splits[1];
- }
-
- static String getNonProxyHostsPattern(List noProxy) {
- if (noProxy == null || noProxy.isEmpty()) {
- return null;
- }
- return noProxy.stream().map(host -> host.replace(".", "\\.")).map(host -> host.replace("-", "\\-"))
- .map(host -> host.startsWith("\\.") ? ".*" + host : host).collect(Collectors.joining(")|(", "(", ")"));
- }
+ private final SystemProxyProperties systemProxyProperties;
+ private final ProxyProperties springAddonsProperties;
+
+ public boolean isEnabled() {
+ return springAddonsProperties.isEnabled() && getHostname().isPresent();
+ }
+
+ public Optional getHostname() {
+ if (!springAddonsProperties.isEnabled()) {
+ return Optional.empty();
+ }
+ return springAddonsProperties.getHost()
+ .or(() -> systemProxyProperties.getHttpProxy().map(URL::getHost));
+ }
+
+ public String getProtocol() {
+ if (!springAddonsProperties.isEnabled()) {
+ return null;
+ }
+ return springAddonsProperties.getHost().map(h -> springAddonsProperties.getProtocol())
+ .orElse(systemProxyProperties.getHttpProxy().map(URL::getProtocol).orElse(null));
+ }
+
+ public int getPort() {
+ return springAddonsProperties.getHost().map(h -> springAddonsProperties.getPort()).orElse(
+ systemProxyProperties.getHttpProxy().map(URL::getPort).orElse(springAddonsProperties.getPort()));
+ }
+
+ public String getUsername() {
+ if (!springAddonsProperties.isEnabled()) {
+ return null;
+ }
+ return springAddonsProperties.getHost().map(h -> springAddonsProperties.getUsername())
+ .orElse(systemProxyProperties.getHttpProxy().map(URL::getUserInfo)
+ .map(ProxySupport::getUserinfoName).orElse(null));
+ }
+
+ public String getPassword() {
+ if (!springAddonsProperties.isEnabled()) {
+ return null;
+ }
+ return springAddonsProperties.getHost().map(h -> springAddonsProperties.getPassword())
+ .orElse(systemProxyProperties.getHttpProxy().map(URL::getUserInfo)
+ .map(ProxySupport::getUserinfoPassword).orElse(null));
+ }
+
+ public String getNoProxy() {
+ if (!springAddonsProperties.isEnabled()) {
+ return null;
+ }
+ return Optional.ofNullable(springAddonsProperties.getNonProxyHostsPattern())
+ .filter(StringUtils::hasText)
+ .orElse(getNonProxyHostsPattern(systemProxyProperties.getNoProxy()));
+ }
+
+ public int getConnectTimeoutMillis() {
+ return springAddonsProperties.getConnectTimeoutMillis();
+ }
+
+ public SystemProxyProperties getSystemProperties() {
+ return systemProxyProperties;
+ }
+
+ static String getUserinfoName(String userinfo) {
+ if (userinfo == null) {
+ return null;
+ }
+ return userinfo.split(":")[0];
+ }
+
+ static String getUserinfoPassword(String userinfo) {
+ if (userinfo == null) {
+ return null;
+ }
+ final var splits = userinfo.split(":");
+ return splits.length < 2 ? null : splits[1];
+ }
+
+ static String getNonProxyHostsPattern(List noProxy) {
+ if (noProxy == null || noProxy.isEmpty()) {
+ return null;
+ }
+ return noProxy.stream().map(host -> host.replace(".", "\\."))
+ .map(host -> host.replace("-", "\\-"))
+ .map(host -> host.startsWith("\\.") ? ".*" + host : host)
+ .collect(Collectors.joining(")|(", "(", ")"));
+ }
}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveAuthorizedClientBearerProvider.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveAuthorizedClientBearerProvider.java
deleted file mode 100644
index 9813bf887..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveAuthorizedClientBearerProvider.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.util.List;
-import java.util.Optional;
-
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.security.authentication.AnonymousAuthenticationToken;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
-import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
-import org.springframework.security.oauth2.core.OAuth2AccessToken;
-
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import reactor.core.publisher.Mono;
-
-/**
- * A {@link ClientHttpRequestInterceptor} adding a Bearer Authorization header (if the {@link OAuth2AuthorizedClientManager} provides one for the configured
- * registration ID).
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class ReactiveAuthorizedClientBearerProvider implements ReactiveBearerProvider {
- private static final AnonymousAuthenticationToken ANONYMOUS = new AnonymousAuthenticationToken(
- "anonymous",
- "anonymous",
- List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")));
-
- private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
- private final String registrationId;
-
- @Override
- public Mono getBearer() {
- final var authentication = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).orElse(ANONYMOUS);
- OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(registrationId).principal(authentication).build();
- final var authorizedClient = authorizedClientManager.authorize(authorizeRequest);
- final var token = authorizedClient.map(OAuth2AuthorizedClient::getAccessToken);
- return token.map(OAuth2AccessToken::getTokenValue);
- }
-}
\ No newline at end of file
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveBearerProvider.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveBearerProvider.java
deleted file mode 100644
index c057198c0..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveBearerProvider.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import reactor.core.publisher.Mono;
-
-public interface ReactiveBearerProvider {
- Mono getBearer();
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveSpringAddonsWebClientSupport.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveSpringAddonsWebClientSupport.java
deleted file mode 100644
index 0b8aa7c22..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/ReactiveSpringAddonsWebClientSupport.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.util.Optional;
-
-import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
-import org.springframework.util.StringUtils;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
-import org.springframework.web.reactive.function.client.ExchangeFunction;
-import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.service.annotation.HttpExchange;
-
-import reactor.core.publisher.Mono;
-
-/**
- *
- * Provides with {@link WebClient} builder instances pre-configured with:
- *
- *
- * - HTTP conector if proxy properties or environment variables are set
- * - Base URL
- * - authorization exchange function if Basic or OAuth2 Bearer
- *
- *
- *
- * Also provides with helper methods to get {@link HttpExchange @@HttpExchange} proxies with {@link WebClient}
- *
- *
- * /!\ Auto-configured only in servlet (WebMVC) applications and only if some {@link SpringAddonsRestProperties} are present /!\
- *
- *
- * @author Jerome Wacongne chl4mp@c4-soft.com
- * @see ReactiveSpringAddonsWebClientSupport an equivalent for reactive (Webflux) applications
- */
-public class ReactiveSpringAddonsWebClientSupport extends AbstractSpringAddonsWebClientSupport {
-
- private final Optional authorizedClientManager;
-
- public ReactiveSpringAddonsWebClientSupport(
- SystemProxyProperties systemProxyProperties,
- SpringAddonsRestProperties addonsProperties,
- BearerProvider forwardingBearerProvider,
- Optional authorizedClientManager) {
- super(systemProxyProperties, addonsProperties, forwardingBearerProvider);
- this.authorizedClientManager = authorizedClientManager;
- }
-
- @Override
- protected ExchangeFilterFunction oauth2RegistrationFilter(String registrationId) {
- return (ClientRequest request, ExchangeFunction next) -> {
- final var provider = Mono.justOrEmpty(authorizedClientManager.map(acm -> new ReactiveAuthorizedClientBearerProvider(acm, registrationId)));
- return provider.flatMap(ReactiveAuthorizedClientBearerProvider::getBearer).defaultIfEmpty("").flatMap(bearer -> {
- if (StringUtils.hasText(bearer)) {
- final var modified = ClientRequest.from(request).headers(headers -> headers.setBearerAuth(bearer)).build();
- return next.exchange(modified);
- }
- return next.exchange(request);
- });
- };
- }
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestClientHttpExchangeProxyFactoryBean.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestClientHttpExchangeProxyFactoryBean.java
new file mode 100644
index 000000000..8fc1ff2e2
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestClientHttpExchangeProxyFactoryBean.java
@@ -0,0 +1,59 @@
+package com.c4_soft.springaddons.rest;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.lang.Nullable;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.client.support.RestClientAdapter;
+import org.springframework.web.service.annotation.HttpExchange;
+import org.springframework.web.service.invoker.HttpExchangeAdapter;
+import org.springframework.web.service.invoker.HttpServiceProxyFactory;
+import lombok.NoArgsConstructor;
+
+/**
+ * Bean factory using {@link HttpServiceProxyFactory} to generate an instance of a
+ * {@link HttpExchange @HttpExchange} interface
+ *
+ * @param {@link HttpExchange @HttpExchange} interface to implement
+ * @see WebClientHttpExchangeProxyFactoryBean WebClientHttpExchangeProxyFactoryBean for an
+ * equivalent accepting only WebClient
+ * @see HttpExchangeProxyFactoryBean HttpExchangeProxyFactoryBean for an equivalent accepting both
+ * RestClient an WebClient
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+@NoArgsConstructor
+public class RestClientHttpExchangeProxyFactoryBean implements FactoryBean {
+ private Class httpExchangeClass;
+ private HttpExchangeAdapter adapter;
+
+ public RestClientHttpExchangeProxyFactoryBean(Class httpExchangeClass, RestClient client) {
+ this.httpExchangeClass = httpExchangeClass;
+ this.setClient(client);
+ }
+
+ @Override
+ @Nullable
+ public T getObject() throws Exception {
+ if (adapter == null || getObjectType() == null) {
+ throw new RestMisconfigurationException(
+ "Both of a RestClient and the @HttpExchange interface to implement must be configured on the HttpExchangeProxyFactoryBean before calling the getObject() method.");
+ }
+ return HttpServiceProxyFactory.builderFor(adapter).build().createClient(getObjectType());
+ }
+
+ @Override
+ public Class getObjectType() {
+ return httpExchangeClass;
+ }
+
+ public RestClientHttpExchangeProxyFactoryBean setHttpExchangeClass(
+ Class httpExchangeClass) {
+ this.httpExchangeClass = httpExchangeClass;
+ return this;
+ }
+
+ public RestClientHttpExchangeProxyFactoryBean setClient(RestClient client) {
+ this.adapter = RestClientAdapter.create(client);
+ return this;
+ }
+
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestMisconfigurationConfigurationException.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestMisconfigurationConfigurationException.java
deleted file mode 100644
index 6848ea67f..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestMisconfigurationConfigurationException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-public class RestMisconfigurationConfigurationException extends RuntimeException {
- private static final long serialVersionUID = 681577983030933423L;
-
- public RestMisconfigurationConfigurationException(String message) {
- super(message);
- }
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestMisconfigurationException.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestMisconfigurationException.java
new file mode 100644
index 000000000..d09603011
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/RestMisconfigurationException.java
@@ -0,0 +1,9 @@
+package com.c4_soft.springaddons.rest;
+
+public class RestMisconfigurationException extends RuntimeException {
+ private static final long serialVersionUID = 681577983030933423L;
+
+ public RestMisconfigurationException(String message) {
+ super(message);
+ }
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactory.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactory.java
new file mode 100644
index 000000000..dcdcfc23a
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactory.java
@@ -0,0 +1,120 @@
+package com.c4_soft.springaddons.rest;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientHttpRequestFactoryProperties;
+
+/**
+ *
+ * A wrapper around {@link SimpleClientHttpRequestFactory} that sends the request through an HTTP or
+ * SOCKS proxy when it is enabled and when the request URI does not match the NO_PROXY pattern.
+ *
+ *
+ * When going through a proxy, the Proxy-Authorization header is set if username and password are
+ * non-empty.
+ *
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+public class SpringAddonsClientHttpRequestFactory implements ClientHttpRequestFactory {
+ private final Optional nonProxyHostsPattern;
+ private final ClientHttpRequestFactory proxyDelegate;
+ private final ClientHttpRequestFactory noProxyDelegate;
+
+ public SpringAddonsClientHttpRequestFactory(SystemProxyProperties systemProperties,
+ ClientHttpRequestFactoryProperties addonsProperties) {
+ final var proxySupport = new ProxySupport(systemProperties, addonsProperties.getProxy());
+
+ this.nonProxyHostsPattern = proxySupport.isEnabled()
+ ? Optional.ofNullable(proxySupport.getNoProxy()).map(Pattern::compile)
+ : Optional.empty();
+
+ this.noProxyDelegate = from(addonsProperties);
+
+ if (proxySupport.isEnabled()) {
+ this.proxyDelegate = new ProxyAwareClientHttpRequestFactory(proxySupport, addonsProperties);
+ } else {
+ this.proxyDelegate = this.noProxyDelegate;
+ }
+ }
+
+ @Override
+ public @NonNull ClientHttpRequest createRequest(@NonNull URI uri, @NonNull HttpMethod httpMethod)
+ throws IOException {
+ final var delegate = nonProxyHostsPattern.filter(pattern -> {
+ final var matcher = pattern.matcher(uri.getHost());
+ return matcher.matches();
+ }).map(isNoProxy -> {
+ return noProxyDelegate;
+ }).orElse(proxyDelegate);
+
+ return delegate.createRequest(uri, httpMethod);
+ }
+
+ static Proxy.Type protocolToProxyType(String protocol) {
+ if (protocol == null) {
+ return null;
+ }
+ final var lower = protocol.toLowerCase();
+ if (lower.startsWith("http")) {
+ return Proxy.Type.HTTP;
+ }
+ if (lower.startsWith("socks")) {
+ return Proxy.Type.SOCKS;
+ }
+ return null;
+ }
+
+ private static SimpleClientHttpRequestFactory from(
+ ClientHttpRequestFactoryProperties properties) {
+ final var requestFactory = new SimpleClientHttpRequestFactory();
+ properties.getConnectTimeoutMillis().ifPresent(requestFactory::setConnectTimeout);
+ properties.getReadTimeoutMillis().ifPresent(requestFactory::setReadTimeout);
+ properties.getChunkSize().ifPresent(requestFactory::setChunkSize);
+ return requestFactory;
+ }
+
+ public static class ProxyAwareClientHttpRequestFactory implements ClientHttpRequestFactory {
+ private final SimpleClientHttpRequestFactory delegate;
+ private final @Nullable String username;
+ private final @Nullable String password;
+
+ public ProxyAwareClientHttpRequestFactory(ProxySupport proxySupport,
+ ClientHttpRequestFactoryProperties properties) {
+ this.username = proxySupport.getUsername();
+ this.password = proxySupport.getPassword();
+ this.delegate = SpringAddonsClientHttpRequestFactory.from(properties);
+ final var address =
+ new InetSocketAddress(proxySupport.getHostname().get(), proxySupport.getPort());
+ final var proxy = new Proxy(protocolToProxyType(proxySupport.getProtocol()), address);
+ this.delegate.setProxy(proxy);
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
+ final var request = delegate.createRequest(uri, httpMethod);
+ if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
+ final var base64 = Base64.getEncoder()
+ .encodeToString((username + ':' + password).getBytes(StandardCharsets.UTF_8));
+ request.getHeaders().set(HttpHeaders.PROXY_AUTHORIZATION, "Basic %s".formatted(base64));
+ }
+ return request;
+ }
+ }
+
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestBeans.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestBeans.java
deleted file mode 100644
index 3251bf666..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestBeans.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.util.Optional;
-
-import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Conditional;
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
-import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
-
-@AutoConfiguration
-public class SpringAddonsRestBeans {
-
- @ConditionalOnMissingBean
- @Bean
- BearerProvider bearerProvider() {
- return new DefaultBearerProvider();
- }
-
- @ConditionalOnWebApplication(type = Type.SERVLET)
- @Bean
- SpringAddonsRestClientSupport restClientSupport(
- SystemProxyProperties systemProxyProperties,
- SpringAddonsRestProperties restProperties,
- BearerProvider forwardingBearerProvider,
- Optional authorizedClientManager) {
- return new SpringAddonsRestClientSupport(systemProxyProperties, restProperties, forwardingBearerProvider, authorizedClientManager);
- }
-
- @Conditional(IsServletWithWebClientCondition.class)
- @Bean
- SpringAddonsWebClientSupport webClientSupport(
- SystemProxyProperties systemProxyProperties,
- SpringAddonsRestProperties addonsProperties,
- BearerProvider forwardingBearerProvider,
- Optional authorizedClientManager) {
- return new SpringAddonsWebClientSupport(systemProxyProperties, addonsProperties, forwardingBearerProvider, authorizedClientManager);
- }
-
- @ConditionalOnWebApplication(type = Type.REACTIVE)
- @Bean
- ReactiveSpringAddonsWebClientSupport reactiveWebClientSupport(
- SystemProxyProperties systemProxyProperties,
- SpringAddonsRestProperties addonsProperties,
- BearerProvider forwardingBearerProvider,
- Optional authorizedClientManager) {
- return new ReactiveSpringAddonsWebClientSupport(systemProxyProperties, addonsProperties, forwardingBearerProvider, authorizedClientManager);
- }
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestClientBeans.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestClientBeans.java
new file mode 100644
index 000000000..cdf30c9e5
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestClientBeans.java
@@ -0,0 +1,269 @@
+package com.c4_soft.springaddons.rest;
+
+import java.net.URL;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
+import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.web.client.RestClient;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.AuthorizationProperties;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientType;
+import lombok.Data;
+import lombok.Setter;
+
+/**
+ * Applied only in servlet applications.
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+@ConditionalOnWebApplication(type = Type.SERVLET)
+@AutoConfiguration
+public class SpringAddonsRestClientBeans {
+
+ @Bean
+ SpringAddonsRestClientBeanDefinitionRegistryPostProcessor springAddonsRestClientBeanDefinitionRegistryPostProcessor(
+ Environment environment) {
+ return new SpringAddonsRestClientBeanDefinitionRegistryPostProcessor(environment);
+ }
+
+ /**
+ *
+ * Post process the {@link BeanDefinitionRegistry} to add a {@link RestClient} (or
+ * {@link RestClient.builder}) bean definitions for each entry in
+ * "com.c4-soft.springaddons.rest.client".
+ *
+ *
+ *
+ * The bean names are by default the camelCase transformation of the client-id, suffixed with
+ * "Builder" if the expose-builder property is true.
+ *
+ *
+ * @author ch4mp@c4-soft.com
+ */
+ static class SpringAddonsRestClientBeanDefinitionRegistryPostProcessor
+ implements BeanDefinitionRegistryPostProcessor {
+
+ private final SpringAddonsRestProperties restProperties;
+ private final SystemProxyProperties systemProxyProperties;
+
+ @SuppressWarnings("unchecked")
+ public SpringAddonsRestClientBeanDefinitionRegistryPostProcessor(Environment environment) {
+ this.restProperties = Binder.get(environment)
+ .bind("com.c4-soft.springaddons.rest", SpringAddonsRestProperties.class)
+ .orElseThrow(() -> new RestMisconfigurationException(
+ "Could not read spring-addons REST properties"));
+
+ final var httpProxy = Optional
+ .ofNullable(Binder.get(environment).bind("http-proxy", String.class).orElse(null));
+ final var noProxy = Binder.get(environment).bind("no-proxy", List.class).orElse(List.of());
+ this.systemProxyProperties = new SystemProxyProperties(httpProxy, noProxy);
+ }
+
+ @Override
+ public void postProcessBeanDefinitionRegistry(@NonNull BeanDefinitionRegistry registry)
+ throws BeansException {
+
+ restProperties.getClient().entrySet().stream()
+ .filter(e -> ClientType.REST_CLIENT.equals(e.getValue().getType())
+ || ClientType.DEFAULT.equals(e.getValue().getType()))
+ .forEach(e -> {
+ final var builder = e.getValue().isExposeBuilder()
+ ? BeanDefinitionBuilder.genericBeanDefinition(RestClientBuilderFactoryBean.class)
+ : BeanDefinitionBuilder.genericBeanDefinition(RestClientFactoryBean.class);
+ builder.addPropertyValue("systemProxyProperties", systemProxyProperties);
+ builder.addPropertyValue("restProperties", restProperties);
+ builder.addAutowiredProperty("authorizedClientManager");
+ builder.addAutowiredProperty("authorizedClientRepository");
+ builder.addPropertyValue("clientId", e.getKey());
+ registry.registerBeanDefinition(restProperties.getClientBeanName(e.getKey()),
+ builder.getBeanDefinition());
+ });
+
+ /*
+ * FIXME: for some reason, this doesn't work: the clientRegistrationRepo is initialized before
+ * OAuth2 client properties are resolved and the HttpExchangeProxyFactoryBean named beans
+ * are not resolved when injecting T in components
+ */
+ // restProperties.getService().entrySet().stream().forEach(e -> {
+ // final var builder =
+ // BeanDefinitionBuilder.genericBeanDefinition(HttpExchangeProxyFactoryBean.class);
+ // try {
+ // builder.addConstructorArgValue(Class.forName(e.getValue().getHttpExchangeClass()));
+ // } catch (ClassNotFoundException e1) {
+ // throw new RestMisconfigurationConfigurationException(
+ // "Unknown class %s for REST service to auto-configure"
+ // .formatted(e.getValue().getHttpExchangeClass()));
+ // }
+ // builder.addConstructorArgReference(e.getValue().getClientBeanName());
+ // final var beanName = e.getValue().getBeanName().orElse(toCamelCase(e.getKey()));
+ // registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
+ // });
+
+ }
+ }
+
+ @Setter
+ public static class RestClientFactoryBean implements FactoryBean {
+ private String clientId;
+ private SystemProxyProperties systemProxyProperties;
+ private SpringAddonsRestProperties restProperties;
+ private Optional authorizedClientManager = Optional.empty();
+ private Optional authorizedClientRepository =
+ Optional.empty();
+
+ @Override
+ @Nullable
+ public RestClient getObject() throws Exception {
+ final var builderFactoryBean = new RestClientBuilderFactoryBean();
+ builderFactoryBean.setClientId(clientId);
+ builderFactoryBean.setSystemProxyProperties(systemProxyProperties);
+ builderFactoryBean.setRestProperties(restProperties);
+ builderFactoryBean.setAuthorizedClientManager(authorizedClientManager);
+ builderFactoryBean.setAuthorizedClientRepository(authorizedClientRepository);
+ return Optional.ofNullable(builderFactoryBean.getObject()).map(RestClient.Builder::build)
+ .orElse(null);
+ }
+
+ @Override
+ @Nullable
+ public Class> getObjectType() {
+ return RestClient.class;
+ }
+ }
+
+ @Data
+ public static class RestClientBuilderFactoryBean implements FactoryBean {
+ private String clientId;
+ private SystemProxyProperties systemProxyProperties = new SystemProxyProperties();
+ private SpringAddonsRestProperties restProperties = new SpringAddonsRestProperties();
+ private Optional authorizedClientManager;
+ private Optional authorizedClientRepository;
+
+
+ @Override
+ @Nullable
+ public RestClient.Builder getObject() throws Exception {
+ final var clientProps = Optional.ofNullable(restProperties.getClient().get(clientId))
+ .orElseThrow(() -> new RestConfigurationNotFoundException(clientId));
+
+ final var builder = RestClient.builder();
+
+ // Handle HTTP or SOCK proxy and set timeouts & chunck-size
+ builder.requestFactory(
+ new SpringAddonsClientHttpRequestFactory(systemProxyProperties, clientProps.getHttp()));
+
+ clientProps.getBaseUrl().map(URL::toString).ifPresent(builder::baseUrl);
+
+ setAuthorizationHeader(builder, clientProps.getAuthorization(), clientId);
+
+ return builder;
+ }
+
+ @Override
+ @Nullable
+ public Class> getObjectType() {
+ return RestClient.Builder.class;
+ }
+
+ protected void setAuthorizationHeader(RestClient.Builder clientBuilder,
+ AuthorizationProperties authProps, String clientId) {
+ if (authProps.getOauth2().isConfigured() && authProps.getBasic().isConfigured()) {
+ throw new RestMisconfigurationException(
+ "REST authorization configuration for %s can be made for either OAuth2 or Basic, but not both at a time"
+ .formatted(clientId));
+ }
+ if (authProps.getOauth2().isConfigured()) {
+ setBearerAuthorizationHeader(clientBuilder, authProps.getOauth2(), clientId);
+ } else if (authProps.getBasic().isConfigured()) {
+ setBasicAuthorizationHeader(clientBuilder, authProps.getBasic(), clientId);
+ }
+ }
+
+ protected void setBearerAuthorizationHeader(RestClient.Builder clientBuilder,
+ AuthorizationProperties.OAuth2Properties oauth2Props, String clientId) {
+ if (!oauth2Props.isConfValid()) {
+ throw new RestMisconfigurationException(
+ "REST OAuth2 authorization configuration for %s can be made for either a registration-id or resource server Bearer forwarding, but not both at a time"
+ .formatted(clientId));
+ }
+ if (oauth2Props.getOauth2RegistrationId().isPresent()) {
+ clientBuilder.requestInterceptor(
+ registrationClientHttpRequestInterceptor(oauth2Props.getOauth2RegistrationId().get()));
+ } else if (oauth2Props.isForwardBearer()) {
+ clientBuilder.requestInterceptor(forwardingClientHttpRequestInterceptor());
+ }
+ }
+
+ protected ClientHttpRequestInterceptor forwardingClientHttpRequestInterceptor() {
+ return (HttpRequest request, byte[] body, ClientHttpRequestExecution execution) -> {
+ final var auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth != null && auth.getPrincipal() instanceof OAuth2Token oauth2Token) {
+ request.getHeaders().setBearerAuth(oauth2Token.getTokenValue());
+ }
+ return execution.execute(request, body);
+ };
+ }
+
+ protected ClientHttpRequestInterceptor registrationClientHttpRequestInterceptor(
+ String registrationId) {
+ if (authorizedClientManager.isEmpty()) {
+ throw new RestMisconfigurationException(
+ "OAuth2 client missconfiguration. Can't setup an OAuth2 Bearer request interceptor because there is no authorizedClientManager bean.");
+ }
+ final var interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager.get());
+ interceptor.setClientRegistrationIdResolver((HttpRequest request) -> registrationId);
+ authorizedClientRepository
+ .map(OAuth2ClientHttpRequestInterceptor::authorizationFailureHandler)
+ .ifPresent(interceptor::setAuthorizationFailureHandler);
+ return interceptor;
+ }
+
+ protected void setBasicAuthorizationHeader(RestClient.Builder clientBuilder,
+ AuthorizationProperties.BasicAuthProperties authProps, String clientId) {
+ if (authProps.getEncodedCredentials().isPresent()) {
+ if (authProps.getUsername().isPresent() || authProps.getPassword().isPresent()
+ || authProps.getCharset().isPresent()) {
+ throw new RestMisconfigurationException(
+ "REST Basic authorization for %s is misconfigured: when encoded-credentials is provided, username, password and charset must be absent."
+ .formatted(clientId));
+ }
+ } else {
+ if (authProps.getUsername().isEmpty() || authProps.getPassword().isEmpty()) {
+ throw new RestMisconfigurationException(
+ "REST Basic authorization for %s is misconfigured: when encoded-credentials is empty, username & password are required."
+ .formatted(clientId));
+ }
+ }
+ clientBuilder.requestInterceptor((request, body, execution) -> {
+ authProps.getEncodedCredentials().ifPresent(request.getHeaders()::setBasicAuth);
+ authProps.getCharset().ifPresentOrElse(
+ charset -> request.getHeaders().setBasicAuth(authProps.getUsername().get(),
+ authProps.getPassword().get(), charset),
+ () -> request.getHeaders().setBasicAuth(authProps.getUsername().get(),
+ authProps.getPassword().get()));
+ return execution.execute(request, body);
+ });
+ }
+
+ }
+
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestClientSupport.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestClientSupport.java
deleted file mode 100644
index ba07217bf..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestClientSupport.java
+++ /dev/null
@@ -1,244 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.Proxy;
-import java.net.URI;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.Map;
-import java.util.Optional;
-import java.util.regex.Pattern;
-
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.client.ClientHttpRequest;
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.http.client.SimpleClientHttpRequestFactory;
-import org.springframework.lang.NonNull;
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
-import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
-import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
-import org.springframework.util.StringUtils;
-import org.springframework.web.client.RestClient;
-import org.springframework.web.client.support.RestClientAdapter;
-import org.springframework.web.service.annotation.HttpExchange;
-import org.springframework.web.service.invoker.HttpServiceProxyFactory;
-
-import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.AuthorizationProperties;
-
-import lombok.Data;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- *
- * Helps building {@link RestClient} instances. Main features are:
- *
- *
- * - providing with builders pre-configured for OAuth2: add a Bearer Authorization header provided by the
- * {@link OAuth2AuthorizedClientManager} for a given registration-id or by a {@link BearerProvider} (taking the Bearer from the security
- * context to forward it)
- * - providing with helper methods to get a HTTP service from the {@link HttpServiceProxyFactory} and application properties
- *
- *
- *
- * When spring-addons {@link SpringAddonsRestProperties.RestClientProperties.AuthorizationProperties.OAuth2Properties#forwardBearer} is
- * true, the Bearer is taken from the {@link BearerProvider} in the context, {@link DefaultBearerProvider} by default which works only with
- * {@link JwtAuthenticationToken} or {@link BearerTokenAuthentication}. You must provide with your own {@link BearerProvider} bean if your
- * security configuration populates the security context with something else.
- *
- *
- * /!\ Auto-configured only in servlet (WebMVC) applications and only if some {@link SpringAddonsRestProperties} are present /!\
- *
- *
- * @author Jerome Wacongne chl4mp@c4-soft.com
- */
-@Data
-@Slf4j
-public class SpringAddonsRestClientSupport {
-
- private final ProxySupport proxySupport;
-
- private final Map restClientProperties;
-
- /**
- * A {@link BearerProvider} to get the Bearer from the request security context
- */
- private final BearerProvider forwardingBearerProvider;
-
- private final Optional authorizedClientManager;
-
- public SpringAddonsRestClientSupport(
- SystemProxyProperties systemProxyProperties,
- SpringAddonsRestProperties restProperties,
- BearerProvider forwardingBearerProvider,
- Optional authorizedClientManager) {
- super();
- this.proxySupport = new ProxySupport(systemProxyProperties, restProperties);
- this.restClientProperties = restProperties.getClient();
- this.forwardingBearerProvider = forwardingBearerProvider;
- this.authorizedClientManager = authorizedClientManager;
- }
-
- public RestClient.Builder client() {
- final var builder = RestClient.builder();
- proxySupport.getHostname().map(proxyHostname -> new SpringAddonsClientHttpRequestFactory(proxySupport)).ifPresent(builder::requestFactory);
- if (proxySupport.getAddonsProperties().isEnabled()
- && StringUtils.hasText(proxySupport.getAddonsProperties().getUsername())
- && StringUtils.hasText(proxySupport.getAddonsProperties().getPassword())) {
- final var base64 = Base64.getEncoder().encodeToString(
- (proxySupport.getAddonsProperties().getUsername() + ':' + proxySupport.getAddonsProperties().getPassword())
- .getBytes(StandardCharsets.UTF_8));
- builder.defaultHeader(HttpHeaders.PROXY_AUTHORIZATION, "Basic %s".formatted(base64));
- }
-
- return builder;
- }
-
- /**
- * @param clientName key in "client" entries of {@link SpringAddonsRestProperties}
- * @return A {@link RestClient} Builder pre-configured with a base-URI and (optionally) with a Bearer Authorization
- */
- public RestClient.Builder client(String clientName) {
- final var clientProps = Optional.ofNullable(restClientProperties.get(clientName)).orElseThrow(() -> new RestConfigurationNotFoundException(clientName));
-
- final var clientBuilder = client();
-
- clientProps.getBaseUrl().map(URL::toString).ifPresent(clientBuilder::baseUrl);
-
- authorize(clientBuilder, clientProps.getAuthorization(), clientName);
-
- return clientBuilder;
- }
-
- /**
- * Uses the provided {@link RestClient} to proxy the httpServiceClass
- *
- * @param
- * @param client
- * @param httpServiceClass class of the #64;Service (with {@link HttpExchange} methods) to proxy with a {@link RestClient}
- * @return a #64;Service proxy with a {@link RestClient}
- */
- public T service(RestClient client, Class httpServiceClass) {
- return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build().createClient(httpServiceClass);
- }
-
- /**
- * Builds a {@link RestClient} with just the provided spring-addons {@link SpringAddonsRestProperties} and uses it to proxy the
- * httpServiceClass.
- *
- * @param
- * @param httpServiceClass class of the #64;Service (with {@link HttpExchange} methods) to proxy with a {@link RestClient}
- * @param clientName key in "client" entries of {@link SpringAddonsRestProperties}
- * @return a #64;Service proxy with a {@link RestClient}
- */
- public T service(String clientName, Class httpServiceClass) {
- return this.service(this.client(clientName).build(), httpServiceClass);
- }
-
- protected void authorize(RestClient.Builder clientBuilder, AuthorizationProperties authProps, String clientName) {
- if (authProps.getOauth2().isConfigured() && authProps.getBasic().isConfigured()) {
- throw new RestMisconfigurationConfigurationException(
- "REST authorization configuration for %s can be made for either OAuth2 or Basic, but not both at a time".formatted(clientName));
- }
- if (authProps.getOauth2().isConfigured()) {
- oauth2(clientBuilder, authProps.getOauth2(), clientName);
- } else if (authProps.getBasic().isConfigured()) {
- basic(clientBuilder, authProps.getBasic(), clientName);
- }
- }
-
- protected void oauth2(RestClient.Builder clientBuilder, AuthorizationProperties.OAuth2Properties oauth2Props, String clientName) {
- if (!oauth2Props.isConfValid()) {
- throw new RestMisconfigurationConfigurationException(
- "REST OAuth2 authorization configuration for %s can be made for either a registration-id or resource server Bearer forwarding, but not both at a time"
- .formatted(clientName));
- }
- oauth2Props.getOauth2RegistrationId().flatMap(this::oauth2RequestInterceptor).ifPresent(clientBuilder::requestInterceptor);
- if (oauth2Props.isForwardBearer()) {
- clientBuilder.requestInterceptor((request, body, execution) -> {
- forwardingBearerProvider.getBearer().ifPresent(bearer -> {
- request.getHeaders().setBearerAuth(bearer);
- });
- return execution.execute(request, body);
- });
- }
- }
-
- protected Optional oauth2RequestInterceptor(String registrationId) {
- if (authorizedClientManager.isEmpty()) {
- log.warn("OAuth2 client missconfiguration. Can't setup an OAuth2 Bearer request interceptor because there is no authorizedClientManager bean.");
- }
- return authorizedClientManager.map(acm -> (request, body, execution) -> {
- final var provider = new AuthorizedClientBearerProvider(acm, registrationId);
- provider.getBearer().ifPresent(bearer -> {
- request.getHeaders().setBearerAuth(bearer);
- });
- return execution.execute(request, body);
- });
- }
-
- protected void basic(RestClient.Builder clientBuilder, AuthorizationProperties.BasicAuthProperties authProps, String clientName) {
- if (authProps.getEncodedCredentials().isPresent()) {
- if (authProps.getUsername().isPresent() || authProps.getPassword().isPresent() || authProps.getCharset().isPresent()) {
- throw new RestMisconfigurationConfigurationException(
- "REST Basic authorization for %s is misconfigured: when encoded-credentials is provided, username, password and charset must be absent."
- .formatted(clientName));
- }
- } else {
- if (authProps.getUsername().isEmpty() || authProps.getPassword().isEmpty()) {
- throw new RestMisconfigurationConfigurationException(
- "REST Basic authorization for %s is misconfigured: when encoded-credentials is empty, username & password are required."
- .formatted(clientName));
- }
- }
- clientBuilder.requestInterceptor((request, body, execution) -> {
- authProps.getEncodedCredentials().ifPresent(request.getHeaders()::setBasicAuth);
- authProps.getCharset().ifPresentOrElse(
- charset -> request.getHeaders().setBasicAuth(authProps.getUsername().get(), authProps.getPassword().get(), charset),
- () -> request.getHeaders().setBasicAuth(authProps.getUsername().get(), authProps.getPassword().get()));
- return execution.execute(request, body);
- });
- }
-
- static Proxy.Type protocoleToProxyType(String protocol) {
- if (protocol == null) {
- return null;
- }
- final var lower = protocol.toLowerCase();
- if (lower.startsWith("http")) {
- return Proxy.Type.HTTP;
- }
- if (lower.startsWith("socks")) {
- return Proxy.Type.SOCKS;
- }
- return null;
- }
-
- static class SpringAddonsClientHttpRequestFactory extends SimpleClientHttpRequestFactory {
- private final Optional nonProxyHostsPattern;
- private final Optional proxyOpt;
-
- public SpringAddonsClientHttpRequestFactory(ProxySupport proxySupport) {
- super();
- this.nonProxyHostsPattern = Optional.ofNullable(proxySupport.getNoProxy()).map(Pattern::compile);
-
- this.proxyOpt = proxySupport.getHostname().map(proxyHostname -> {
- final var address = new InetSocketAddress(proxyHostname, proxySupport.getPort());
- return new Proxy(protocoleToProxyType(proxySupport.getProtocol()), address);
- });
-
- setConnectTimeout(proxySupport.getConnectTimeoutMillis());
- }
-
- @Override
- public @NonNull ClientHttpRequest createRequest(@NonNull URI uri, @NonNull HttpMethod httpMethod) throws IOException {
- super.setProxy(proxyOpt.filter(proxy -> {
- return nonProxyHostsPattern.map(pattern -> !pattern.matcher(uri.getHost()).matches()).orElse(true);
- }).orElse(null));
- return super.createRequest(uri, httpMethod);
- }
-
- }
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestProperties.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestProperties.java
index 66fdaaa66..80f975e4c 100644
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestProperties.java
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsRestProperties.java
@@ -6,138 +6,280 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
+import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
-
+import org.springframework.web.service.annotation.HttpExchange;
import lombok.Data;
/**
- *
- * Configuration for HTTP or SOCKS proxy.
- *
- *
- * HTTP_PROXY and NO_PROXY standard environment variable are used only if com.c4-soft.springaddons.rest.proxy.hostname is left empty and
- * com.c4-soft.springaddons.rest.proxy.enabled is TRUE or null.
- *
- *
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
@Data
@AutoConfiguration
@ConfigurationProperties(prefix = "com.c4-soft.springaddons.rest")
public class SpringAddonsRestProperties {
- private ProxyProperties proxy = new ProxyProperties();
-
- private Map client = new HashMap<>();
-
- @Data
- @ConfigurationProperties
- public static class ProxyProperties {
- private boolean enabled = true;
- private String protocol = "http";
- private int port = 8080;
- private String username;
- private String password;
- private int connectTimeoutMillis = 10000;
-
- private Optional host = Optional.empty();
-
- private String nonProxyHostsPattern;
- }
-
- @Data
- @ConfigurationProperties
- public static class RestClientProperties {
- /**
- * Base URI used to build the REST client ({@link RestClient} or {@link WebClient})
- */
- private Optional baseUrl = Optional.empty();
-
- private AuthorizationProperties authorization = new AuthorizationProperties();
-
- public Optional getBaseUrl() {
- return baseUrl.map(t -> {
- try {
- return new URL(t);
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- });
- }
-
- @Data
- @ConfigurationProperties
- public static class AuthorizationProperties {
-
- private OAuth2Properties oauth2 = new OAuth2Properties();
-
- private BasicAuthProperties basic = new BasicAuthProperties();
-
- boolean isConfigured() {
- return oauth2.isConfigured() || basic.isConfigured();
- }
-
- boolean isConfValid() {
- return oauth2.isConfValid() && basic.isConfValid() && (!oauth2.isConfigured() || !basic.isConfigured());
- }
-
- @Data
- @ConfigurationProperties
- public static class OAuth2Properties {
- /**
- *
- * If provided, it is used to get an access token from the {@link OAuth2AuthorizedClientManager}.
- *
- *
- * Must reference a valid entry under spring.security.oauth2.client.registration
- *
- *
- * Mutually exclusive with forward-bearer property.
- *
- */
- private Optional oauth2RegistrationId = Optional.empty();
-
- /**
- *
- * If true, a {@link BearerProvider} is used to retrieve a Bearer token from the {@link Authentication} in the security context.
- *
- *
- * Mutually exclusive with auth2-registration-id property.
- *
- *
- * @see DefaultBearerProvider
- */
- private boolean forwardBearer = false;
-
- boolean isConfigured() {
- return forwardBearer || oauth2RegistrationId.isPresent();
- }
-
- boolean isConfValid() {
- return !forwardBearer || oauth2RegistrationId.isEmpty();
- }
- }
-
- @Data
- @ConfigurationProperties
- public static class BasicAuthProperties {
- private Optional username = Optional.empty();
- private Optional password = Optional.empty();
- private Optional charset = Optional.empty();
- private Optional encodedCredentials = Optional.empty();
-
- boolean isConfigured() {
- return encodedCredentials.isPresent() || username.isPresent();
- }
-
- boolean isConfValid() {
- return encodedCredentials.isEmpty() || (username.isEmpty() && password.isEmpty());
- }
- }
- }
- }
+
+ /**
+ * Expose {@link RestClient} or {@link WebClient} instances as named beans
+ */
+ private Map client = new HashMap<>();
+
+ // FIXME: enable when a way is found to generate and register service proxies as beans.
+ // For instance, have the HttpExchangeProxyFactoryBean definitions registered with a
+ // BeanDefinitionRegistryPostProcessor
+
+ // /**
+ // * Expose {@link HttpExchange @HttpExchange} proxies as named beans (generated using
+ // * {@link HttpServiceProxyFactory})
+ // */
+ // private Map service = new HashMap<>();
+
+ public String getClientBeanName(String clientId) {
+ if (!client.containsKey(clientId)) {
+ return null;
+ }
+ final var clientProperties = client.get(clientId);
+ return clientProperties.getBeanName()
+ .orElse(clientProperties.isExposeBuilder() ? toCamelCase(clientId) + "Builder"
+ : toCamelCase(clientId));
+ }
+
+ private static String toCamelCase(String in) {
+ if (in == null) {
+ return null;
+ }
+ if (!StringUtils.hasText(in)) {
+ return "";
+ }
+ String[] words = in.split("[\\W_]+");
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < words.length; i++) {
+ String word = words[i];
+ if (i == 0) {
+ word = word.isEmpty() ? word : word.toLowerCase();
+ } else {
+ word = word.isEmpty() ? word
+ : Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase();
+ }
+ builder.append(word);
+ }
+ return builder.toString();
+ }
+
+ @Data
+ public static class RestClientProperties {
+ /**
+ * Base URI used to build the REST client ({@link RestClient} or {@link WebClient})
+ */
+ private Optional baseUrl = Optional.empty();
+
+ /**
+ * Configure a {@link ClientHttpRequestInterceptor} or {@link ExchangeFilterFunction} to
+ * authorize requests (add a Basic or Bearer header to each request)
+ */
+ private AuthorizationProperties authorization = new AuthorizationProperties();
+
+ /**
+ * Configure the internal {@link SimpleClientHttpRequestFactory} with timeouts and HTTP or SOCKS
+ * proxy
+ */
+ private ClientHttpRequestFactoryProperties http = new ClientHttpRequestFactoryProperties();
+
+ /**
+ * Defines the type of the REST client. Default is {@link RestClient} in servlet applications
+ * and {@link WebClient} in reactive ones.
+ */
+ private ClientType type = ClientType.DEFAULT;
+
+ /**
+ * If true, what is exposed as a bean is the pre-configured {@link RestClient.Builder} or
+ * {@link WebClient.Builder}. This allows to add some more configuration. Don't forget to expose
+ * the resulting {@link RestClient} or {@link WebClient} as a named bean if you intend to use it
+ * as the REST client in an auto-configured {@link HttpExchange @HttpExchange} proxy.
+ */
+ private boolean exposeBuilder = false;
+
+ /**
+ *
+ * Override the auto-configured bean name which defaults to the camelCase version of the
+ * client-id, with the "Builder" suffix if expose-builder is true.
+ *
+ *
+ * For instance, "com.c4-soft.springaddons.rest.client.machin-client" will create a bean named
+ * machinClient or machinClientBuilder depending on
+ * "com.c4-soft.springaddons.rest.client.machin-client.expose-builder" value.
+ *
+ */
+ private Optional beanName = Optional.empty();
+
+ public Optional getBaseUrl() {
+ return baseUrl.map(t -> {
+ try {
+ return new URL(t);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Data
+ public static class AuthorizationProperties {
+
+ private OAuth2Properties oauth2 = new OAuth2Properties();
+
+ private BasicAuthProperties basic = new BasicAuthProperties();
+
+ boolean isConfigured() {
+ return oauth2.isConfigured() || basic.isConfigured();
+ }
+
+ boolean isConfValid() {
+ return oauth2.isConfValid() && basic.isConfValid()
+ && (!oauth2.isConfigured() || !basic.isConfigured());
+ }
+
+ @Data
+ public static class OAuth2Properties {
+ /**
+ *
+ * If provided, it is used to get an access token from the
+ * {@link OAuth2AuthorizedClientManager}.
+ *
+ *
+ * Must reference a valid entry under spring.security.oauth2.client.registration
+ *
+ *
+ * Mutually exclusive with forward-bearer property.
+ *
+ */
+ private Optional oauth2RegistrationId = Optional.empty();
+
+ /**
+ *
+ * If true, the access token is taken from the {@link Authentication} in the security
+ * context.
+ *
+ *
+ * Mutually exclusive with auth2-registration-id property.
+ *
+ */
+ private boolean forwardBearer = false;
+
+ boolean isConfigured() {
+ return forwardBearer || oauth2RegistrationId.isPresent();
+ }
+
+ boolean isConfValid() {
+ return !forwardBearer || oauth2RegistrationId.isEmpty();
+ }
+ }
+
+ @Data
+ public static class BasicAuthProperties {
+ private Optional username = Optional.empty();
+ private Optional password = Optional.empty();
+ private Optional charset = Optional.empty();
+ private Optional encodedCredentials = Optional.empty();
+
+ boolean isConfigured() {
+ return encodedCredentials.isPresent() || username.isPresent();
+ }
+
+ boolean isConfValid() {
+ return encodedCredentials.isEmpty() || (username.isEmpty() && password.isEmpty());
+ }
+ }
+ }
+
+ @Data
+ public static class ClientHttpRequestFactoryProperties {
+ /**
+ *
+ * Configure Proxy-Authorization header for authentication on a HTTP or SOCKS proxy. This
+ * header auto-configuration can be disable on each client.
+ *
+ *
+ * HTTP_PROXY and NO_PROXY standard environment variable are used only if
+ * "com.c4-soft.springaddons.rest.proxy.hostname" is left empty and
+ * "com.c4-soft.springaddons.rest.proxy.enabled" is TRUE or null. In other words, if the
+ * standard environment variables are correctly set, leaving "proxy" properties empty here is
+ * probably the best option.
+ *
+ */
+ private ProxyProperties proxy = new ProxyProperties();
+
+ /**
+ * Connection timeout in milliseconds.
+ */
+ private Optional connectTimeoutMillis = Optional.empty();
+
+ /**
+ * Read timeout in milliseconds.
+ */
+ private Optional readTimeoutMillis = Optional.empty();
+
+ /**
+ * Supported only with {@link RestClient}. Set the number of bytes to write in each chunk.
+ */
+ private Optional chunkSize = Optional.empty();
+
+ @Data
+ public static class ProxyProperties {
+ private boolean enabled = true;
+ private String protocol = "http";
+ private int port = 8080;
+ private String username;
+ private String password;
+ private int connectTimeoutMillis = 10000;
+
+ private Optional host = Optional.empty();
+
+ private String nonProxyHostsPattern;
+ }
+
+ }
+
+ public static enum ClientType {
+ DEFAULT, REST_CLIENT, WEB_CLIENT;
+ }
+ }
+
+ @Data
+ public static class RestServiceProperties {
+ /**
+ *
+ * Name of a {@link RestClient} or {@link WebClient} bean.
+ *
+ * Note that:
+ *
+ * - This bean does not have to be one of the auto-generated REST clients.
+ * - The value is a REST client bean name, not a "com.c4-soft.springaddons.rest.client"
+ * key, which is the ID of for an auto-generated REST client (or builder) bean.
+ * - As a reminder, auto-generated REST client beans hare named with a camel-case version of
+ * their ID. For instance "com.c4-soft.springaddons.rest.client.machin-client" properties would
+ * create a bean named "machinClient"
+ *
+ */
+ private String clientBeanName;
+
+ /**
+ * Fully qualified class name of the {@link HttpExchange} to implement
+ */
+ private String httpExchangeClass;
+
+ /**
+ *
+ * Override the auto-configured bean name which defaults to the camelCase version of the
+ * client-id.
+ *
+ */
+ private Optional beanName = Optional.empty();
+ }
}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServerWebClientBeans.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServerWebClientBeans.java
new file mode 100644
index 000000000..890fe4803
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServerWebClientBeans.java
@@ -0,0 +1,159 @@
+package com.c4_soft.springaddons.rest;
+
+import java.util.List;
+import java.util.Optional;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientType;
+import lombok.Setter;
+
+/**
+ * Applied only in reactive (WebFlux) applications.
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+@ConditionalOnWebApplication(type = Type.REACTIVE)
+@AutoConfiguration
+public class SpringAddonsServerWebClientBeans {
+
+ @Bean
+ SpringAddonsServerWebClientBeanDefinitionRegistryPostProcessor springAddonsWebClientBeanDefinitionRegistryPostProcessor(
+ Environment environment) {
+ return new SpringAddonsServerWebClientBeanDefinitionRegistryPostProcessor(environment);
+ }
+
+ /**
+ *
+ * Post process the {@link BeanDefinitionRegistry} to add a {@link WebClient} (or
+ * {@link WebClient.builder}) bean definitions for each entry in
+ * "com.c4-soft.springaddons.rest.client".
+ *
+ *
+ *
+ * Bean definitions include a base URI, header and {@link ReactorClientHttpConnector} for HTTP or
+ * SOCKS proxy, as well as exchange function for Basic or OAuth2 (Bearer) authorization.
+ *
+ *
+ *
+ * The bean names are by default the camelCase transformation of the client-id, suffixed with
+ * "Builder" if the expose-builder property is true.
+ *
+ *
+ * @author ch4mp@c4-soft.com
+ */
+ static class SpringAddonsServerWebClientBeanDefinitionRegistryPostProcessor
+ implements BeanDefinitionRegistryPostProcessor {
+
+ private final SpringAddonsRestProperties restProperties;
+ private final SystemProxyProperties systemProxyProperties;
+
+ @SuppressWarnings("unchecked")
+ public SpringAddonsServerWebClientBeanDefinitionRegistryPostProcessor(Environment environment) {
+ this.restProperties = Binder.get(environment)
+ .bind("com.c4-soft.springaddons.rest", SpringAddonsRestProperties.class)
+ .orElseThrow(() -> new RestMisconfigurationException(
+ "Could not read spring-addons REST properties"));
+
+ final var httpProxy = Optional
+ .ofNullable(Binder.get(environment).bind("http-proxy", String.class).orElse(null));
+ final var noProxy = Binder.get(environment).bind("no-proxy", List.class).orElse(List.of());
+ this.systemProxyProperties = new SystemProxyProperties(httpProxy, noProxy);
+ }
+
+ @Override
+ public void postProcessBeanDefinitionRegistry(@NonNull BeanDefinitionRegistry registry)
+ throws BeansException {
+
+ restProperties.getClient().entrySet().stream()
+ .filter(e -> ClientType.WEB_CLIENT.equals(e.getValue().getType())
+ || ClientType.DEFAULT.equals(e.getValue().getType()))
+ .forEach(e -> {
+ final var builder = e.getValue().isExposeBuilder()
+ ? BeanDefinitionBuilder
+ .genericBeanDefinition(ServerWebClientBuilderFactoryBean.class)
+ : BeanDefinitionBuilder.genericBeanDefinition(ServerWebClientFactoryBean.class);
+ builder.addPropertyValue("systemProxyProperties", systemProxyProperties);
+ builder.addPropertyValue("restProperties", restProperties);
+ builder.addAutowiredProperty("clientRegistrationRepository");
+ builder.addAutowiredProperty("authorizedClientRepository");
+ builder.addPropertyValue("clientId", e.getKey());
+ registry.registerBeanDefinition(restProperties.getClientBeanName(e.getKey()),
+ builder.getBeanDefinition());
+ });
+
+ /*
+ * FIXME: for some reason, registering HttpExchangeProxyFactoryBean definitions doesn't
+ * work: the clientRegistrationRepo is initialized before OAuth2 client properties are
+ * resolved and the HttpExchangeProxyFactoryBean named beans are not resolved when
+ * injecting T in components
+ */
+
+ }
+ }
+
+ @Setter
+ public static class ServerWebClientBuilderFactoryBean
+ extends AbstractWebClientBuilderFactoryBean {
+ private Optional clientRegistrationRepository;
+ private Optional authorizedClientRepository;
+
+ @Override
+ protected ExchangeFilterFunction registrationExchangeFilterFunction(
+ String Oauth2RegistrationId) {
+ return SpringAddonsServerWebClientSupport.registrationExchangeFilterFunction(
+ clientRegistrationRepository.get(), authorizedClientRepository.get(),
+ Oauth2RegistrationId);
+ }
+
+ @Override
+ protected ExchangeFilterFunction forwardingBearerExchangeFilterFunction() {
+ return SpringAddonsServerWebClientSupport.forwardingBearerExchangeFilterFunction();
+ }
+ }
+
+ @Setter
+ public static class ServerWebClientFactoryBean implements FactoryBean {
+ private String clientId;
+ private SystemProxyProperties systemProxyProperties;
+ private SpringAddonsRestProperties restProperties;
+ private Optional clientRegistrationRepository =
+ Optional.empty();
+ private Optional authorizedClientRepository =
+ Optional.empty();
+
+ @Override
+ @Nullable
+ public WebClient getObject() throws Exception {
+ final var builderFactoryBean = new ServerWebClientBuilderFactoryBean();
+ builderFactoryBean.setClientId(clientId);
+ builderFactoryBean.setSystemProxyProperties(systemProxyProperties);
+ builderFactoryBean.setRestProperties(restProperties);
+ builderFactoryBean.setClientRegistrationRepository(clientRegistrationRepository);
+ builderFactoryBean.setAuthorizedClientRepository(authorizedClientRepository);
+ return Optional.ofNullable(builderFactoryBean.getObject()).map(WebClient.Builder::build)
+ .orElse(null);
+ }
+
+ @Override
+ @Nullable
+ public Class> getObjectType() {
+ return WebClient.class;
+ }
+ }
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServerWebClientSupport.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServerWebClientSupport.java
new file mode 100644
index 000000000..47c0ff286
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServerWebClientSupport.java
@@ -0,0 +1,59 @@
+package com.c4_soft.springaddons.rest;
+
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
+import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.ExchangeFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+/**
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+public class SpringAddonsServerWebClientSupport {
+
+ /**
+ * @return Filter function to add Bearer authorization to {@link WebClient} requests in a WebFlux
+ * application. The access token being retrieved from the security context, the
+ * application must be a resource server. If the context is anonymous (the parent request
+ * is not authorized), then the child request is anonymous too (no authorization header is
+ * set).
+ */
+ public static ExchangeFilterFunction forwardingBearerExchangeFilterFunction() {
+ return (ClientRequest request, ExchangeFunction next) -> {
+ return ReactiveSecurityContextHolder.getContext().map(sch -> {
+ final var auth = sch.getAuthentication();
+ if (auth != null && auth.getPrincipal() instanceof AbstractOAuth2Token oauth2Token) {
+ return ClientRequest.from(request)
+ .headers(headers -> headers.setBearerAuth(oauth2Token.getTokenValue())).build();
+ }
+ return request;
+ }).or(Mono.just(request)).flatMap(next::exchange);
+ };
+ }
+
+ /**
+ *
+ * @param clientRegistrationRepository
+ * @param authorizedClientRepository
+ * @param registrationId the registration ID to use (a key in
+ * "spring.security.oauth2.client.registration" properties)
+ * @return Filter function to add Bearer authorization to {@link WebClient} requests in a WebFlux
+ * application. The access token being retrieved from an OAuth2 client registration, with
+ * client credentials in a resource server application, or any flow in an app is
+ * oauth2Login.
+ */
+ public static ExchangeFilterFunction registrationExchangeFilterFunction(
+ ReactiveClientRegistrationRepository clientRegistrationRepository,
+ ServerOAuth2AuthorizedClientRepository authorizedClientRepository, String registrationId) {
+ final var delegate = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
+ clientRegistrationRepository, authorizedClientRepository);
+ delegate.setDefaultClientRegistrationId(registrationId);
+ return delegate;
+ }
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServletWebClientBeans.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServletWebClientBeans.java
new file mode 100644
index 000000000..91e9dc9db
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServletWebClientBeans.java
@@ -0,0 +1,156 @@
+package com.c4_soft.springaddons.rest;
+
+import java.util.List;
+import java.util.Optional;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.core.env.Environment;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientType;
+import lombok.Setter;
+
+/**
+ * Applied only in servlet applications and only if {@link WebClient} is on the classpath.
+ *
+ * @author ch4mp@c4-soft.com
+ */
+@Conditional(IsServletWithWebClientCondition.class)
+@AutoConfiguration
+public class SpringAddonsServletWebClientBeans {
+
+ @Bean
+ SpringAddonsServletWebClientBeanDefinitionRegistryPostProcessor springAddonsWebClientBeanDefinitionRegistryPostProcessor(
+ Environment environment) {
+ return new SpringAddonsServletWebClientBeanDefinitionRegistryPostProcessor(environment);
+ }
+
+ /**
+ *
+ * Post process the {@link BeanDefinitionRegistry} to add a {@link WebClient} (or
+ * {@link WebClient.builder}) bean definitions for each entry in
+ * "com.c4-soft.springaddons.rest.client".
+ *
+ *
+ *
+ * Bean definitions include a base URI, header and {@link ReactorClientHttpConnector} for HTTP or
+ * SOCKS proxy, as well as exchange function for Basic or OAuth2 (Bearer) authorization.
+ *
+ *
+ *
+ * The bean names are by default the camelCase transformation of the client-id, suffixed with
+ * "Builder" if the expose-builder property is true.
+ *
+ *
+ * @author ch4mp@c4-soft.com
+ */
+ static class SpringAddonsServletWebClientBeanDefinitionRegistryPostProcessor
+ implements BeanDefinitionRegistryPostProcessor {
+
+ private final SpringAddonsRestProperties restProperties;
+ private final SystemProxyProperties systemProxyProperties;
+
+ @SuppressWarnings("unchecked")
+ public SpringAddonsServletWebClientBeanDefinitionRegistryPostProcessor(
+ Environment environment) {
+ this.restProperties = Binder.get(environment)
+ .bind("com.c4-soft.springaddons.rest", SpringAddonsRestProperties.class)
+ .orElseThrow(() -> new RestMisconfigurationException(
+ "Could not read spring-addons REST properties"));
+
+ final var httpProxy = Optional
+ .ofNullable(Binder.get(environment).bind("http-proxy", String.class).orElse(null));
+ final var noProxy = Binder.get(environment).bind("no-proxy", List.class).orElse(List.of());
+ this.systemProxyProperties = new SystemProxyProperties(httpProxy, noProxy);
+ }
+
+ @Override
+ public void postProcessBeanDefinitionRegistry(@NonNull BeanDefinitionRegistry registry)
+ throws BeansException {
+
+ restProperties.getClient().entrySet().stream()
+ .filter(e -> ClientType.WEB_CLIENT.equals(e.getValue().getType())).forEach(e -> {
+ final var builder = e.getValue().isExposeBuilder()
+ ? BeanDefinitionBuilder
+ .genericBeanDefinition(ServletWebClientBuilderFactoryBean.class)
+ : BeanDefinitionBuilder.genericBeanDefinition(ServletWebClientFactoryBean.class);
+ builder.addPropertyValue("systemProxyProperties", systemProxyProperties);
+ builder.addPropertyValue("restProperties", restProperties);
+ builder.addAutowiredProperty("clientRegistrationRepository");
+ builder.addAutowiredProperty("authorizedClientRepository");
+ builder.addPropertyValue("clientId", e.getKey());
+ registry.registerBeanDefinition(restProperties.getClientBeanName(e.getKey()),
+ builder.getBeanDefinition());
+ });
+
+ /*
+ * FIXME: for some reason, registering HttpExchangeProxyFactoryBean definitions doesn't
+ * work: the clientRegistrationRepo is initialized before OAuth2 client properties are
+ * resolved and the HttpExchangeProxyFactoryBean named beans are not resolved when
+ * injecting T in components
+ */
+
+ }
+ }
+
+ @Setter
+ public static class ServletWebClientBuilderFactoryBean
+ extends AbstractWebClientBuilderFactoryBean {
+ private Optional clientRegistrationRepository;
+ private Optional authorizedClientRepository;
+
+ @Override
+ protected ExchangeFilterFunction registrationExchangeFilterFunction(
+ String Oauth2RegistrationId) {
+ return SpringAddonsServletWebClientSupport.registrationExchangeFilterFunction(
+ clientRegistrationRepository.get(), authorizedClientRepository.get(),
+ Oauth2RegistrationId);
+ }
+
+ @Override
+ protected ExchangeFilterFunction forwardingBearerExchangeFilterFunction() {
+ return SpringAddonsServletWebClientSupport.forwardingBearerExchangeFilterFunction();
+ }
+ }
+
+ @Setter
+ public static class ServletWebClientFactoryBean implements FactoryBean {
+ private String clientId;
+ private SystemProxyProperties systemProxyProperties;
+ private SpringAddonsRestProperties restProperties;
+ private Optional clientRegistrationRepository = Optional.empty();
+ private Optional authorizedClientRepository =
+ Optional.empty();
+
+ @Override
+ @Nullable
+ public WebClient getObject() throws Exception {
+ final var builderFactoryBean = new ServletWebClientBuilderFactoryBean();
+ builderFactoryBean.setClientId(clientId);
+ builderFactoryBean.setSystemProxyProperties(systemProxyProperties);
+ builderFactoryBean.setRestProperties(restProperties);
+ builderFactoryBean.setClientRegistrationRepository(clientRegistrationRepository);
+ builderFactoryBean.setAuthorizedClientRepository(authorizedClientRepository);
+ return Optional.ofNullable(builderFactoryBean.getObject()).map(WebClient.Builder::build)
+ .orElse(null);
+ }
+
+ @Override
+ @Nullable
+ public Class> getObjectType() {
+ return WebClient.class;
+ }
+ }
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServletWebClientSupport.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServletWebClientSupport.java
new file mode 100644
index 000000000..883d91aaf
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsServletWebClientSupport.java
@@ -0,0 +1,55 @@
+package com.c4_soft.springaddons.rest;
+
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
+import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
+import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.ExchangeFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ *
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+public class SpringAddonsServletWebClientSupport {
+ /**
+ * @return Filter function to add Bearer authorization to {@link WebClient} requests in a servlet
+ * application. The access token being retrieved from the security context, the
+ * application must be a resource server. If the context is anonymous (the parent request
+ * is not authorized), then the child request is anonymous too (no authorization header is
+ * set).
+ */
+ public static ExchangeFilterFunction forwardingBearerExchangeFilterFunction() {
+ return (ClientRequest request, ExchangeFunction next) -> {
+ final var auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth != null && auth.getPrincipal() instanceof AbstractOAuth2Token oauth2Token) {
+ return next.exchange(ClientRequest.from(request)
+ .headers(headers -> headers.setBearerAuth(oauth2Token.getTokenValue())).build());
+ }
+ return next.exchange(request);
+ };
+ }
+
+ /**
+ *
+ * @param clientRegistrationRepository
+ * @param authorizedClientRepository
+ * @param registrationId the registration ID to use (a key in
+ * "spring.security.oauth2.client.registration" properties)
+ * @return Filter function to add Bearer authorization to {@link WebClient} requests in a servlet
+ * application. The access token being retrieved from an OAuth2 client registration, with
+ * client credentials in a resource server application, or any flow in an app is
+ * oauth2Login.
+ */
+ public static ExchangeFilterFunction registrationExchangeFilterFunction(
+ ClientRegistrationRepository clientRegistrationRepository,
+ OAuth2AuthorizedClientRepository authorizedClientRepository, String registrationId) {
+ final var delegate = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
+ clientRegistrationRepository, authorizedClientRepository);
+ delegate.setDefaultClientRegistrationId(registrationId);
+ return delegate;
+ }
+}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsWebClientSupport.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsWebClientSupport.java
deleted file mode 100644
index 318c379a6..000000000
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SpringAddonsWebClientSupport.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.c4_soft.springaddons.rest;
-
-import java.util.Optional;
-
-import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
-import org.springframework.web.reactive.function.client.ExchangeFunction;
-import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.service.annotation.HttpExchange;
-
-/**
- *
- * Provides with {@link WebClient} builder instances pre-configured with:
- *
- *
- * - HTTP conector if proxy properties or environment variables are set
- * - Base URL
- * - authorization exchange function if Basic or OAuth2 Bearer
- *
- *
- *
- * Also provides with helper methods to get {@link HttpExchange @@HttpExchange} proxies with {@link WebClient}
- *
- *
- * /!\ Auto-configured only in servlet (WebMVC) applications and only if some {@link SpringAddonsRestProperties} are present /!\
- *
- *
- * @author Jerome Wacongne chl4mp@c4-soft.com
- * @see ReactiveSpringAddonsWebClientSupport an equivalent for reactive (Webflux) applications
- */
-public class SpringAddonsWebClientSupport extends AbstractSpringAddonsWebClientSupport {
-
- private final Optional authorizedClientManager;
-
- public SpringAddonsWebClientSupport(
- SystemProxyProperties systemProxyProperties,
- SpringAddonsRestProperties addonsProperties,
- BearerProvider forwardingBearerProvider,
- Optional authorizedClientManager) {
- super(systemProxyProperties, addonsProperties, forwardingBearerProvider);
- this.authorizedClientManager = authorizedClientManager;
- }
-
- @Override
- protected ExchangeFilterFunction oauth2RegistrationFilter(String registrationId) {
- return (ClientRequest request, ExchangeFunction next) -> {
- final var provider = authorizedClientManager.map(acm -> new AuthorizedClientBearerProvider(acm, registrationId));
- if (provider.flatMap(AuthorizedClientBearerProvider::getBearer).isPresent()) {
- final var modified = ClientRequest.from(request).headers(headers -> headers.setBearerAuth(provider.get().getBearer().get())).build();
- return next.exchange(modified);
- }
- return next.exchange(request);
- };
- }
-}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SystemProxyProperties.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SystemProxyProperties.java
index cbfe6dd25..219cf8058 100644
--- a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SystemProxyProperties.java
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/SystemProxyProperties.java
@@ -4,43 +4,46 @@
import java.net.URL;
import java.util.List;
import java.util.Optional;
-
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
-
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
/**
*
* Configuration for HTTP or SOCKS proxy.
*
*
- * HTTP_PROXY and NO_PROXY standard environment variable are used only if com.c4-soft.springaddons.rest.proxy.hostname is left empty and
+ * HTTP_PROXY and NO_PROXY standard environment variable are used only if
+ * com.c4-soft.springaddons.rest.proxy.hostname is left empty and
* com.c4-soft.springaddons.rest.proxy.enabled is TRUE or null.
*
*
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
@Data
@AutoConfiguration
@ConfigurationProperties
+@AllArgsConstructor
+@NoArgsConstructor
public class SystemProxyProperties {
- /* also parse standard environment variables */
- @Value("${http_proxy:#{null}}")
- private Optional httpProxy = Optional.empty();
+ /* also parse standard environment variables */
+ @Value("${http_proxy:#{null}}")
+ private Optional httpProxy = Optional.empty();
- @Value("${no_proxy:}")
- private List noProxy = List.of();
+ @Value("${no_proxy:}")
+ private List noProxy = List.of();
- public Optional getHttpProxy() {
- return httpProxy.map(t -> {
- try {
- return new URL(t);
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- });
- }
+ public Optional getHttpProxy() {
+ return httpProxy.map(t -> {
+ try {
+ return new URL(t);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
}
diff --git a/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/WebClientHttpExchangeProxyFactoryBean.java b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/WebClientHttpExchangeProxyFactoryBean.java
new file mode 100644
index 000000000..6535d059e
--- /dev/null
+++ b/spring-addons-starter-rest/src/main/java/com/c4_soft/springaddons/rest/WebClientHttpExchangeProxyFactoryBean.java
@@ -0,0 +1,58 @@
+package com.c4_soft.springaddons.rest;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.lang.Nullable;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.support.WebClientAdapter;
+import org.springframework.web.service.annotation.HttpExchange;
+import org.springframework.web.service.invoker.HttpExchangeAdapter;
+import org.springframework.web.service.invoker.HttpServiceProxyFactory;
+import lombok.NoArgsConstructor;
+
+/**
+ * Bean factory using {@link HttpServiceProxyFactory} to generate an instance of a
+ * {@link HttpExchange @HttpExchange} interface
+ *
+ * @param {@link HttpExchange @HttpExchange} interface to implement
+ * @see RestClientHttpExchangeProxyFactoryBean RestClientHttpExchangeProxyFactoryBean for an
+ * equivalent accepting only RestClient
+ * @see HttpExchangeProxyFactoryBean HttpExchangeProxyFactoryBean for an equivalent accepting both
+ * RestClient an WebClient
+ * @author Jérôme Wacongne <ch4mp@c4-soft.com>
+ */
+@NoArgsConstructor
+public class WebClientHttpExchangeProxyFactoryBean implements FactoryBean {
+ private Class httpExchangeClass;
+ private HttpExchangeAdapter adapter;
+
+ public WebClientHttpExchangeProxyFactoryBean(Class httpExchangeClass, WebClient client) {
+ this.httpExchangeClass = httpExchangeClass;
+ this.setClient(client);
+ }
+
+ @Override
+ @Nullable
+ public T getObject() throws Exception {
+ if (adapter == null || getObjectType() == null) {
+ throw new RestMisconfigurationException(
+ "Both of a WebClient and the @HttpExchange interface to implement must be configured on the HttpExchangeProxyFactoryBean before calling the getObject() method.");
+ }
+ return HttpServiceProxyFactory.builderFor(adapter).build().createClient(getObjectType());
+ }
+
+ @Override
+ public Class getObjectType() {
+ return httpExchangeClass;
+ }
+
+ public WebClientHttpExchangeProxyFactoryBean setHttpExchangeClass(Class httpExchangeClass) {
+ this.httpExchangeClass = httpExchangeClass;
+ return this;
+ }
+
+ public WebClientHttpExchangeProxyFactoryBean setClient(WebClient client) {
+ this.adapter = WebClientAdapter.create(client);
+ return this;
+ }
+
+}
diff --git a/spring-addons-starter-rest/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-addons-starter-rest/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 85935af2c..a45703eb0 100644
--- a/spring-addons-starter-rest/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-addons-starter-rest/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,3 +1,5 @@
com.c4_soft.springaddons.rest.SystemProxyProperties
com.c4_soft.springaddons.rest.SpringAddonsRestProperties
-com.c4_soft.springaddons.rest.SpringAddonsRestBeans
\ No newline at end of file
+com.c4_soft.springaddons.rest.SpringAddonsRestClientBeans
+com.c4_soft.springaddons.rest.SpringAddonsServerWebClientBeans
+com.c4_soft.springaddons.rest.SpringAddonsServletWebClientBeans
\ No newline at end of file
diff --git a/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/AbstractSpringAddonsClientHttpRequestFactoryTest.java b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/AbstractSpringAddonsClientHttpRequestFactoryTest.java
new file mode 100644
index 000000000..fc4c0a7b4
--- /dev/null
+++ b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/AbstractSpringAddonsClientHttpRequestFactoryTest.java
@@ -0,0 +1,31 @@
+package com.c4_soft.springaddons.rest;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(classes = StubBootApplication.class)
+@ActiveProfiles("minimal")
+class AbstractSpringAddonsClientHttpRequestFactoryTest {
+ @Autowired
+ SpringAddonsClientHttpRequestFactory requestFactory;
+
+ protected HttpURLConnection getConnection(ClientHttpRequest request) throws NoSuchFieldException,
+ SecurityException, IllegalArgumentException, IllegalAccessException {
+ final var connectionField = request.getClass().getDeclaredField("connection");
+ connectionField.setAccessible(true);
+ return (HttpURLConnection) connectionField.get(request);
+ }
+
+ protected boolean isUsingProxy(String uri) throws NoSuchFieldException, SecurityException,
+ IllegalArgumentException, IllegalAccessException, IOException {
+ final var connection =
+ getConnection(requestFactory.createRequest(URI.create(uri), HttpMethod.GET));
+ return connection.usingProxy();
+ }
+}
diff --git a/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryDisabledTest.java b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryDisabledTest.java
new file mode 100644
index 000000000..c4ca8b4fd
--- /dev/null
+++ b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryDisabledTest.java
@@ -0,0 +1,23 @@
+package com.c4_soft.springaddons.rest;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(classes = StubBootApplication.class)
+@ActiveProfiles("disabled")
+class SpringAddonsClientHttpRequestFactoryDisabledTest
+ extends AbstractSpringAddonsClientHttpRequestFactoryTest {
+
+ @Test
+ void test() throws IOException, IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, SecurityException {
+ assertFalse(isUsingProxy("http://server.external.com/foo"));
+ assertFalse(isUsingProxy("http://localhost/foo"));
+ assertFalse(isUsingProxy("http://bravo-ch4mp/foo"));
+ assertFalse(isUsingProxy("http://server.corporate-domain.pf/foo"));
+ }
+
+}
diff --git a/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryFullTest.java b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryFullTest.java
new file mode 100644
index 000000000..5887ce6fd
--- /dev/null
+++ b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryFullTest.java
@@ -0,0 +1,23 @@
+package com.c4_soft.springaddons.rest;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(classes = StubBootApplication.class)
+@ActiveProfiles("full")
+class SpringAddonsClientHttpRequestFactoryFullTest
+ extends AbstractSpringAddonsClientHttpRequestFactoryTest {
+
+ @Test
+ void test() throws IOException, IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, SecurityException {
+ assertTrue(isUsingProxy("http://server.external.com/foo"));
+ assertFalse(isUsingProxy("http://localhost/foo"));
+ assertFalse(isUsingProxy("http://bravo-ch4mp/foo"));
+ assertFalse(isUsingProxy("http://server.corporate-domain.pf/foo"));
+ }
+}
diff --git a/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryMinimalTest.java b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryMinimalTest.java
new file mode 100644
index 000000000..7b0b62636
--- /dev/null
+++ b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryMinimalTest.java
@@ -0,0 +1,22 @@
+package com.c4_soft.springaddons.rest;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(classes = StubBootApplication.class)
+@ActiveProfiles("minimal")
+class SpringAddonsClientHttpRequestFactoryMinimalTest
+ extends AbstractSpringAddonsClientHttpRequestFactoryTest {
+
+ @Test
+ void test() throws IOException, IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, SecurityException {
+ assertTrue(isUsingProxy("http://server.external.com/foo"));
+ assertTrue(isUsingProxy("http://localhost/foo"));
+ assertTrue(isUsingProxy("http://bravo-ch4mp/foo"));
+ assertTrue(isUsingProxy("http://server.corporate-domain.pf/foo"));
+ }
+}
diff --git a/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryStdEnvVarsTest.java b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryStdEnvVarsTest.java
new file mode 100644
index 000000000..8e2ac376f
--- /dev/null
+++ b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/SpringAddonsClientHttpRequestFactoryStdEnvVarsTest.java
@@ -0,0 +1,23 @@
+package com.c4_soft.springaddons.rest;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(classes = StubBootApplication.class)
+@ActiveProfiles("std-env-vars")
+class SpringAddonsClientHttpRequestFactoryStdEnvVarsTest
+ extends AbstractSpringAddonsClientHttpRequestFactoryTest {
+
+ @Test
+ void test() throws IOException, IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, SecurityException {
+ assertTrue(isUsingProxy("http://server.external.com/foo"));
+ assertFalse(isUsingProxy("http://localhost/foo"));
+ assertFalse(isUsingProxy("http://bravo-ch4mp/foo"));
+ assertFalse(isUsingProxy("http://server.corporate-domain.pf/foo"));
+ }
+}
diff --git a/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/StubBootApplication.java b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/StubBootApplication.java
new file mode 100644
index 000000000..7bdabf512
--- /dev/null
+++ b/spring-addons-starter-rest/src/test/java/com/c4_soft/springaddons/rest/StubBootApplication.java
@@ -0,0 +1,14 @@
+package com.c4_soft.springaddons.rest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication class StubBootApplication {
+
+ @Bean
+ SpringAddonsClientHttpRequestFactory springAddonsClientHttpRequestFactory(
+ SystemProxyProperties systemProperties, SpringAddonsRestProperties addonsProperties) {
+ return new SpringAddonsClientHttpRequestFactory(systemProperties,
+ addonsProperties.getClient().get("test").getHttp());
+ }
+}
\ No newline at end of file
diff --git a/spring-addons-starter-rest/src/test/resources/application.properties b/spring-addons-starter-rest/src/test/resources/application.properties
index 6ed717db3..e9d922247 100644
--- a/spring-addons-starter-rest/src/test/resources/application.properties
+++ b/spring-addons-starter-rest/src/test/resources/application.properties
@@ -1,25 +1,28 @@
server.ssl.enabled=false
#---
-spring.config.activate.on-profile=host-port
-com.c4-soft.springaddons.proxy.host=mini-proxy
-com.c4-soft.springaddons.proxy.port=7080
+spring.config.activate.on-profile=minimal
+com.c4-soft.springaddons.rest.client.test.http.proxy.host=mini-proxy
+com.c4-soft.springaddons.rest.client.test.http.proxy.port=7080
#---
-spring.config.activate.on-profile=addons
-com.c4-soft.springaddons.proxy.type=socks5
-com.c4-soft.springaddons.proxy.host=corp-proxy
-com.c4-soft.springaddons.proxy.port=8080
-com.c4-soft.springaddons.proxy.username=toto
-com.c4-soft.springaddons.proxy.password=abracadabra
-com.c4-soft.springaddons.proxy.nonProxyHostsPattern=(localhost)|(bravo\\-ch4mp)|(.*\\.corporate\\-domain\\.com)
-com.c4-soft.springaddons.proxy.connect-timeout-millis=500
+spring.config.activate.on-profile=full
+com.c4-soft.springaddons.rest.client.test.http.proxy.enabled=true
+com.c4-soft.springaddons.rest.client.test.http.proxy.protocol=http
+com.c4-soft.springaddons.rest.client.test.http.proxy.host=corp-proxy
+com.c4-soft.springaddons.rest.client.test.http.proxy.port=8080
+com.c4-soft.springaddons.rest.client.test.http.proxy.username=toto
+com.c4-soft.springaddons.rest.client.test.http.proxy.password=abracadabra
+com.c4-soft.springaddons.rest.client.test.http.proxy.nonProxyHostsPattern=(localhost)|(bravo\\-ch4mp)|(.*\\.corporate\\-domain\\.pf)
+com.c4-soft.springaddons.rest.client.test.http.proxy.connect-timeout-millis=500
#---
-spring.config.activate.on-profile=disabled-proxy
-com.c4-soft.springaddons.proxy.enabled=false
+spring.config.activate.on-profile=disabled
+com.c4-soft.springaddons.rest.client.test.http.proxy.enabled=false
+com.c4-soft.springaddons.rest.client.test.http.proxy.host=mini-proxy
+com.c4-soft.springaddons.rest.client.test.http.proxy.port=7080
#---
spring.config.activate.on-profile=std-env-vars
-http_proxy=https://machin:truc@env-proxy:8080
-no_proxy=localhost,bravo-ch4mp,.env-domain.pf
\ No newline at end of file
+http-proxy=https://machin:truc@env-proxy:8080
+no-proxy=localhost,bravo-ch4mp,.corporate-domain.pf
\ No newline at end of file
diff --git a/starters/pom.xml b/starters/pom.xml
deleted file mode 100644
index 99921adfb..000000000
--- a/starters/pom.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
- 4.0.0
-
- com.c4-soft.springaddons
- spring-addons
- 7.8.13-SNAPSHOT
- ..
-
- starters
- pom
-
-
- spring-addons-starters-webclient
- spring-addons-starters-recaptcha
-
-
-
-
-
- com.c4-soft.springaddons.starter
- spring-addons-starters-webclient
- ${project.version}
-
-
-
-
-
diff --git a/starters/spring-addons-starters-recaptcha/pom.xml b/starters/spring-addons-starters-recaptcha/pom.xml
deleted file mode 100644
index 1fd81d453..000000000
--- a/starters/spring-addons-starters-recaptcha/pom.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
- 4.0.0
-
-
- com.c4-soft.springaddons
- starters
- 7.8.13-SNAPSHOT
- ..
-
- com.c4-soft.springaddons.starter
- spring-addons-starters-recaptcha
-
- https://github.com/ch4mpy/spring-addons/
-
- scm:git:git://github.com/ch4mpy/spring-addons.git
- scm:git:git@github.com:ch4mpy/spring-addons.git
- https://github.com/ch4mpy/spring-addons
- spring-addons-7.8.8
-
-
-
-
- org.springframework
- spring-web
-
-
- org.springframework.boot
- spring-boot
-
-
- org.springframework.boot
- spring-boot-autoconfigure
-
-
- com.c4-soft.springaddons
- spring-addons-starter-rest
-
-
-
- org.slf4j
- slf4j-api
-
-
- org.projectlombok
- lombok
- true
-
-
-
- org.springframework.boot
- spring-boot-configuration-processor
- true
-
-
-
diff --git a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaSettings.java b/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaSettings.java
deleted file mode 100644
index 93459b155..000000000
--- a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaSettings.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.c4_soft.springaddons.starter.recaptcha;
-
-import java.net.URL;
-
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-import lombok.Data;
-
-@Data
-@Component
-@ConfigurationProperties(prefix = "com.c4-soft.springaddons.recaptcha")
-public class C4ReCaptchaSettings {
- private String secretKey;
- @Value("${siteverify-url:https://www.google.com/recaptcha/api/siteverify}")
- private URL siteverifyUrl;
- private double v3Threshold = .5;
-}
diff --git a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaValidationService.java b/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaValidationService.java
deleted file mode 100644
index 7dbd8f8ab..000000000
--- a/starters/spring-addons-starters-recaptcha/src/main/java/com/c4_soft/springaddons/starter/recaptcha/C4ReCaptchaValidationService.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.c4_soft.springaddons.starter.recaptcha;
-
-import java.util.stream.Collectors;
-
-import org.springframework.http.MediaType;
-import org.springframework.stereotype.Service;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.web.client.RestClient;
-
-import com.c4_soft.springaddons.rest.SpringAddonsRestClientSupport;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * Usage:
- *
- *
- * if (Boolean.FALSE.equals(captcha.checkV2(reCaptcha).block())) {
- * throw new RuntimeException("Are you a robot?");
- * }
- *
- *
- * @author Jérôme Wacongne ch4mp@c4-soft.com
- */
-@Service
-@Slf4j
-public class C4ReCaptchaValidationService {
-
- private final RestClient client;
- private final String googleRecaptchaSecret;
- private final double v3Threshold;
-
- public C4ReCaptchaValidationService(C4ReCaptchaSettings settings, SpringAddonsRestClientSupport clientSupport) {
- this.client = clientSupport.client().baseUrl(settings.getSiteverifyUrl().toString()).build();
- this.googleRecaptchaSecret = settings.getSecretKey();
- this.v3Threshold = settings.getV3Threshold();
- }
-
- /**
- * Checks a reCaptcha V2 challenge response
- *
- * @param response answer provided by the client
- * @return true / false
- */
- public Boolean checkV2(String response) {
- final var dto = response(response, V2ValidationResponseDto.class);
- log.debug("reCaptcha result : {}", dto);
- return dto.isSuccess();
- }
-
- /**
- * Checks a reCaptcha V3 challenge response
- *
- * @param response answer provided by the client
- * @return a score between 0 and 1
- * @throws ReCaptchaValidationException if response wasn't a valid reCAPTCHA token for your site or score is below configured threshold
- */
- public Double checkV3(String response) throws ReCaptchaValidationException {
- final var dto = response(response, V3ValidationResponseDto.class);
- log.debug("reCaptcha result : {}", dto);
- if (!dto.isSuccess()) {
- throw new ReCaptchaValidationException(
- String.format("Failed to validate reCaptcha: %s %s", response, dto.getErrorCodes().stream().collect(Collectors.joining("[", ", ", "]"))));
- }
- if (dto.getScore() < v3Threshold) {
- throw new ReCaptchaValidationException(String.format("Failed to validate reCaptcha: %s. Score is %f", response, dto.getScore()));
- }
- return dto.getScore();
- }
-
- private T response(String response, Class dtoType) {
- final var formData = new LinkedMultiValueMap<>();
- formData.add("secret", googleRecaptchaSecret);
- formData.add("response", response);
- return client.post().contentType(MediaType.APPLICATION_FORM_URLENCODED).body(formData).retrieve().toEntity(dtoType).getBody();
- }
-}
diff --git a/starters/spring-addons-starters-webclient/README.md b/starters/spring-addons-starters-webclient/README.md
deleted file mode 100644
index 58ebf1010..000000000
--- a/starters/spring-addons-starters-webclient/README.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# spring-boot starter for `C4WebClientBuilderFactoryService`
-Tiny lib exposing a factory service for WebClient builders with proxy configuration from properties
-
-## Usage
-Thanks to `@AutoConfiguration` magic, only 3 very simple steps are needed:
-### Put this library on your classpath
-```xml
-
- com.c4-soft.springaddons.starter
- spring-addons-starters-webclient
- ${spring-addons.version}
-
-```
-
-### Configuration
-Two sources of configuration properties are evaluated:
-- `com.c4-soft.springaddons.proxy.*`
-- `http_proxy` and `no_proxy`
-
-`com.c4-soft.springaddons.proxy.*` have precedence if `host` is not empty. This means that the standard `HTTP_PROXY` and `NO_PROXY` environment variables will be used only if:
-- `com.c4-soft.springaddons.proxy.host` is left empty
-- `com.c4-soft.springaddons.proxy.enabled` is left empty or is explicitly set to `true`
-
-There is a noteworthy difference between the two possible properties for configuring proxy bypass:
-- `com.c4-soft.springaddons.proxy.non-proxy-hosts-pattern` expects Java RegEx (for instance `(localhost)|(bravo\\-ch4mp)|(.*\\.corporate\\-domain\\.com)`)
-- `no_proxy` expects comma separated list of hosts / domains (for instance `localhost,bravo-ch4mp,.env-domain.pf`)
-
-### Inject `C4WebClientBuilderFactoryService` where you need it
-```java
-@RestController
-@RequestMapping("/")
-@RequiredArgsConstructor
-public class GreetingController {
- private final C4WebClientBuilderFactoryService webClientBuilderFactory;
- ...
-}
-```
-
-## Sample
-You might refer to unit-tests for a sample spring-boot app:
-```java
-@SpringBootApplication
-public class WebClientSampleApp {
- public static void main(String[] args) {
- new SpringApplicationBuilder(WebClientSampleApp.class).web(WebApplicationType.REACTIVE).run(args);
- }
-
- @RestController
- @RequestMapping("/sample")
- @RequiredArgsConstructor
- public static class SampleController {
- private final C4WebClientBuilderFactoryService webClientBuilderFactory;
-
- @GetMapping("/delegating")
- public Mono calling() throws MalformedURLException {
- return webClientBuilderFactory.get(new URL("http://localhost:8080")).build().get().uri("/sample/delegate").retrieve().bodyToMono(String.class);
- }
-
- @GetMapping("/delegate")
- public Mono remote() {
- return Mono.just("Hello!");
- }
- }
-}
-```
-Properties file uses profiles to try various configuration scenarios:
-```properties
-server.port=8080
-server.ssl.enabled=false
-
-#---
-spring.config.activate.on-profile=host-port
-com.c4-soft.springaddons.proxy.host=mini-proxy
-com.c4-soft.springaddons.proxy.port=7080
-
-#---
-spring.config.activate.on-profile=addons
-com.c4-soft.springaddons.proxy.type=socks5
-com.c4-soft.springaddons.proxy.host=corp-proxy
-com.c4-soft.springaddons.proxy.port=8080
-com.c4-soft.springaddons.proxy.username=toto
-com.c4-soft.springaddons.proxy.password=abracadabra
-com.c4-soft.springaddons.proxy.nonProxyHostsPattern=(localhost)|(bravo\\-ch4mp)|(.*\\.corporate\\-domain\\.com)
-com.c4-soft.springaddons.proxy.connect-timeout-millis=500
-
-#---
-spring.config.activate.on-profile=disabled-proxy
-com.c4-soft.springaddons.proxy.enabled=false
-
-#---
-spring.config.activate.on-profile=std-env-vars
-http_proxy=https://machin:truc@env-proxy:8080
-no_proxy=localhost,bravo-ch4mp,.env-domain.pf
-```
\ No newline at end of file
diff --git a/starters/spring-addons-starters-webclient/pom.xml b/starters/spring-addons-starters-webclient/pom.xml
deleted file mode 100644
index d9c57d47f..000000000
--- a/starters/spring-addons-starters-webclient/pom.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
- 4.0.0
-
-
- com.c4-soft.springaddons
- starters
- 7.8.13-SNAPSHOT
- ..
-
- com.c4-soft.springaddons.starter
- spring-addons-starters-webclient
-
- https://github.com/ch4mpy/spring-addons/
-
- scm:git:git://github.com/ch4mpy/spring-addons.git
- scm:git:git@github.com:ch4mpy/spring-addons.git
- https://github.com/ch4mpy/spring-addons
- spring-addons-7.8.8
-
-
-
-
- org.springframework
- spring-context
-
-
- org.springframework.boot
- spring-boot
-
-
- org.springframework.boot
- spring-boot-autoconfigure
-
-
- org.springframework.boot
- spring-boot-starter-webflux
-
-
-
- org.slf4j
- slf4j-api
-
-
- org.projectlombok
- lombok
- true
-
-
-
- org.springframework.boot
- spring-boot-configuration-processor
- true
-
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
-
diff --git a/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/C4ProxySettings.java b/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/C4ProxySettings.java
deleted file mode 100644
index a00fe0c08..000000000
--- a/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/C4ProxySettings.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-
-import lombok.AccessLevel;
-import lombok.Data;
-import lombok.Getter;
-import reactor.netty.transport.ProxyProvider;
-
-/**
- *
- * Configuration for HTTP or SOCKS proxy.
- *
- *
- * HTTP_PROXY and NO_PROXY standard environment variable are used only if com.c4-soft.springaddons.proxy.hostname is left empty and
- * com.c4-soft.springaddons.proxy.enabled is TRUE or null.
- *
- *
- * @author Jérôme Wacongne <ch4mp#64;c4-soft.com>
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "com.c4-soft.springaddons.proxy")
-public class C4ProxySettings {
- private Boolean enabled;
- private ProxyProvider.Proxy type = ProxyProvider.Proxy.HTTP;
- @Getter(AccessLevel.NONE)
- private Optional host;
- private Integer port;
- private String username;
- private String password;
- @Getter(AccessLevel.NONE)
- private String nonProxyHostsPattern;
- private long connectTimeoutMillis = 10000;
-
- /* also parse standard environment variables */
- @Getter(AccessLevel.NONE)
- private Optional httpProxy;
-
- @Getter(AccessLevel.NONE)
- @Value("${no_proxy:#{T(java.util.List).of()}}")
- private List noProxy = List.of();
-
- @Value("${com.c4-soft.springaddons.proxy.host:#{null}}")
- public void setHost(String host) {
- this.host = StringUtils.hasText(host) ? Optional.of(host) : Optional.empty();
- }
-
- @Value("${http_proxy:#{null}}")
- public void setHttpProxy(String url) throws MalformedURLException {
- this.httpProxy = StringUtils.hasText(url) ? Optional.of(new URL(url)) : Optional.empty();
- }
-
- public String getHostname() {
- if (Boolean.FALSE.equals(enabled)) {
- return null;
- }
- return host.orElse(httpProxy.map(URL::getHost).orElse(null));
- }
-
- public ProxyProvider.Proxy getType() {
- if (Boolean.FALSE.equals(enabled)) {
- return null;
- }
- return host.map(h -> type).orElse(httpProxy.map(URL::getProtocol).map(C4ProxySettings::getProtocoleType).orElse(null));
- }
-
- public Integer getPort() {
- if (Boolean.FALSE.equals(enabled)) {
- return null;
- }
- return host.map(h -> port).orElse(httpProxy.map(URL::getPort).orElse(null));
- }
-
- public String getUsername() {
- if (Boolean.FALSE.equals(enabled)) {
- return null;
- }
- return host.map(h -> username).orElse(httpProxy.map(URL::getUserInfo).map(C4ProxySettings::getUserinfoName).orElse(null));
- }
-
- public String getPassword() {
- if (Boolean.FALSE.equals(enabled)) {
- return null;
- }
- return host.map(h -> password).orElse(httpProxy.map(URL::getUserInfo).map(C4ProxySettings::getUserinfoPassword).orElse(null));
- }
-
- public String getNoProxy() {
- if (Boolean.FALSE.equals(enabled)) {
- return null;
- }
- return host.map(h -> nonProxyHostsPattern).orElse(getNonProxyHostsPattern(noProxy));
- }
-
- static ProxyProvider.Proxy getProtocoleType(String protocol) {
- if (protocol == null) {
- return null;
- }
- final var lower = protocol.toLowerCase();
- if (lower.startsWith("http")) {
- return ProxyProvider.Proxy.HTTP;
- }
- if (lower.startsWith("socks4")) {
- return ProxyProvider.Proxy.SOCKS4;
- }
- return ProxyProvider.Proxy.SOCKS5;
- }
-
- static String getUserinfoName(String userinfo) {
- if (userinfo == null) {
- return null;
- }
- return userinfo.split(":")[0];
- }
-
- static String getUserinfoPassword(String userinfo) {
- if (userinfo == null) {
- return null;
- }
- final var splits = userinfo.split(":");
- return splits.length < 2 ? null : splits[1];
- }
-
- static String getNonProxyHostsPattern(List noProxy) {
- if (noProxy == null || noProxy.isEmpty()) {
- return null;
- }
- return noProxy.stream().map(host -> host.replace(".", "\\.")).map(host -> host.replace("-", "\\-"))
- .map(host -> host.startsWith("\\.") ? ".*" + host : host).collect(Collectors.joining(")|(", "(", ")"));
- }
-}
diff --git a/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/C4WebClientBuilderFactoryService.java b/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/C4WebClientBuilderFactoryService.java
deleted file mode 100644
index 24fb567ef..000000000
--- a/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/C4WebClientBuilderFactoryService.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import java.net.URL;
-import java.util.Optional;
-
-import org.springframework.http.client.reactive.ReactorClientHttpConnector;
-import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-import org.springframework.web.reactive.function.client.WebClient;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import reactor.netty.http.client.HttpClient;
-
-/**
- * @author Jérôme Wacongne ch4mp@c4-soft.com
- */
-@Service
-@Slf4j
-@RequiredArgsConstructor
-public class C4WebClientBuilderFactoryService {
-
- private final C4ProxySettings settings;
-
- public WebClient.Builder get() {
- return get(null);
- }
-
- public WebClient.Builder get(URL baseUrl) {
- final var builder = WebClient.builder();
- Optional.ofNullable(baseUrl).map(URL::toString).ifPresent(builder::baseUrl);
- if (Boolean.FALSE.equals(settings.getEnabled()) || !StringUtils.hasText(settings.getHostname())) {
- return builder;
- }
- log.debug("Building ReactorClientHttpConnector with {}", settings);
- final var connector = new ReactorClientHttpConnector(
- HttpClient.create().proxy(
- proxy -> proxy.type(settings.getType()).host(settings.getHostname()).port(settings.getPort()).username(settings.getUsername())
- .password(username -> settings.getPassword()).nonProxyHosts(settings.getNoProxy())
- .connectTimeoutMillis(settings.getConnectTimeoutMillis())));
-
- return builder.clientConnector(connector);
- }
-}
diff --git a/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/SpringBootAutoConfiguration.java b/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/SpringBootAutoConfiguration.java
deleted file mode 100644
index 53b0acd89..000000000
--- a/starters/spring-addons-starters-webclient/src/main/java/com/c4_soft/springaddons/starter/webclient/SpringBootAutoConfiguration.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.context.annotation.Import;
-
-@AutoConfiguration
-@Import({ C4ProxySettings.class, C4WebClientBuilderFactoryService.class })
-public class SpringBootAutoConfiguration {
-}
diff --git a/starters/spring-addons-starters-webclient/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/starters/spring-addons-starters-webclient/src/main/resources/META-INF/additional-spring-configuration-metadata.json
deleted file mode 100644
index 2b2d45424..000000000
--- a/starters/spring-addons-starters-webclient/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ /dev/null
@@ -1,108 +0,0 @@
-{
- "groups": [
- {
- "name": "com.c4-soft.springaddons.proxy",
- "type": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "Proxy settings set to org.springframework.http.client.reactive.ReactorClientHttpConnector injected into built org.springframework.web.reactive.function.client.WebClient"
- }
- ],
- "properties": [
- {
- "name": "com.c4-soft.springaddons.proxy.enabled",
- "type": "java.lang.Boolean",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "defaultValue": null,
- "description": "If false, WebClient proxy configuration is disabled"
- },
- {
- "name": "com.c4-soft.springaddons.proxy.connect-timeout-millis",
- "type": "java.lang.Long",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "defaultValue": 10000,
- "description": "Delay in ms to connect to proxy before timeout"
- },
- {
- "name": "com.c4-soft.springaddons.proxy.host",
- "type": "java.lang.String",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "The proxy host to connect to."
- },
- {
- "name": "com.c4-soft.springaddons.proxy.non-proxy-hosts-pattern",
- "type": "java.lang.String",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "Regular expression (using java.util.regex) for a configuredlist of hosts that should be reached directly, bypassing the proxy."
- },
- {
- "name": "com.c4-soft.springaddons.proxy.password",
- "type": "java.lang.String",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "The proxy password for provided username."
- },
- {
- "name": "com.c4-soft.springaddons.proxy.port",
- "type": "java.lang.Short",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "The proxy port."
- },
- {
- "name": "com.c4-soft.springaddons.proxy.type",
- "type": "reactor.netty.transport.ProxyProvider$Proxy",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "defaultValue": "reactor.netty.transport.ProxyProvider.Proxy.HTTP",
- "description": "The proxy type."
- },
- {
- "name": "com.c4-soft.springaddons.proxy.username",
- "type": "java.lang.String",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "The proxy username."
- },
- {
- "name": "http_proxy",
- "type": "java.lang.String",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "The complete proxy URL as used in standard HTTP_PROXY environment variable."
- },
- {
- "name": "no_proxy",
- "type": "java.lang.String",
- "sourceType": "com.c4_soft.springaddons.starter.webclient.C4ProxySettings",
- "description": "A list of hosts / domains for which a direct connection should be applied. The format is NO_PROXY standard environment variable one."
- }
- ],
- "hints": [
- {
- "name": "com.c4-soft.springaddons.proxy.non-proxy-hosts-pattern",
- "values": [
- {
- "value": "(localhost)|(bravo\\-ch4mp)|(.*\\.corporate\\-domain\\.com)",
- "description": "Regular expression (using java.util.regex) for a configuredlist of hosts that should be reached directly, bypassing the proxy."
- }
- ]
- },
- {
- "name": "http_proxy",
- "values": [
- {
- "value": "http://username:password@proxy-host:8080",
- "description": "Full URL with protocol, username, password and port"
- }
- ]
- },
- {
- "name": "no_proxy",
- "values": [
- {
- "value": "host.corporate.com",
- "description": "Exact match on domain / host"
- },
- {
- "value": ".corporate.com",
- "description": "All sub-domains / hosts"
- }
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/starters/spring-addons-starters-webclient/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/spring-addons-starters-webclient/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
deleted file mode 100644
index 245be42dc..000000000
--- a/starters/spring-addons-starters-webclient/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ /dev/null
@@ -1 +0,0 @@
-com.c4_soft.springaddons.starter.webclient.SpringBootAutoConfiguration
\ No newline at end of file
diff --git a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/AddonsTest.java b/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/AddonsTest.java
deleted file mode 100644
index 810f1b414..000000000
--- a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/AddonsTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.ActiveProfiles;
-
-import reactor.netty.transport.ProxyProvider;
-
-@SpringBootTest
-@ActiveProfiles("addons")
-class AddonsTest {
-
- @Autowired
- C4ProxySettings settings;
-
- @Autowired
- C4WebClientBuilderFactoryService service;
-
- @Test
- void testSettings() {
- assertEquals(500, settings.getConnectTimeoutMillis());
- assertNull(settings.getEnabled());
- assertEquals("corp-proxy", settings.getHostname());
- assertEquals("(localhost)|(bravo\\-ch4mp)|(.*\\.corporate\\-domain\\.com)", settings.getNoProxy());
- assertEquals("abracadabra", settings.getPassword());
- assertEquals(8080, settings.getPort());
- assertEquals(ProxyProvider.Proxy.SOCKS5, settings.getType());
- assertEquals("toto", settings.getUsername());
- }
-
- @Test
- void testService() {
- final var actual = service.get();
- assertNotNull(actual);
- }
-
-}
diff --git a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/HostPortTest.java b/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/HostPortTest.java
deleted file mode 100644
index 93bb16c94..000000000
--- a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/HostPortTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.ActiveProfiles;
-
-import reactor.netty.transport.ProxyProvider;
-
-@SpringBootTest
-@ActiveProfiles("host-port")
-class HostPortTest {
-
- @Autowired
- C4ProxySettings settings;
-
- @Autowired
- C4WebClientBuilderFactoryService service;
-
- @Test
- void testSettings() {
- assertEquals(10000, settings.getConnectTimeoutMillis());
- assertNull(settings.getEnabled());
- assertEquals("mini-proxy", settings.getHostname());
- assertNull(settings.getNoProxy());
- assertNull(settings.getPassword());
- assertEquals(7080, settings.getPort());
- assertEquals(ProxyProvider.Proxy.HTTP, settings.getType());
- assertNull(settings.getUsername());
- }
-
- @Test
- void testService() {
- final var actual = service.get();
- assertNotNull(actual);
- }
-
-}
diff --git a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/NoPropertiesTest.java b/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/NoPropertiesTest.java
deleted file mode 100644
index 05e19cad9..000000000
--- a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/NoPropertiesTest.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class NoPropertiesTest {
-
- @Autowired
- C4ProxySettings settings;
-
- @Autowired
- C4WebClientBuilderFactoryService service;
-
- @Test
- void testSettings() {
- assertEquals(10000, settings.getConnectTimeoutMillis());
- assertNull(settings.getEnabled());
- assertNull(settings.getHostname());
- assertNull(settings.getNoProxy());
- assertNull(settings.getPassword());
- assertNull(settings.getPort());
- assertNull(settings.getType());
- assertNull(settings.getUsername());
- }
-
- @Test
- void testService() {
- final var actual = service.get();
- assertNotNull(actual);
- }
-
-}
diff --git a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/StdEnvVarsDisabledProxyTest.java b/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/StdEnvVarsDisabledProxyTest.java
deleted file mode 100644
index aeaf8fd4c..000000000
--- a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/StdEnvVarsDisabledProxyTest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.ActiveProfiles;
-
-@SpringBootTest
-@ActiveProfiles({ "std-env-vars", "disabled-proxy" })
-class StdEnvVarsDisabledProxyTest {
-
- @Autowired
- C4ProxySettings settings;
-
- @Autowired
- C4WebClientBuilderFactoryService service;
-
- @Test
- void testSettings() {
- assertEquals(10000, settings.getConnectTimeoutMillis());
- assertEquals(Boolean.FALSE, settings.getEnabled());
- assertNull(settings.getHostname());
- assertNull(settings.getNoProxy());
- assertNull(settings.getPassword());
- assertNull(settings.getPort());
- assertNull(settings.getType());
- assertNull(settings.getUsername());
- }
-
- @Test
- void testService() {
- final var actual = service.get();
- assertNotNull(actual);
- }
-
-}
diff --git a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/StdEnvVarsTest.java b/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/StdEnvVarsTest.java
deleted file mode 100644
index bb3d0a400..000000000
--- a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/StdEnvVarsTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.ActiveProfiles;
-
-import reactor.netty.transport.ProxyProvider;
-
-@SpringBootTest
-@ActiveProfiles("std-env-vars")
-class StdEnvVarsTest {
-
- @Autowired
- C4ProxySettings settings;
-
- @Autowired
- C4WebClientBuilderFactoryService service;
-
- @Test
- void testSettings() {
- assertEquals(10000, settings.getConnectTimeoutMillis());
- assertNull(settings.getEnabled());
- assertEquals("env-proxy", settings.getHostname());
- assertEquals("(localhost)|(bravo\\-ch4mp)|(.*\\.env\\-domain\\.pf)", settings.getNoProxy());
- assertEquals("truc", settings.getPassword());
- assertEquals(8080, settings.getPort());
- assertEquals(ProxyProvider.Proxy.HTTP, settings.getType());
- assertEquals("machin", settings.getUsername());
- }
-
- @Test
- void testService() {
- final var actual = service.get();
- assertNotNull(actual);
- }
-
-}
diff --git a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/WebClientSampleApp.java b/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/WebClientSampleApp.java
deleted file mode 100644
index 766259886..000000000
--- a/starters/spring-addons-starters-webclient/src/test/java/com/c4_soft/springaddons/starter/webclient/WebClientSampleApp.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.c4_soft.springaddons.starter.webclient;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import org.springframework.boot.WebApplicationType;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.builder.SpringApplicationBuilder;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import lombok.RequiredArgsConstructor;
-import reactor.core.publisher.Mono;
-
-@SpringBootApplication
-public class WebClientSampleApp {
- public static void main(String[] args) {
- new SpringApplicationBuilder(WebClientSampleApp.class).web(WebApplicationType.REACTIVE).run(args);
- }
-
- @RestController
- @RequestMapping("/sample")
- @RequiredArgsConstructor
- public static class SampleController {
- private final C4WebClientBuilderFactoryService webClientBuilderFactory;
-
- @GetMapping("/delegating")
- public Mono calling() throws MalformedURLException {
- return webClientBuilderFactory.get(new URL("http://localhost:8080")).build().get().uri("/sample/delegate").retrieve().bodyToMono(String.class);
- }
-
- @GetMapping("/delegate")
- public Mono remote() {
- return Mono.just("Hello!");
- }
- }
-}
diff --git a/starters/spring-addons-starters-webclient/src/test/resources/application.properties b/starters/spring-addons-starters-webclient/src/test/resources/application.properties
deleted file mode 100644
index 6ed717db3..000000000
--- a/starters/spring-addons-starters-webclient/src/test/resources/application.properties
+++ /dev/null
@@ -1,25 +0,0 @@
-server.ssl.enabled=false
-
-#---
-spring.config.activate.on-profile=host-port
-com.c4-soft.springaddons.proxy.host=mini-proxy
-com.c4-soft.springaddons.proxy.port=7080
-
-#---
-spring.config.activate.on-profile=addons
-com.c4-soft.springaddons.proxy.type=socks5
-com.c4-soft.springaddons.proxy.host=corp-proxy
-com.c4-soft.springaddons.proxy.port=8080
-com.c4-soft.springaddons.proxy.username=toto
-com.c4-soft.springaddons.proxy.password=abracadabra
-com.c4-soft.springaddons.proxy.nonProxyHostsPattern=(localhost)|(bravo\\-ch4mp)|(.*\\.corporate\\-domain\\.com)
-com.c4-soft.springaddons.proxy.connect-timeout-millis=500
-
-#---
-spring.config.activate.on-profile=disabled-proxy
-com.c4-soft.springaddons.proxy.enabled=false
-
-#---
-spring.config.activate.on-profile=std-env-vars
-http_proxy=https://machin:truc@env-proxy:8080
-no_proxy=localhost,bravo-ch4mp,.env-domain.pf
\ No newline at end of file