diff --git a/samples/webflux-jwt-oauthentication/src/test/java/com/c4_soft/springaddons/samples/webflux_oidcauthentication/MessageServiceTests.java b/samples/webflux-jwt-oauthentication/src/test/java/com/c4_soft/springaddons/samples/webflux_oidcauthentication/MessageServiceTests.java index c2423ff62..7c9d5cbee 100644 --- a/samples/webflux-jwt-oauthentication/src/test/java/com/c4_soft/springaddons/samples/webflux_oidcauthentication/MessageServiceTests.java +++ b/samples/webflux-jwt-oauthentication/src/test/java/com/c4_soft/springaddons/samples/webflux_oidcauthentication/MessageServiceTests.java @@ -42,8 +42,10 @@ import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; import com.c4_soft.springaddons.security.oidc.starter.reactive.ReactiveSpringAddonsOidcBeans; import com.c4_soft.springaddons.security.oidc.starter.reactive.client.ReactiveSpringAddonsOidcClientBeans; +import com.c4_soft.springaddons.security.oidc.starter.reactive.client.ReactiveSpringAddonsOidcClientWithLoginBeans; import com.c4_soft.springaddons.security.oidc.starter.reactive.resourceserver.ReactiveSpringAddonsOidcResourceServerBeans; import com.c4_soft.springaddons.security.oidc.starter.synchronised.client.SpringAddonsOidcClientBeans; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.client.SpringAddonsOidcClientWithLoginBeans; import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.SpringAddonsOidcResourceServerBeans; import reactor.core.publisher.Mono; @@ -59,8 +61,10 @@ exclude = { ReactiveSpringAddonsOidcResourceServerBeans.class, ReactiveSpringAddonsOidcClientBeans.class, + ReactiveSpringAddonsOidcClientWithLoginBeans.class, SpringAddonsOidcResourceServerBeans.class, - SpringAddonsOidcClientBeans.class }) + SpringAddonsOidcClientBeans.class, + SpringAddonsOidcClientWithLoginBeans.class }) @SpringBootTest(classes = { WebfluxJwtOauthentication.class, MessageService.class }) @Import({ AddonsWebfluxTestConf.class }) @ImportAutoConfiguration({ SpringAddonsOidcProperties.class, ReactiveSpringAddonsOidcBeans.class, AuthenticationFactoriesTestConf.class }) diff --git a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxClientSecurity.java b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxClientSecurity.java index 3b4d22451..7233d866d 100644 --- a/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxClientSecurity.java +++ b/spring-addons-starter-oidc-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/webflux/AutoConfigureAddonsWebfluxClientSecurity.java @@ -10,7 +10,7 @@ import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcComponentTest; import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcMinimalSecurity; import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; -import com.c4_soft.springaddons.security.oidc.starter.reactive.client.ReactiveSpringAddonsOidcClientBeans; +import com.c4_soft.springaddons.security.oidc.starter.reactive.client.ReactiveSpringAddonsOidcClientWithLoginBeans; import com.c4_soft.springaddons.security.oidc.starter.reactive.resourceserver.ReactiveSpringAddonsOidcResourceServerBeans; /** @@ -27,6 +27,6 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @AutoConfigureAddonsWebfluxMinimalSecurity -@ImportAutoConfiguration({ ReactiveSpringAddonsOidcClientBeans.class, AddonsWebfluxTestConf.class }) +@ImportAutoConfiguration({ ReactiveSpringAddonsOidcClientWithLoginBeans.class, AddonsWebfluxTestConf.class }) public @interface AutoConfigureAddonsWebfluxClientSecurity { } 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 7c6fe9691..657db481c 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 @@ -8,11 +8,11 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import com.c4_soft.springaddons.security.oauth2.test.webflux.AutoConfigureAddonsWebfluxClientSecurity; -import com.c4_soft.springaddons.security.oidc.starter.synchronised.client.SpringAddonsOidcClientBeans; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.client.SpringAddonsOidcClientWithLoginBeans; /** *

- * Auto-configures {@link SpringAddonsOidcClientBeans} as well as what is already configured by {@link AutoConfigureAddonsWebmvcMinimalSecurity}. To be used to + * Auto-configures {@link SpringAddonsOidcClientWithLoginBeans} as well as what is already configured by {@link AutoConfigureAddonsWebmvcMinimalSecurity}. To be used to * test controllers but not services or repositories (web context is not desired in that case). *

* @@ -23,6 +23,6 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @AutoConfigureAddonsWebmvcMinimalSecurity -@ImportAutoConfiguration({ SpringAddonsOidcClientBeans.class }) +@ImportAutoConfiguration({ SpringAddonsOidcClientWithLoginBeans.class }) public @interface AutoConfigureAddonsWebmvcClientSecurity { } 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 b2ca94d94..1302567f5 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 @@ -163,6 +163,11 @@ public URI getPostLogoutRedirectUri() { */ private Map> authorizationRequestParams = new HashMap<>(); + /** + * Additional parameters to send with token request, mapped by client registration IDs + */ + private Map> tokenRequestParams = new HashMap<>(); + /** * Logout properties for OpenID Providers which do not implement the RP-Initiated Logout spec * diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultGrantedAuthoritiesMapperCondition.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultGrantedAuthoritiesMapperCondition.java index 38c125b31..3a2d79359 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultGrantedAuthoritiesMapperCondition.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/bean/DefaultGrantedAuthoritiesMapperCondition.java @@ -5,14 +5,14 @@ import org.springframework.context.annotation.Conditional; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsOidcClientCondition; +import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientWithLoginCondition; public class DefaultGrantedAuthoritiesMapperCondition extends AllNestedConditions { DefaultGrantedAuthoritiesMapperCondition() { super(ConfigurationPhase.REGISTER_BEAN); } - @Conditional(IsOidcClientCondition.class) + @Conditional(IsClientWithLoginCondition.class) static class SpringAddonsOidcClientEnabled { } diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/configuration/IsOidcClientCondition.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/configuration/IsClientWithLoginCondition.java similarity index 88% rename from spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/configuration/IsOidcClientCondition.java rename to spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/configuration/IsClientWithLoginCondition.java index 729532918..f25445efb 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/configuration/IsOidcClientCondition.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/condition/configuration/IsClientWithLoginCondition.java @@ -4,9 +4,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -public class IsOidcClientCondition extends AnyNestedCondition { +public class IsClientWithLoginCondition extends AnyNestedCondition { - public IsOidcClientCondition() { + public IsClientWithLoginCondition() { super(ConfigurationPhase.PARSE_CONFIGURATION); } diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/PerRegistrationReactiveOAuth2AuthorizedClientProvider.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/PerRegistrationReactiveOAuth2AuthorizedClientProvider.java new file mode 100644 index 000000000..fa7e6098b --- /dev/null +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/PerRegistrationReactiveOAuth2AuthorizedClientProvider.java @@ -0,0 +1,125 @@ +package com.c4_soft.springaddons.security.oidc.starter.reactive.client; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.DelegatingReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.endpoint.WebClientReactiveClientCredentialsTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.WebClientReactiveRefreshTokenTokenResponseClient; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; + +import reactor.core.publisher.Mono; + +/** + *

+ * An alternative {@link ReactiveOAuth2AuthorizedClientProvider} to {@link DelegatingReactiveOAuth2AuthorizedClientProvider} keeping a different provider for + * each client registration. This allows to define for each a set of extra parameters to add to token requests. + *

+ * + * @author Jerome Wacongne ch4mp@c4-soft.com + */ +public final class PerRegistrationReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider { + + private final Map providers; + + public PerRegistrationReactiveOAuth2AuthorizedClientProvider( + SpringAddonsOidcProperties addonsProperties, + Map providers) { + this.providers = new HashMap<>(providers); + } + + public PerRegistrationReactiveOAuth2AuthorizedClientProvider( + InMemoryReactiveClientRegistrationRepository clientRegistrationRepo, + SpringAddonsOidcProperties addonsProperties, + Map customProviders) { + this.providers = new HashMap<>(customProviders); + StreamSupport.stream(clientRegistrationRepo.spliterator(), false).forEach(reg -> { + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(reg.getAuthorizationGrantType())) { + this.providers.putIfAbsent(reg.getRegistrationId(), new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider()); + } else if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(reg.getAuthorizationGrantType())) { + this.providers.putIfAbsent(reg.getRegistrationId(), new ClientCredentialsReactiveOAuth2AuthorizedClientProvider()); + } else if (AuthorizationGrantType.REFRESH_TOKEN.equals(reg.getAuthorizationGrantType())) { + this.providers.putIfAbsent(reg.getRegistrationId(), new RefreshTokenReactiveOAuth2AuthorizedClientProvider()); + } else { + throw new UnsupportedGrantTypeException(reg.getAuthorizationGrantType()); + } + + final var tokenParams = addonsProperties.getClient().getTokenRequestParams().getOrDefault(reg.getRegistrationId(), List.of()); + if (tokenParams.isEmpty()) { + return; + } + final MultiValueMap extraParameters = new LinkedMultiValueMap<>(tokenParams.size()); + for (final var param : tokenParams) { + extraParameters.add(param.getName(), param.getValue()); + } + + final var delegate = this.providers.get(reg.getRegistrationId()); + if (delegate instanceof ClientCredentialsReactiveOAuth2AuthorizedClientProvider clientCredentialsProvider) { + final var clientCredentialsResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient(); + clientCredentialsResponseClient.addParametersConverter(source -> extraParameters); + + clientCredentialsProvider.setAccessTokenResponseClient(clientCredentialsResponseClient); + + } else if (delegate instanceof RefreshTokenReactiveOAuth2AuthorizedClientProvider refreshTokenProvider) { + final var refreshTokenResponseClient = new WebClientReactiveRefreshTokenTokenResponseClient(); + refreshTokenResponseClient.addParametersConverter(source -> extraParameters); + + refreshTokenProvider.setAccessTokenResponseClient(refreshTokenResponseClient); + } + }); + } + + public PerRegistrationReactiveOAuth2AuthorizedClientProvider( + InMemoryReactiveClientRegistrationRepository clientRegistrationRepo, + SpringAddonsOidcProperties addonsProperties) { + this(clientRegistrationRepo, addonsProperties, Map.of()); + } + + @Override + public Mono authorize(OAuth2AuthorizationContext context) throws UnsupportedGrantTypeException { + if (context == null) { + return null; + } + + final var provider = getDelegate(context.getClientRegistration().getRegistrationId()); + + return provider.authorize(context); + } + + @SuppressWarnings("unchecked") + public T getDelegate(String registrationId) throws UnsupportedGrantTypeException { + final var provider = providers.get(registrationId); + return (T) provider; + } + + public PerRegistrationReactiveOAuth2AuthorizedClientProvider setDelegate(String registrationId, ReactiveOAuth2AuthorizedClientProvider delegate) { + Assert.notNull(registrationId, "registrationId cannot be null"); + Assert.notNull(delegate, "delegate cannot be null"); + providers.put(registrationId, delegate); + return this; + } + + static class UnsupportedGrantTypeException extends RuntimeException { + private static final long serialVersionUID = 5600617070203595919L; + + public UnsupportedGrantTypeException(AuthorizationGrantType grantType) { + super( + "No OAuth2AuthorizedClientProvider registered for GrantType: %s. Consider adding one to SpringAddonsDelegatingOAuth2AuthorizedClientProvider in your conf." + .formatted(grantType)); + } + } +} diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsAop.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsAop.java index 01fd22f24..ceeb1ed51 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsAop.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsAop.java @@ -21,12 +21,12 @@ import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientMultiTenancyEnabled; import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsNotServlet; -import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsOidcClientCondition; +import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientWithLoginCondition; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; -@Conditional({ IsOidcClientCondition.class, IsNotServlet.class, IsClientMultiTenancyEnabled.class }) +@Conditional({ IsClientWithLoginCondition.class, IsNotServlet.class, IsClientMultiTenancyEnabled.class }) @AutoConfiguration @PropertySource(value = "classpath:/c4-spring-addons.properties", ignoreResourceNotFound = true) public class ReactiveSpringAddonsAop { diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java index f5737108a..10c84c490 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java @@ -1,297 +1,41 @@ package com.c4_soft.springaddons.security.oidc.starter.reactive.client; -import java.util.Optional; - import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -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.ServerHttpSecurity; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; 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.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.DelegatingServerLogoutHandler; -import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler; -import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; -import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; -import org.springframework.security.web.server.authentication.logout.WebSessionServerLogoutHandler; -import org.springframework.security.web.server.csrf.CsrfToken; -import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; -import org.springframework.web.server.WebFilter; -import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; -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.DefaultAuthenticationSuccessHandlerCondition; -import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsNotServlet; -import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsOidcClientCondition; -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 ClientHttpSecurityPostProcessor} 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({ IsOidcClientCondition.class, IsNotServlet.class }) -@EnableWebFluxSecurity +@ConditionalOnClass(ReactiveOAuth2AuthorizedClientManager.class) @AutoConfiguration -@ImportAutoConfiguration(ReactiveSpringAddonsOidcBeans.class) -@Slf4j public class ReactiveSpringAddonsOidcClientBeans { - /** - *

- * 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 ClientHttpSecurityPostProcessor} 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, - ClientHttpSecurityPostProcessor httpPostProcessor, - ServerLogoutHandler logoutHandler, - Customizer oidcLogoutCustomizer) - 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)); - - // @formatter:off - addonsProperties.getClient().getLoginPath().ifPresent(loginPath -> { - http.exceptionHandling(exceptionHandling -> exceptionHandling - .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint(UriComponentsBuilder.fromUri(addonsProperties.getClient().getClientUri()).path(loginPath).build().toString()))); - }); - - http.oauth2Login(oauth2 -> { - oauth2.authorizationRequestResolver(authorizationRequestResolver); - oauth2.authorizationRedirectStrategy(preAuthorizationCodeRedirectStrategy); - authenticationSuccessHandler.ifPresent(oauth2::authenticationSuccessHandler); - authenticationFailureHandler.ifPresent(oauth2::authenticationFailureHandler); - }); - - http.logout((logout) -> { - logout.logoutHandler(logoutHandler); - logout.logoutSuccessHandler(logoutSuccessHandler); - }); - - if(addonsProperties.getClient().getBackChannelLogout().isEnabled()) { - http.oidcLogout(oidcLogoutCustomizer); - } - - ReactiveConfigurationSupport.configureClient(http, serverProperties, addonsProperties.getClient(), authorizePostProcessor, httpPostProcessor); - - return http.build(); - } - - /** - * 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 - * @return {@link SpringAddonsServerLogoutSuccessHandler} - */ - @ConditionalOnMissingBean - @Bean - ServerLogoutSuccessHandler logoutSuccessHandler(LogoutRequestUriBuilder logoutUriBuilder, - ReactiveClientRegistrationRepository clientRegistrationRepo, SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsServerLogoutSuccessHandler(logoutUriBuilder, clientRegistrationRepo, addonsProperties); - } - - /** - * 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 - ClientAuthorizeExchangeSpecPostProcessor clientAuthorizePostProcessor() { - return (ServerHttpSecurity.AuthorizeExchangeSpec spec) -> spec.anyExchange().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 - ClientHttpSecurityPostProcessor clientHttpPostProcessor() { - return serverHttpSecurity -> serverHttpSecurity; - } - - /** - * 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) -> { - exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe(); - return chain.filter(exchange); - }; - } + ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository, + ReactiveOAuth2AuthorizedClientProvider oauth2AuthorizedClientProvider) { - @ConditionalOnMissingBean - @Bean - ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository, SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository, addonsProperties.getClient()); - } + final var authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(oauth2AuthorizedClientProvider); - @ConditionalOnMissingBean - @Bean - ServerLogoutHandler logoutHandler() { - return new DelegatingServerLogoutHandler( - new WebSessionServerLogoutHandler(), - new SecurityContextServerLogoutHandler()); + return authorizedClientManager; } @ConditionalOnMissingBean @Bean - PreAuthorizationCodeServerRedirectStrategy preAuthorizationCodeRedirectStrategy(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsPreAuthorizationCodeServerRedirectStrategy( - addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode()); - } - - @Conditional(DefaultAuthenticationSuccessHandlerCondition.class) - @Bean - ServerAuthenticationSuccessHandler authenticationSuccessHandler(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOauth2ServerAuthenticationSuccessHandler(addonsProperties); - } - - @Conditional(DefaultAuthenticationSuccessHandlerCondition.class) - @Bean - ServerAuthenticationFailureHandler authenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOauth2ServerAuthenticationFailureHandler(addonsProperties); - } - - static interface PreAuthorizationCodeServerRedirectStrategy extends ServerRedirectStrategy {} - - public static class SpringAddonsPreAuthorizationCodeServerRedirectStrategy extends SpringAddonsOauth2ServerRedirectStrategy implements PreAuthorizationCodeServerRedirectStrategy { - public SpringAddonsPreAuthorizationCodeServerRedirectStrategy(HttpStatus defaultStatus) { - super(defaultStatus); - } - + ReactiveOAuth2AuthorizedClientProvider oauth2AuthorizedClientProvider( + SpringAddonsOidcProperties addonsProperties, + InMemoryReactiveClientRegistrationRepository clientRegistrationRepository) { + return new PerRegistrationReactiveOAuth2AuthorizedClientProvider(clientRegistrationRepository, addonsProperties); } - @ConditionalOnMissingBean - @Bean - Customizer oidcLogoutSpec() { - return Customizer.withDefaults(); - } -} \ 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/ReactiveSpringAddonsOidcClientWithLoginBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientWithLoginBeans.java new file mode 100644 index 000000000..d7eb520ed --- /dev/null +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientWithLoginBeans.java @@ -0,0 +1,297 @@ +package com.c4_soft.springaddons.security.oidc.starter.reactive.client; + +import java.util.Optional; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +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.ServerHttpSecurity; +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.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.DelegatingServerLogoutHandler; +import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler; +import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import org.springframework.security.web.server.authentication.logout.WebSessionServerLogoutHandler; +import org.springframework.security.web.server.csrf.CsrfToken; +import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +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.DefaultAuthenticationSuccessHandlerCondition; +import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsNotServlet; +import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientWithLoginCondition; +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 ClientHttpSecurityPostProcessor} 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 }) +@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 ClientHttpSecurityPostProcessor} 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, + ClientHttpSecurityPostProcessor httpPostProcessor, + ServerLogoutHandler logoutHandler, + Customizer oidcLogoutCustomizer) + 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)); + + // @formatter:off + addonsProperties.getClient().getLoginPath().ifPresent(loginPath -> { + http.exceptionHandling(exceptionHandling -> exceptionHandling + .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint(UriComponentsBuilder.fromUri(addonsProperties.getClient().getClientUri()).path(loginPath).build().toString()))); + }); + + http.oauth2Login(oauth2 -> { + oauth2.authorizationRequestResolver(authorizationRequestResolver); + oauth2.authorizationRedirectStrategy(preAuthorizationCodeRedirectStrategy); + authenticationSuccessHandler.ifPresent(oauth2::authenticationSuccessHandler); + authenticationFailureHandler.ifPresent(oauth2::authenticationFailureHandler); + }); + + http.logout((logout) -> { + logout.logoutHandler(logoutHandler); + logout.logoutSuccessHandler(logoutSuccessHandler); + }); + + if(addonsProperties.getClient().getBackChannelLogout().isEnabled()) { + http.oidcLogout(oidcLogoutCustomizer); + } + + ReactiveConfigurationSupport.configureClient(http, serverProperties, addonsProperties.getClient(), authorizePostProcessor, httpPostProcessor); + + return http.build(); + } + + /** + * 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 + * @return {@link SpringAddonsServerLogoutSuccessHandler} + */ + @ConditionalOnMissingBean + @Bean + ServerLogoutSuccessHandler logoutSuccessHandler(LogoutRequestUriBuilder logoutUriBuilder, + ReactiveClientRegistrationRepository clientRegistrationRepo, SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsServerLogoutSuccessHandler(logoutUriBuilder, clientRegistrationRepo, addonsProperties); + } + + /** + * 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 + ClientAuthorizeExchangeSpecPostProcessor clientAuthorizePostProcessor() { + return (ServerHttpSecurity.AuthorizeExchangeSpec spec) -> spec.anyExchange().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 + ClientHttpSecurityPostProcessor clientHttpPostProcessor() { + return serverHttpSecurity -> serverHttpSecurity; + } + + /** + * 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) -> { + exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe(); + return chain.filter(exchange); + }; + } + + @ConditionalOnMissingBean + @Bean + ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository, SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository, addonsProperties.getClient()); + } + + @ConditionalOnMissingBean + @Bean + ServerLogoutHandler logoutHandler() { + return new DelegatingServerLogoutHandler( + new WebSessionServerLogoutHandler(), + new SecurityContextServerLogoutHandler()); + } + + @ConditionalOnMissingBean + @Bean + PreAuthorizationCodeServerRedirectStrategy preAuthorizationCodeRedirectStrategy(SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsPreAuthorizationCodeServerRedirectStrategy( + addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode()); + } + + @Conditional(DefaultAuthenticationSuccessHandlerCondition.class) + @Bean + ServerAuthenticationSuccessHandler authenticationSuccessHandler(SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOauth2ServerAuthenticationSuccessHandler(addonsProperties); + } + + @Conditional(DefaultAuthenticationSuccessHandlerCondition.class) + @Bean + ServerAuthenticationFailureHandler authenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOauth2ServerAuthenticationFailureHandler(addonsProperties); + } + + static interface PreAuthorizationCodeServerRedirectStrategy extends ServerRedirectStrategy {} + + public static class SpringAddonsPreAuthorizationCodeServerRedirectStrategy extends SpringAddonsOauth2ServerRedirectStrategy implements PreAuthorizationCodeServerRedirectStrategy { + public SpringAddonsPreAuthorizationCodeServerRedirectStrategy(HttpStatus defaultStatus) { + super(defaultStatus); + } + + } + + @ConditionalOnMissingBean + @Bean + Customizer oidcLogoutSpec() { + return Customizer.withDefaults(); + } +} \ 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/SpringAddonsServerLogoutSuccessHandler.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerLogoutSuccessHandler.java index ec5cb3877..fce843968 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerLogoutSuccessHandler.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerLogoutSuccessHandler.java @@ -30,7 +30,7 @@ * one which emitted the access-token with which the logout request is made). *

*

- * This bean is auto-configured by {@link ReactiveSpringAddonsOidcClientBeans} as {@link ConditionalOnMissingBean @ConditionalOnMissingBean} of type + * This bean is auto-configured by {@link ReactiveSpringAddonsOidcClientWithLoginBeans} as {@link ConditionalOnMissingBean @ConditionalOnMissingBean} of type * {@link ServerLogoutSuccessHandler}. Usage: *

* diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/PerRegistrationOAuth2AuthorizedClientProvider.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/PerRegistrationOAuth2AuthorizedClientProvider.java new file mode 100644 index 000000000..8511cc1e5 --- /dev/null +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/PerRegistrationOAuth2AuthorizedClientProvider.java @@ -0,0 +1,129 @@ +package com.c4_soft.springaddons.security.oidc.starter.synchronised.client; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; + +/** + *

+ * An alternative {@link OAuth2AuthorizedClientProvider} to {@link DelegatingOAuth2AuthorizedClientProvider} keeping a different provider for each client + * registration. This allows to define for each a set of extra parameters to add to token requests. + *

+ * + * @author Jerome Wacongne ch4mp@c4-soft.com + */ +public final class PerRegistrationOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider { + + private final Map providers; + + public PerRegistrationOAuth2AuthorizedClientProvider(SpringAddonsOidcProperties addonsProperties, Map providers) { + this.providers = new HashMap<>(providers); + } + + public PerRegistrationOAuth2AuthorizedClientProvider( + InMemoryClientRegistrationRepository clientRegistrationRepo, + SpringAddonsOidcProperties addonsProperties, + Map customProviders) { + this.providers = new HashMap<>(customProviders); + StreamSupport.stream(clientRegistrationRepo.spliterator(), false).forEach(reg -> { + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(reg.getAuthorizationGrantType())) { + this.providers.putIfAbsent(reg.getRegistrationId(), new AuthorizationCodeOAuth2AuthorizedClientProvider()); + } else if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(reg.getAuthorizationGrantType())) { + this.providers.putIfAbsent(reg.getRegistrationId(), new ClientCredentialsOAuth2AuthorizedClientProvider()); + } else if (AuthorizationGrantType.REFRESH_TOKEN.equals(reg.getAuthorizationGrantType())) { + this.providers.putIfAbsent(reg.getRegistrationId(), new RefreshTokenOAuth2AuthorizedClientProvider()); + } else { + throw new UnsupportedGrantTypeException(reg.getAuthorizationGrantType()); + } + + final var tokenParams = addonsProperties.getClient().getTokenRequestParams().getOrDefault(reg.getRegistrationId(), List.of()); + if (tokenParams.isEmpty()) { + return; + } + final MultiValueMap extraParameters = new LinkedMultiValueMap<>(tokenParams.size()); + for (final var param : tokenParams) { + extraParameters.add(param.getName(), param.getValue()); + } + + final var delegate = this.providers.get(reg.getRegistrationId()); + if (delegate instanceof ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider) { + final var requestEntityConverter = new OAuth2ClientCredentialsGrantRequestEntityConverter(); + requestEntityConverter.addParametersConverter(source -> extraParameters); + + final var clientCredentialsResponseClient = new DefaultClientCredentialsTokenResponseClient(); + clientCredentialsResponseClient.setRequestEntityConverter(requestEntityConverter); + + clientCredentialsProvider.setAccessTokenResponseClient(clientCredentialsResponseClient); + + } else if (delegate instanceof RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider) { + final var requestEntityConverter = new OAuth2RefreshTokenGrantRequestEntityConverter(); + requestEntityConverter.addParametersConverter(source -> extraParameters); + + final var refreshTokenResponseClient = new DefaultRefreshTokenTokenResponseClient(); + refreshTokenResponseClient.setRequestEntityConverter(requestEntityConverter); + + refreshTokenProvider.setAccessTokenResponseClient(refreshTokenResponseClient); + } + }); + } + + public PerRegistrationOAuth2AuthorizedClientProvider( + InMemoryClientRegistrationRepository clientRegistrationRepo, + SpringAddonsOidcProperties addonsProperties) { + this(clientRegistrationRepo, addonsProperties, Map.of()); + } + + @Override + public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) throws UnsupportedGrantTypeException { + if (context == null) { + return null; + } + + final var provider = getDelegate(context.getClientRegistration().getRegistrationId()); + + return provider.authorize(context); + } + + @SuppressWarnings("unchecked") + public T getDelegate(String registrationId) throws UnsupportedGrantTypeException { + final var provider = providers.get(registrationId); + return (T) provider; + } + + public PerRegistrationOAuth2AuthorizedClientProvider setDelegate(String registrationId, OAuth2AuthorizedClientProvider delegate) { + Assert.notNull(registrationId, "registrationId cannot be null"); + Assert.notNull(delegate, "delegate cannot be null"); + providers.put(registrationId, delegate); + return this; + } + + static class UnsupportedGrantTypeException extends RuntimeException { + private static final long serialVersionUID = 5600617070203595919L; + + public UnsupportedGrantTypeException(AuthorizationGrantType grantType) { + super( + "No OAuth2AuthorizedClientProvider registered for GrantType: %s. Consider adding one to SpringAddonsDelegatingOAuth2AuthorizedClientProvider in your conf." + .formatted(grantType)); + } + } +} diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsAop.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsAop.java index 776989a96..a4dcc74b4 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsAop.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsAop.java @@ -20,12 +20,12 @@ import org.springframework.stereotype.Component; import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientMultiTenancyEnabled; -import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsOidcClientCondition; +import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientWithLoginCondition; import lombok.RequiredArgsConstructor; @ConditionalOnWebApplication(type = Type.SERVLET) -@Conditional({ IsOidcClientCondition.class, IsClientMultiTenancyEnabled.class }) +@Conditional({ IsClientWithLoginCondition.class, IsClientMultiTenancyEnabled.class }) @AutoConfiguration @PropertySource(value = "classpath:/c4-spring-addons.properties", ignoreResourceNotFound = true) public class SpringAddonsAop { diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsLogoutSuccessHandler.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsLogoutSuccessHandler.java index 98881c310..7b21d32d7 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsLogoutSuccessHandler.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsLogoutSuccessHandler.java @@ -32,7 +32,7 @@ * one which emitted the access-token with which the logout request is made). *

