diff --git a/README.MD b/README.MD index 5b7432cf5..77cd31387 100644 --- a/README.MD +++ b/README.MD @@ -2,7 +2,7 @@ You can now **test your OAuth2 / OpenID knowledge with a dedicated quiz** availa 7.x is a break through in usability: all 6 `spring-addons` Boot starters are merged into a single one: [`com.c4-soft.springaddons:spring-addons-starter-oidc`](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons-starter-oidc/), and so are 4 of the test libs: [`com.c4-soft.springaddons:spring-addons-starter-oidc-test`](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons-starter-oidc-test/). To use the test annotations without the starter, the dependency is unchanged: [`com.c4-soft.springaddons:spring-addons-oauth2-test`](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons-oauth2-test/). -Please follow the [migration guide](https://github.com/ch4mpy/spring-addons/blob/master/7.0.0-migration-guide.md) to move from `6.x` to `7.1.9`. There is no urge to do so on existing projects as 6.2.x patches should be published untill the end of 2023. +Please follow the [migration guide](https://github.com/ch4mpy/spring-addons/blob/master/7.0.0-migration-guide.md) to move from `6.x` to `7.1.10`. There is no urge to do so on existing projects as 6.2.x patches should be published untill the end of 2023. All samples and tutorials sources are migrated to latest starter and test annotations, but some READMEs might still need a refresh. Please make sure you refer to source code for up to date configuration. @@ -426,7 +426,7 @@ This starters are designed to push auto-configuration one step further. In most I could forget to update README before releasing, so please refer to [maven central](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons/) to pick latest available release ```xml - 7.1.9 + 7.1.10 @@ -462,6 +462,10 @@ I could forget to update README before releasing, so please refer to [maven cent ### 5.1. `7.x` Branch +#### `7.1.10` +- Spring boot `3.1.5` as transcient dependency +- [gh-151](https://github.com/ch4mpy/spring-addons/issues/151) scan application context for `authenticationEntryPoint` and `accessDeniedHandler` to auto-configure resource servers (default returns `401` for unauthorized requests instead of `302 redirect to login`). + #### `7.1.9` - Spring boot `3.1.4` as transcient dependency - [gh-147](https://github.com/ch4mpy/spring-addons/issues/147) prevent addons test security conf to be auto-configured (complicates integration testing with test containers) diff --git a/pom.xml b/pom.xml index fc4e1bd6b..a9fb486b4 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ 3.9.1.2184 - 3.1.4 + 3.1.5 6.3.1.Final 6.2.7.Final diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java index 496fbe06b..27beec983 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ReactiveConfigurationSupport.java @@ -3,9 +3,11 @@ import static org.springframework.security.config.Customizer.withDefaults; import java.util.Arrays; +import java.util.Optional; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository; @@ -28,7 +30,8 @@ public static ServerHttpSecurity configureResourceServer( ServerHttpSecurity http, ServerProperties serverProperties, SpringAddonsOidcResourceServerProperties addonsResourceServerProperties, - ServerAccessDeniedHandler accessDeniedHandler, + ServerAuthenticationEntryPoint authenticationEntryPoint, + Optional accessDeniedHandler, ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor, ResourceServerHttpSecurityPostProcessor httpPostProcessor) { @@ -36,7 +39,10 @@ public static ServerHttpSecurity configureResourceServer( ReactiveConfigurationSupport.configureState(http, addonsResourceServerProperties.isStatlessSessions(), addonsResourceServerProperties.getCsrf()); ReactiveConfigurationSupport.configureAccess(http, addonsResourceServerProperties.getPermitAll()); - http.exceptionHandling(handling -> handling.accessDeniedHandler(accessDeniedHandler)); + http.exceptionHandling(handling -> { + handling.authenticationEntryPoint(authenticationEntryPoint); + accessDeniedHandler.ifPresent(handling::accessDeniedHandler); + }); if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) { http.redirectToHttps(withDefaults()); diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java index fb49a08e5..1183df565 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/resourceserver/ReactiveSpringAddonsOidcResourceServerBeans.java @@ -19,15 +19,15 @@ import org.springframework.core.annotation.Order; import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jwt.Jwt; @@ -37,7 +37,12 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerReactiveAuthenticationManagerResolver; import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.security.web.server.csrf.CsrfToken; import org.springframework.util.StringUtils; @@ -60,22 +65,22 @@ /** *

* Usage
- * If not using spring-boot, @Import or @ComponentScan this class. All beans defined here are @ConditionalOnMissingBean => just define your own - * @Beans to override. + * If not using spring-boot, @Import or @ComponentScan this class. All beans defined here are @ConditionalOnMissingBean => + * just define your own @Beans to override. *

*

* Provided @Beans *

*
    - *
  • SecurityWebFilterChain: applies CORS, CSRF, anonymous, sessionCreationPolicy, SSL redirect and 401 instead of redirect to login properties as - * defined in {@link SpringAddonsOidcProperties}
  • - *
  • AuthorizeExchangeSpecPostProcessor. Override if you need fined grained HTTP security (more than authenticated() to all routes but the ones defined - * as permitAll() in {@link SpringAddonsOidcProperties}
  • + *
  • SecurityWebFilterChain: applies CORS, CSRF, anonymous, sessionCreationPolicy, SSL redirect and 401 instead of redirect to + * login properties as defined in {@link SpringAddonsOidcProperties}
  • + *
  • AuthorizeExchangeSpecPostProcessor. Override if you need fined grained HTTP security (more than authenticated() to all routes + * but the ones defined as permitAll() in {@link SpringAddonsOidcProperties}
  • *
  • Jwt2AuthoritiesConverter: responsible for converting the JWT into Collection<? extends GrantedAuthority>
  • - *
  • ReactiveJwt2OpenidClaimSetConverter<T extends Map<String, Object> & Serializable>: responsible for converting the JWT into a - * claim-set of your choice (OpenID or not)
  • - *
  • ReactiveJwt2AuthenticationConverter<OAuthentication<T extends OpenidClaimSet>>: responsible for converting the JWT into an - * Authentication (uses both beans above)
  • + *
  • ReactiveJwt2OpenidClaimSetConverter<T extends Map<String, Object> & Serializable>: responsible for converting + * the JWT into a claim-set of your choice (OpenID or not)
  • + *
  • ReactiveJwt2AuthenticationConverter<OAuthentication<T extends OpenidClaimSet>>: responsible for converting the JWT + * into an Authentication (uses both beans above)
  • *
  • ReactiveAuthenticationManagerResolver: required to be able to define more than one token issuer until * https://github.com/spring-projects/spring-boot/issues/30108 is solved
  • *
@@ -91,8 +96,8 @@ public class ReactiveSpringAddonsOidcResourceServerBeans { /** *

- * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no security matcher and - * an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers. + * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no + * security matcher and an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers. *

*

* You should consider to set security matcher to all other {@link SecurityWebFilterChain} beans and provide a @@ -106,9 +111,10 @@ public class ReactiveSpringAddonsOidcResourceServerBeans { * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all" * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration * @param authenticationManagerResolver Converts successful JWT decoding result into an {@link Authentication} - * @param accessDeniedHandler handler for unauthorized requests (missing or invalid access-token) - * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with JWT decoder(matches all unmatched - * routes with lowest precedence) + * @param authenticationEntryPoint The {@link AuthenticationEntryPoint} to use (defaults returns 401) + * @param accessDeniedHandler An optional {@link AccessDeniedHandler} to use instead of Boot default one + * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with JWT decoder(matches all + * unmatched routes with lowest precedence) */ @Conditional(IsJwtDecoderResourceServerCondition.class) @Order(Ordered.LOWEST_PRECEDENCE) @@ -120,24 +126,27 @@ SecurityWebFilterChain springAddonsJwtResourceServerSecurityFilterChain( ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor, ResourceServerHttpSecurityPostProcessor httpPostProcessor, ReactiveAuthenticationManagerResolver authenticationManagerResolver, - ServerAccessDeniedHandler accessDeniedHandler) { + ServerAuthenticationEntryPoint authenticationEntryPoint, + Optional accessDeniedHandler) { http.oauth2ResourceServer(server -> server.authenticationManagerResolver(authenticationManagerResolver)); - ReactiveConfigurationSupport.configureResourceServer( - http, - serverProperties, - addonsProperties.getResourceserver(), - accessDeniedHandler, - authorizePostProcessor, - httpPostProcessor); + ReactiveConfigurationSupport + .configureResourceServer( + http, + serverProperties, + addonsProperties.getResourceserver(), + authenticationEntryPoint, + accessDeniedHandler, + authorizePostProcessor, + httpPostProcessor); return http.build(); } /** *

- * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no security matcher and - * an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers. + * Applies SpringAddonsSecurityProperties to web security config. Be aware that defining a {@link SecurityWebFilterChain} bean with no + * security matcher and an order higher than LOWEST_PRECEDENCE will disable most of this lib auto-configuration for OpenID resource-servers. *

*

* You should consider to set security matcher to all other {@link SecurityWebFilterChain} beans and provide a @@ -151,9 +160,10 @@ SecurityWebFilterChain springAddonsJwtResourceServerSecurityFilterChain( * @param authorizePostProcessor Hook to override access-control rules for all path that are not listed in "permit-all" * @param httpPostProcessor Hook to override all or part of HttpSecurity auto-configuration * @param introspectionAuthenticationConverter Converts successful introspection result into an {@link Authentication} - * @param accessDeniedHandler handler for unauthorized requests (missing or invalid access-token) - * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with access-token introspection - * (matches all unmatched routes with lowest precedence) + * @param authenticationEntryPoint The {@link AuthenticationEntryPoint} to use (defaults returns 401) + * @param accessDeniedHandler An optional {@link AccessDeniedHandler} to use instead of Boot default one + * @return A default {@link SecurityWebFilterChain} for reactive resource-servers with access-token + * introspection (matches all unmatched routes with lowest precedence) */ @Conditional(IsIntrospectingResourceServerCondition.class) @Order(Ordered.LOWEST_PRECEDENCE) @@ -164,21 +174,24 @@ SecurityWebFilterChain springAddonsIntrospectingResourceServerSecurityFilterChai SpringAddonsOidcProperties addonsProperties, ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor, ResourceServerHttpSecurityPostProcessor httpPostProcessor, - org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter introspectionAuthenticationConverter, - org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector, - ServerAccessDeniedHandler accessDeniedHandler) { + ReactiveOpaqueTokenAuthenticationConverter introspectionAuthenticationConverter, + ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector, + ServerAuthenticationEntryPoint authenticationEntryPoint, + Optional accessDeniedHandler) { http.oauth2ResourceServer(server -> server.opaqueToken(ot -> { ot.introspector(opaqueTokenIntrospector); ot.authenticationConverter(introspectionAuthenticationConverter); })); - ReactiveConfigurationSupport.configureResourceServer( - http, - serverProperties, - addonsProperties.getResourceserver(), - accessDeniedHandler, - authorizePostProcessor, - httpPostProcessor); + ReactiveConfigurationSupport + .configureResourceServer( + http, + serverProperties, + addonsProperties.getResourceserver(), + authenticationEntryPoint, + accessDeniedHandler, + authorizePostProcessor, + httpPostProcessor); return http.build(); } @@ -195,10 +208,11 @@ ResourceServerAuthorizeExchangeSpecPostProcessor authorizePostProcessor() { } /** - * Hook to override all or part of HttpSecurity auto-configuration. Called after spring-addons configuration was applied so that you can modify anything + * 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 + * @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 @@ -231,12 +245,17 @@ ReactiveAuthenticationManagerResolver authenticationManagerRe final Map> jwtManagers = Stream.of(addonsProperties.getOps()).collect(Collectors.toMap(issuer -> issuer.getIss().toString(), issuer -> { - final var decoder = issuer.getJwkSetUri() != null && StringUtils.hasLength(issuer.getJwkSetUri().toString()) - ? NimbusReactiveJwtDecoder.withJwkSetUri(issuer.getJwkSetUri().toString()).build() - : NimbusReactiveJwtDecoder.withIssuerLocation(issuer.getIss().toString()).build(); + final var decoder = + issuer.getJwkSetUri() != null && StringUtils.hasLength(issuer.getJwkSetUri().toString()) + ? NimbusReactiveJwtDecoder.withJwkSetUri(issuer.getJwkSetUri().toString()).build() + : NimbusReactiveJwtDecoder.withIssuerLocation(issuer.getIss().toString()).build(); - final OAuth2TokenValidator defaultValidator = Optional.ofNullable(issuer.getIss()).map(URI::toString) - .map(JwtValidators::createDefaultWithIssuer).orElse(JwtValidators.createDefault()); + final OAuth2TokenValidator defaultValidator = + Optional + .ofNullable(issuer.getIss()) + .map(URI::toString) + .map(JwtValidators::createDefaultWithIssuer) + .orElse(JwtValidators.createDefault()); // If the spring-addons conf for resource server contains a non empty audience, add an audience validator // @formatter:off @@ -255,10 +274,11 @@ ReactiveAuthenticationManagerResolver authenticationManagerRe return Mono.just(provider); })); - log.debug( - "Building default JwtIssuerReactiveAuthenticationManagerResolver with: {} {}", - auth2ResourceServerProperties.getJwt(), - Stream.of(addonsProperties.getOps()).toList()); + log + .debug( + "Building default JwtIssuerReactiveAuthenticationManagerResolver with: {} {}", + auth2ResourceServerProperties.getJwt(), + Stream.of(addonsProperties.getOps()).toList()); return new JwtIssuerReactiveAuthenticationManagerResolver(issuerLocation -> jwtManagers.getOrDefault(issuerLocation, Mono.empty())); } @@ -269,12 +289,11 @@ ReactiveAuthenticationManagerResolver authenticationManagerRe */ @ConditionalOnMissingBean @Bean - ServerAccessDeniedHandler serverAccessDeniedHandler() { - log.debug("Building default ServerAccessDeniedHandler"); - return (var exchange, var ex) -> exchange.getPrincipal().flatMap(principal -> { + ServerAuthenticationEntryPoint authenticationEntryPoint() { + return (ServerWebExchange exchange, AuthenticationException ex) -> exchange.getPrincipal().flatMap(principal -> { var response = exchange.getResponse(); - response.setStatusCode(principal instanceof AnonymousAuthenticationToken ? HttpStatus.UNAUTHORIZED : HttpStatus.FORBIDDEN); - response.getHeaders().setContentType(MediaType.TEXT_PLAIN); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\""); var dataBufferFactory = response.bufferFactory(); var buffer = dataBufferFactory.wrap(ex.getMessage().getBytes(Charset.defaultCharset())); return response.writeWith(Mono.just(buffer)).doOnError(error -> DataBufferUtils.release(buffer));