diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java index 1dcfc9608..e6a86ec59 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientWithLoginBeans.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.Optional; - import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -27,12 +26,12 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.session.InvalidSessionStrategy; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; import org.springframework.web.filter.CorsFilter; import org.springframework.web.util.UriComponentsBuilder; - import com.c4_soft.springaddons.security.oidc.starter.ClaimSetAuthoritiesConverter; import com.c4_soft.springaddons.security.oidc.starter.ConfigurableClaimSetAuthoritiesConverter; import com.c4_soft.springaddons.security.oidc.starter.LogoutRequestUriBuilder; @@ -48,7 +47,6 @@ import com.c4_soft.springaddons.security.oidc.starter.properties.condition.configuration.IsClientWithLoginCondition; import com.c4_soft.springaddons.security.oidc.starter.synchronised.ServletConfigurationSupport; import com.c4_soft.springaddons.security.oidc.starter.synchronised.SpringAddonsOidcBeans; - import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -56,23 +54,31 @@ /** * The following {@link ConditionalOnMissingBean @ConditionalOnMissingBeans} are auto-configured * * * @author Jerome Wacongne ch4mp@c4-soft.com @@ -85,64 +91,74 @@ @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: - * - * - * @param http the security filter-chain builder to configure - * @param serverProperties Spring Boot standard server properties - * @param authorizationRequestResolver the authorization request resolver to use. By default {@link SpringAddonsOAuth2AuthorizationRequestResolver} (adds - * authorization request parameters defined in properties and builds absolutes callback URI) - * @param preAuthorizationCodeRedirectStrategy the redirection strategy to use for authorization-code request - * @param authenticationEntryPoint the {@link AuthenticationEntryPoint} to use. Default is {@link SpringAddonsAuthenticationEntryPoint} - * @param authenticationSuccessHandler the authentication success handler to use. Default is a {@link SpringAddonsOauth2AuthenticationSuccessHandler} - * @param authenticationFailureHandler the authentication failure handler to use. Default is a {@link SpringAddonsOauth2AuthenticationFailureHandler} - * @param invalidSessionStrategy default redirects to login, unless another status is set in - * com.c4-soft.springaddons.oidc.client.oauth2-redirections.invalid-session-strategy - * @param logoutSuccessHandler Defaulted to {@link SpringAddonsLogoutSuccessHandler} which can handle "almost" RP Initiated Logout conformant OPs (like - * Auth0 and Cognito). Default is a {@link SpringAddonsLogoutSuccessHandler} - * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties spring-addons client properties} - * @param authorizePostProcessor post process authorization after "permit-all" configuration was applied (default is "isAuthenticated()" to everything that - * was not matched) - * @param httpPostProcessor post process the "http" builder just before it is returned (enables to override anything from the auto-configuration) - * spring-addons client properties} - * @param oidcBackChannelLogoutHandler if present, Back-Channel Logout is enabled. A default {@link OidcBackChannelLogoutHandler} is provided if - * com.c4-soft.springaddons.oidc.client.back-channel-logout.enabled is true - * @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2 clients - * @throws Exception in case of miss-configuration - */ - @Order(Ordered.LOWEST_PRECEDENCE - 1) - @Bean - SecurityFilterChain springAddonsClientFilterChain( - HttpSecurity http, - ServerProperties serverProperties, - PreAuthorizationCodeRedirectStrategy preAuthorizationCodeRedirectStrategy, - OAuth2AuthorizationRequestResolver authorizationRequestResolver, - AuthenticationEntryPoint authenticationEntryPoint, - AuthenticationSuccessHandler authenticationSuccessHandler, - AuthenticationFailureHandler authenticationFailureHandler, - InvalidSessionStrategy invalidSessionStrategy, - LogoutSuccessHandler logoutSuccessHandler, - SpringAddonsOidcProperties addonsProperties, - ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor, - ClientSynchronizedHttpSecurityPostProcessor httpPostProcessor, - Optional oidcBackChannelLogoutHandler) - throws Exception { - // @formatter:off + /** + *

+ * Instantiated only if "com.c4-soft.springaddons.oidc.client.security-matchers" property has at + * least one entry. If defined, it is with higher precedence than resource server one. + *

+ * It defines: + * + * + * @param http the security filter-chain builder to configure + * @param serverProperties Spring Boot standard server properties + * @param authorizationRequestResolver the authorization request resolver to use. By default + * {@link SpringAddonsOAuth2AuthorizationRequestResolver} (adds authorization request + * parameters defined in properties and builds absolutes callback URI) + * @param preAuthorizationCodeRedirectStrategy the redirection strategy to use for + * authorization-code request + * @param authenticationEntryPoint the {@link AuthenticationEntryPoint} to use. Default is + * {@link SpringAddonsAuthenticationEntryPoint} + * @param authenticationSuccessHandler the authentication success handler to use. Default is a + * {@link SpringAddonsOauth2AuthenticationSuccessHandler} + * @param authenticationFailureHandler the authentication failure handler to use. Default is a + * {@link SpringAddonsOauth2AuthenticationFailureHandler} + * @param invalidSessionStrategy default redirects to login, unless another status is set in + * com.c4-soft.springaddons.oidc.client.oauth2-redirections.invalid-session-strategy + * @param logoutSuccessHandler Defaulted to {@link SpringAddonsLogoutSuccessHandler} which can + * handle "almost" RP Initiated Logout conformant OPs (like Auth0 and Cognito). Default is + * a {@link SpringAddonsLogoutSuccessHandler} + * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties spring-addons client + * properties} + * @param authorizePostProcessor post process authorization after "permit-all" configuration was + * applied (default is "isAuthenticated()" to everything that was not matched) + * @param httpPostProcessor post process the "http" builder just before it is returned (enables to + * override anything from the auto-configuration) spring-addons client properties} + * @param oidcBackChannelLogoutHandler if present, Back-Channel Logout is enabled. A default + * {@link OidcBackChannelLogoutHandler} is provided if + * com.c4-soft.springaddons.oidc.client.back-channel-logout.enabled is true + * @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2 + * clients + * @throws Exception in case of miss-configuration + */ + @Order(Ordered.LOWEST_PRECEDENCE - 1) + @Bean + SecurityFilterChain springAddonsClientFilterChain(HttpSecurity http, + ServerProperties serverProperties, + PreAuthorizationCodeRedirectStrategy preAuthorizationCodeRedirectStrategy, + OAuth2AuthorizationRequestResolver authorizationRequestResolver, + AuthenticationEntryPoint authenticationEntryPoint, + AuthenticationSuccessHandler authenticationSuccessHandler, + AuthenticationFailureHandler authenticationFailureHandler, + InvalidSessionStrategy invalidSessionStrategy, Optional logoutHandler, + LogoutSuccessHandler logoutSuccessHandler, SpringAddonsOidcProperties addonsProperties, + ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor, + ClientSynchronizedHttpSecurityPostProcessor httpPostProcessor, + Optional oidcBackChannelLogoutHandler) throws Exception { + // @formatter:off log.info("Applying client OAuth2 configuration for: {}", addonsProperties.getClient().getSecurityMatchers()); http.securityMatcher(addonsProperties.getClient().getSecurityMatchers().toArray(new String[] {})); @@ -168,181 +184,191 @@ SecurityFilterChain springAddonsClientFilterChain( }); // @formatter:on - if (oidcBackChannelLogoutHandler.isPresent()) { - http.oidcLogout(ol -> ol.backChannel(bc -> bc.logoutHandler(oidcBackChannelLogoutHandler.get()))); - } + if (oidcBackChannelLogoutHandler.isPresent()) { + http.oidcLogout( + ol -> ol.backChannel(bc -> bc.logoutHandler(oidcBackChannelLogoutHandler.get()))); + } - ServletConfigurationSupport.configureClient(http, serverProperties, addonsProperties, authorizePostProcessor, httpPostProcessor); + ServletConfigurationSupport.configureClient(http, serverProperties, addonsProperties, + authorizePostProcessor, httpPostProcessor); - return http.build(); - } + return http.build(); + } - /** - * Use a {@link SpringAddonsOAuth2AuthorizationRequestResolver} which: - *
    - *
  • takes hostname and port from configuration properties (and works even if SSL is enabled on port 8080)
  • - *
  • spport defining additionl authorization request parameters from properties
  • - *
- * - * @param bootClientProperties "standard" Spring Boot OAuth2 client properties - * @param clientRegistrationRepository - * @param addonsProperties "spring-addons" OAuth2 client properties - * @return {@link SpringAddonsOAuth2AuthorizationRequestResolver} - */ - @ConditionalOnMissingBean - @Bean - OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver( - OAuth2ClientProperties bootClientProperties, - ClientRegistrationRepository clientRegistrationRepository, - SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOAuth2AuthorizationRequestResolver(bootClientProperties, clientRegistrationRepository, addonsProperties.getClient()); - } + /** + * Use a {@link SpringAddonsOAuth2AuthorizationRequestResolver} which: + *
    + *
  • takes hostname and port from configuration properties (and works even if SSL is enabled on + * port 8080)
  • + *
  • spport defining additionl authorization request parameters from properties
  • + *
+ * + * @param bootClientProperties "standard" Spring Boot OAuth2 client properties + * @param clientRegistrationRepository + * @param addonsProperties "spring-addons" OAuth2 client properties + * @return {@link SpringAddonsOAuth2AuthorizationRequestResolver} + */ + @ConditionalOnMissingBean + @Bean + OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver( + OAuth2ClientProperties bootClientProperties, + ClientRegistrationRepository clientRegistrationRepository, + SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOAuth2AuthorizationRequestResolver(bootClientProperties, + clientRegistrationRepository, addonsProperties.getClient()); + } - /** - * Build logout request for RP-Initiated Logout. It works with most OIDC - * provider: those complying with the spec (Keycloak for instance), off course, but also those which are close enough to it (Auth0, Cognito, ...) - * - * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties} to pick logout configuration for divergence to the standard (logout URI not provided - * in .well-known/openid-configuration and non-conform parameter names) - * @return {@link SpringAddonsOAuth2LogoutRequestUriBuilder] - */ - @ConditionalOnMissingBean - @Bean - LogoutRequestUriBuilder logoutRequestUriBuilder(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOAuth2LogoutRequestUriBuilder(addonsProperties.getClient()); - } + /** + * Build logout request for + * RP-Initiated + * Logout. It works with most OIDC provider: those complying with the spec (Keycloak for + * instance), off course, but also those which are close enough to it (Auth0, Cognito, ...) + * + * @param addonsProperties {@link SpringAddonsOAuth2ClientProperties} to pick logout configuration + * for divergence to the standard (logout URI not provided in + * .well-known/openid-configuration and non-conform parameter names) + * @return {@link SpringAddonsOAuth2LogoutRequestUriBuilder] + */ + @ConditionalOnMissingBean + @Bean + LogoutRequestUriBuilder logoutRequestUriBuilder(SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOAuth2LogoutRequestUriBuilder(addonsProperties.getClient()); + } - /** - * Single tenant logout handler for OIDC provider complying to RP-Initiated - * Logout (or approximately complying to it like Auth0 or Cognito) - * - * @param logoutRequestUriBuilder delegate doing the smart job - * @param clientRegistrationRepository - * @param addonsProperties - * @return {@link SpringAddonsLogoutSuccessHandler} - */ - @ConditionalOnMissingBean - @Bean - LogoutSuccessHandler logoutSuccessHandler( - LogoutRequestUriBuilder logoutRequestUriBuilder, - ClientRegistrationRepository clientRegistrationRepository, - SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsLogoutSuccessHandler(logoutRequestUriBuilder, clientRegistrationRepository, addonsProperties); - } + /** + * Single tenant logout handler for OIDC provider complying to + * RP-Initiated Logout + * (or approximately complying to it like Auth0 or Cognito) + * + * @param logoutRequestUriBuilder delegate doing the smart job + * @param clientRegistrationRepository + * @param addonsProperties + * @return {@link SpringAddonsLogoutSuccessHandler} + */ + @ConditionalOnMissingBean + @Bean + LogoutSuccessHandler logoutSuccessHandler(LogoutRequestUriBuilder logoutRequestUriBuilder, + ClientRegistrationRepository clientRegistrationRepository, + SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsLogoutSuccessHandler(logoutRequestUriBuilder, + clientRegistrationRepository, addonsProperties); + } - /** - * @return a Post processor for access control in Java configuration which requires users to be authenticated. It is called after "permit-all" configuration - * property was applied. - */ - @ConditionalOnMissingBean - @Bean - ClientExpressionInterceptUrlRegistryPostProcessor clientAuthorizePostProcessor() { - return registry -> registry.anyRequest().authenticated(); - } + /** + * @return a Post processor for access control in Java configuration which requires users to be + * authenticated. It is called after "permit-all" configuration property was applied. + */ + @ConditionalOnMissingBean + @Bean + ClientExpressionInterceptUrlRegistryPostProcessor clientAuthorizePostProcessor() { + return registry -> registry.anyRequest().authenticated(); + } - /** - * @return a no-op post processor - */ - @ConditionalOnMissingBean - @Bean - ClientSynchronizedHttpSecurityPostProcessor clientHttpPostProcessor() { - return http -> http; - } + /** + * @return a no-op post processor + */ + @ConditionalOnMissingBean + @Bean + ClientSynchronizedHttpSecurityPostProcessor clientHttpPostProcessor() { + return http -> http; + } - @ConditionalOnMissingBean - @Bean - PreAuthorizationCodeRedirectStrategy authorizationCodeRedirectStrategy(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsPreAuthorizationCodeRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode()); - } + @ConditionalOnMissingBean + @Bean + PreAuthorizationCodeRedirectStrategy authorizationCodeRedirectStrategy( + SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsPreAuthorizationCodeRedirectStrategy( + addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode()); + } - public static class SpringAddonsPreAuthorizationCodeRedirectStrategy extends SpringAddonsOauth2RedirectStrategy - implements - PreAuthorizationCodeRedirectStrategy { - public SpringAddonsPreAuthorizationCodeRedirectStrategy(HttpStatus defaultStatus) { - super(defaultStatus); - } + public static class SpringAddonsPreAuthorizationCodeRedirectStrategy + extends SpringAddonsOauth2RedirectStrategy implements PreAuthorizationCodeRedirectStrategy { + public SpringAddonsPreAuthorizationCodeRedirectStrategy(HttpStatus defaultStatus) { + super(defaultStatus); } + } - @ConditionalOnMissingBean(InvalidSessionStrategy.class) - @Bean - InvalidSessionStrategy invalidSessionStrategy(SpringAddonsOidcProperties addonsProperties) { - final var location = addonsProperties - .getClient() - .getLoginUri() - .orElse( - UriComponentsBuilder - .fromUri(addonsProperties.getClient().getClientUri()) - .pathSegment(addonsProperties.getClient().getClientUri().getPath(), "/login") - .build() - .toUri()) - .toString(); - log - .debug( - "Invalid session. Returning %d and request authentication at %s" - .formatted(addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy().value(), location)); - - if (addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy() == HttpStatus.FOUND) { - return new SimpleRedirectInvalidSessionStrategy(location); - } + @ConditionalOnMissingBean(InvalidSessionStrategy.class) + @Bean + InvalidSessionStrategy invalidSessionStrategy(SpringAddonsOidcProperties addonsProperties) { + final var location = addonsProperties.getClient().getLoginUri() + .orElse(UriComponentsBuilder.fromUri(addonsProperties.getClient().getClientUri()) + .pathSegment(addonsProperties.getClient().getClientUri().getPath(), "/login").build() + .toUri()) + .toString(); + log.debug("Invalid session. Returning %d and request authentication at %s".formatted( + addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy().value(), + location)); - return (HttpServletRequest request, HttpServletResponse response) -> { - response.setStatus(addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy().value()); - response.setHeader(HttpHeaders.LOCATION, location); - if (addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy().is4xxClientError() || addonsProperties - .getClient() - .getOauth2Redirections() - .getInvalidSessionStrategy() - .is5xxServerError()) { - response.getOutputStream().write("Invalid session. Please authenticate at %s".formatted(location).getBytes()); - } - response.flushBuffer(); - }; + if (addonsProperties.getClient().getOauth2Redirections() + .getInvalidSessionStrategy() == HttpStatus.FOUND) { + return new SimpleRedirectInvalidSessionStrategy(location); } - @Conditional(DefaultAuthenticationEntryPointCondition.class) - @Bean - AuthenticationEntryPoint authenticationEntryPoint(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsAuthenticationEntryPoint(addonsProperties.getClient()); - } + return (HttpServletRequest request, HttpServletResponse response) -> { + response.setStatus( + addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy().value()); + response.setHeader(HttpHeaders.LOCATION, location); + if (addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy() + .is4xxClientError() + || addonsProperties.getClient().getOauth2Redirections().getInvalidSessionStrategy() + .is5xxServerError()) { + response.getOutputStream() + .write("Invalid session. Please authenticate at %s".formatted(location).getBytes()); + } + response.flushBuffer(); + }; + } - @Conditional(DefaultAuthenticationSuccessHandlerCondition.class) - @Bean - AuthenticationSuccessHandler authenticationSuccessHandler(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOauth2AuthenticationSuccessHandler(addonsProperties); - } + @Conditional(DefaultAuthenticationEntryPointCondition.class) + @Bean + AuthenticationEntryPoint authenticationEntryPoint(SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsAuthenticationEntryPoint(addonsProperties.getClient()); + } - @Conditional(DefaultAuthenticationFailureHandlerCondition.class) - @Bean - AuthenticationFailureHandler authenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) { - return new SpringAddonsOauth2AuthenticationFailureHandler(addonsProperties); - } + @Conditional(DefaultAuthenticationSuccessHandlerCondition.class) + @Bean + AuthenticationSuccessHandler authenticationSuccessHandler( + SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOauth2AuthenticationSuccessHandler(addonsProperties); + } - /** - * FIXME: use only the new CORS properties at next major release - */ - @Conditional(DefaultCorsFilterCondition.class) - @Bean - CorsFilter corsFilter(SpringAddonsOidcProperties addonsProperties) { - final var corsProps = new ArrayList<>(addonsProperties.getCors()); - final var deprecatedClientCorsProps = addonsProperties.getClient().getCors(); - corsProps.addAll(deprecatedClientCorsProps); + @Conditional(DefaultAuthenticationFailureHandlerCondition.class) + @Bean + AuthenticationFailureHandler authenticationFailureHandler( + SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsOauth2AuthenticationFailureHandler(addonsProperties); + } - return ServletConfigurationSupport.getCorsFilterBean(corsProps); - } + /** + * FIXME: use only the new CORS properties at next major release + */ + @Conditional(DefaultCorsFilterCondition.class) + @Bean + CorsFilter corsFilter(SpringAddonsOidcProperties addonsProperties) { + final var corsProps = new ArrayList<>(addonsProperties.getCors()); + final var deprecatedClientCorsProps = addonsProperties.getClient().getCors(); + corsProps.addAll(deprecatedClientCorsProps); - @Conditional(DefaultOidcSessionRegistryCondition.class) - @Bean - OidcSessionRegistry oidcSessionRegistry() { - return new InMemoryOidcSessionRegistry(); - } + return ServletConfigurationSupport.getCorsFilterBean(corsProps); + } - @Conditional(DefaultOidcBackChannelLogoutHandlerCondition.class) - @Bean - OidcBackChannelLogoutHandler oidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry, SpringAddonsOidcProperties addonsProperties) { - OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry); - addonsProperties.getClient().getBackChannelLogout().getInternalLogoutUri().ifPresent(logoutHandler::setLogoutUri); - addonsProperties.getClient().getBackChannelLogout().getCookieName().ifPresent(logoutHandler::setSessionCookieName); - return logoutHandler; - } + @Conditional(DefaultOidcSessionRegistryCondition.class) + @Bean + OidcSessionRegistry oidcSessionRegistry() { + return new InMemoryOidcSessionRegistry(); + } + + @Conditional(DefaultOidcBackChannelLogoutHandlerCondition.class) + @Bean + OidcBackChannelLogoutHandler oidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry, + SpringAddonsOidcProperties addonsProperties) { + OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry); + addonsProperties.getClient().getBackChannelLogout().getInternalLogoutUri() + .ifPresent(logoutHandler::setLogoutUri); + addonsProperties.getClient().getBackChannelLogout().getCookieName() + .ifPresent(logoutHandler::setSessionCookieName); + return logoutHandler; + } }