*

- * This bean is auto-configured by {@link SpringAddonsOidcClientBeans} as {@link ConditionalOnMissingBean @ConditionalOnMissingBean} of type + * This bean is auto-configured by {@link SpringAddonsOidcClientWithLoginBeans} as {@link ConditionalOnMissingBean @ConditionalOnMissingBean} of type * {@link LogoutSuccessHandler}. Usage: *

* diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java index 3c89fa770..52122edcc 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java @@ -1,251 +1,40 @@ package com.c4_soft.springaddons.security.oidc.starter.synchronised.client; -import java.util.Optional; - import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -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.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; -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.LogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; -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.DefaultAuthenticationFailureHandlerCondition; -import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationSuccessHandlerCondition; -import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsOidcClientCondition; -import com.c4_soft.springaddons.security.oidc.starter.synchronised.ServletConfigurationSupport; -import com.c4_soft.springaddons.security.oidc.starter.synchronised.SpringAddonsOidcBeans; - -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 ClientHttpSecurityPostProcessor} 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 - */ -@ConditionalOnWebApplication(type = Type.SERVLET) -@Conditional(IsOidcClientCondition.class) -@EnableWebSecurity +@ConditionalOnClass(OAuth2AuthorizedClientManager.class) @AutoConfiguration -@ImportAutoConfiguration(SpringAddonsOidcBeans.class) -@Slf4j public class SpringAddonsOidcClientBeans { - /** - *

- * 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 ClientHttpSecurityPostProcessor} 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} - * @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 - SecurityFilterChain springAddonsClientFilterChain( - HttpSecurity http, - ServerProperties serverProperties, - PreAuthorizationCodeRedirectStrategy preAuthorizationCodeRedirectStrategy, - OAuth2AuthorizationRequestResolver authorizationRequestResolver, - Optional authenticationSuccessHandler, - Optional authenticationFailureHandler, - LogoutSuccessHandler logoutSuccessHandler, - SpringAddonsOidcProperties addonsProperties, - ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor, - ClientHttpSecurityPostProcessor httpPostProcessor, - Customizer> oidcLogoutCustomizer) - throws Exception { - // @formatter:off - log.info("Applying client OAuth2 configuration for: {}", addonsProperties.getClient().getSecurityMatchers()); - http.securityMatcher(addonsProperties.getClient().getSecurityMatchers().toArray(new String[] {})); - - 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); - }); - - http.logout(logout -> { - logout.logoutSuccessHandler(logoutSuccessHandler); - }); - // @formatter:on - - if (addonsProperties.getClient().getBackChannelLogout().isEnabled()) { - http.oidcLogout(oidcLogoutCustomizer); - } - - ServletConfigurationSupport.configureClient(http, serverProperties, addonsProperties.getClient(), authorizePostProcessor, httpPostProcessor); - - 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 clientRegistrationRepository - * @param addonsProperties - * @return {@link SpringAddonsOAuth2AuthorizationRequestResolver} - */ @ConditionalOnMissingBean @Bean - OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver( + OAuth2AuthorizedClientManager authorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, - SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOAuth2AuthorizationRequestResolver(clientRegistrationRepository, addonsProperties.getClient()); - } + OAuth2AuthorizedClientRepository authorizedClientRepository, + OAuth2AuthorizedClientProvider oauth2AuthorizedClientProvider) { - /** - * 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()); - } + final var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(oauth2AuthorizedClientProvider); - /** - * 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 authorizedClientManager; } - /** - * @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 - ClientHttpSecurityPostProcessor clientHttpPostProcessor() { - return http -> http; - } - - @ConditionalOnMissingBean - @Bean - PreAuthorizationCodeRedirectStrategy authorizationCodeRedirectStrategy(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsPreAuthorizationCodeRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode()); - } - - 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); - } - - @Conditional(DefaultAuthenticationFailureHandlerCondition.class) - @Bean - AuthenticationFailureHandler authenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOauth2AuthenticationFailureHandler(addonsProperties); - } - - @ConditionalOnMissingBean - @Bean - Customizer> oidcLogoutCustomizer() { - return Customizer.withDefaults(); + OAuth2AuthorizedClientProvider oauth2AuthorizedClientProvider( + SpringAddonsOidcProperties addonsProperties, + InMemoryClientRegistrationRepository clientRegistrationRepository) { + return new PerRegistrationOAuth2AuthorizedClientProvider(clientRegistrationRepository, addonsProperties); } } 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 new file mode 100644 index 000000000..c2907a5a7 --- /dev/null +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java @@ -0,0 +1,251 @@ +package com.c4_soft.springaddons.security.oidc.starter.synchronised.client; + +import java.util.Optional; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +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.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +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.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +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.LogoutSuccessHandler; + +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.DefaultAuthenticationFailureHandlerCondition; +import com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean.DefaultAuthenticationSuccessHandlerCondition; +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 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 ClientHttpSecurityPostProcessor} 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 + */ +@ConditionalOnWebApplication(type = Type.SERVLET) +@Conditional(IsClientWithLoginCondition.class) +@EnableWebSecurity +@AutoConfiguration +@ImportAutoConfiguration(SpringAddonsOidcBeans.class) +@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 ClientHttpSecurityPostProcessor} 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} + * @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 + SecurityFilterChain springAddonsClientFilterChain( + HttpSecurity http, + ServerProperties serverProperties, + PreAuthorizationCodeRedirectStrategy preAuthorizationCodeRedirectStrategy, + OAuth2AuthorizationRequestResolver authorizationRequestResolver, + Optional authenticationSuccessHandler, + Optional authenticationFailureHandler, + LogoutSuccessHandler logoutSuccessHandler, + SpringAddonsOidcProperties addonsProperties, + ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor, + ClientHttpSecurityPostProcessor httpPostProcessor, + Customizer> oidcLogoutCustomizer) + throws Exception { + // @formatter:off + log.info("Applying client OAuth2 configuration for: {}", addonsProperties.getClient().getSecurityMatchers()); + http.securityMatcher(addonsProperties.getClient().getSecurityMatchers().toArray(new String[] {})); + + 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); + }); + + http.logout(logout -> { + logout.logoutSuccessHandler(logoutSuccessHandler); + }); + // @formatter:on + + if (addonsProperties.getClient().getBackChannelLogout().isEnabled()) { + http.oidcLogout(oidcLogoutCustomizer); + } + + ServletConfigurationSupport.configureClient(http, serverProperties, addonsProperties.getClient(), authorizePostProcessor, httpPostProcessor); + + 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 clientRegistrationRepository + * @param addonsProperties + * @return {@link SpringAddonsOAuth2AuthorizationRequestResolver} + */ + @ConditionalOnMissingBean + @Bean + OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver( + ClientRegistrationRepository clientRegistrationRepository, + SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOAuth2AuthorizationRequestResolver(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()); + } + + /** + * 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 no-op post processor + */ + @ConditionalOnMissingBean + @Bean + ClientHttpSecurityPostProcessor clientHttpPostProcessor() { + return http -> http; + } + + @ConditionalOnMissingBean + @Bean + PreAuthorizationCodeRedirectStrategy authorizationCodeRedirectStrategy(SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsPreAuthorizationCodeRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode()); + } + + 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); + } + + @Conditional(DefaultAuthenticationFailureHandlerCondition.class) + @Bean + AuthenticationFailureHandler authenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOauth2AuthenticationFailureHandler(addonsProperties); + } + + @ConditionalOnMissingBean + @Bean + Customizer> oidcLogoutCustomizer() { + return Customizer.withDefaults(); + } +} diff --git a/spring-addons-starter-oidc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-addons-starter-oidc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index e6c060e23..c8f428a70 100644 --- a/spring-addons-starter-oidc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-addons-starter-oidc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,7 +1,9 @@ com.c4_soft.springaddons.security.oidc.starter.reactive.client.ReactiveSpringAddonsOidcClientBeans +com.c4_soft.springaddons.security.oidc.starter.reactive.client.ReactiveSpringAddonsOidcClientWithLoginBeans com.c4_soft.springaddons.security.oidc.starter.reactive.client.ReactiveSpringAddonsAop com.c4_soft.springaddons.security.oidc.starter.reactive.resourceserver.ReactiveSpringAddonsOidcResourceServerBeans com.c4_soft.springaddons.security.oidc.starter.synchronised.client.SpringAddonsOidcClientBeans +com.c4_soft.springaddons.security.oidc.starter.synchronised.client.SpringAddonsOidcClientWithLoginBeans com.c4_soft.springaddons.security.oidc.starter.synchronised.client.SpringAddonsAop com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.SpringAddonsOidcResourceServerBeans