From 2b5e12b27c81353e680256c793703f3c4944c2c4 Mon Sep 17 00:00:00 2001 From: ch4mpy Date: Tue, 21 Nov 2023 22:29:31 -1000 Subject: [PATCH] gh-155 Configurable HTTP status in OAuth2 flows responses --- README.MD | 22 ++- .../official_bff/SecurityConf.java | 149 +++++++++++++++--- ...itional-spring-configuration-metadata.json | 12 ++ .../src/main/resources/application.yml | 16 +- .../src/main/resources/application.yml | 4 +- .../c4_bff/SecurityConf.java | 28 ---- .../bff-c4/src/main/resources/application.yml | 27 +++- .../src/main/resources/application.yml | 4 +- .../SpringAddonsOidcClientProperties.java | 27 ++++ .../C4Oauth2ServerRedirectStrategy.java | 53 +++++++ .../ReactiveSpringAddonsOidcClientBeans.java | 50 +++++- ...pringAddonsServerLogoutSuccessHandler.java | 17 +- .../client/C4Oauth2RedirectStrategy.java | 45 ++++++ .../SpringAddonsLogoutSuccessHandler.java | 23 ++- .../client/SpringAddonsOidcClientBeans.java | 66 +++++++- 15 files changed, 452 insertions(+), 91 deletions(-) create mode 100644 spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/C4Oauth2ServerRedirectStrategy.java create mode 100644 spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/C4Oauth2RedirectStrategy.java diff --git a/README.MD b/README.MD index 8b4961543..a7f5a7203 100644 --- a/README.MD +++ b/README.MD @@ -1,8 +1,6 @@ You can now **test your OAuth2 / OpenID knowledge with a dedicated quiz** available at [https://quiz.c4-soft.com/ui/quizzes](https://quiz.c4-soft.com/ui/quizzes) before you rush into configuring your applications. -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.1`. There is no urge to do so on existing projects as 6.2.x patches should be published until 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.x`. 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 +424,7 @@ These 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.14 + 7.1.15 @@ -462,6 +460,22 @@ I could forget to update README before releasing, so please refer to [maven cent ### 5.1. `7.x` Branch +#### `7.1.15` +- [gh-155](https://github.com/ch4mpy/spring-addons/issues/155) Configurable HTTP status for responses to authorization_code flow initiation, authorization-code callback and logout. This makes BFF configuration easier for single page and mobile applications. Default OAuth2 response status (`302 Found`) can be overriden with: +```yaml +com: + c4-soft: + springaddons: + oidc: + ops: + client: + oauth2-redirections: + pre-authorization-code: FOUND + post-authorization-code: FOUND + rp-initiated-logout: ACCEPTED +``` +A per-request override can be done by setting `X-RESPONSE-STATUS` header with either a status code or label (for instance, both `201` and `ACCEPTED` are accepted as value). + #### `7.1.14` - update CSRF configuration for SPAs as instructed by spring-security team in https://github.com/spring-projects/spring-security/issues/14125 diff --git a/samples/tutorials/bff/backend/official/bff-official/src/main/java/com/c4_soft/dzone_oauth2_spring/official_bff/SecurityConf.java b/samples/tutorials/bff/backend/official/bff-official/src/main/java/com/c4_soft/dzone_oauth2_spring/official_bff/SecurityConf.java index b0438d79b..064b079f6 100644 --- a/samples/tutorials/bff/backend/official/bff-official/src/main/java/com/c4_soft/dzone_oauth2_spring/official_bff/SecurityConf.java +++ b/samples/tutorials/bff/backend/official/bff-official/src/main/java/com/c4_soft/dzone_oauth2_spring/official_bff/SecurityConf.java @@ -2,7 +2,11 @@ import static org.springframework.security.config.Customizer.withDefaults; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.util.List; +import java.util.Optional; import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Value; @@ -14,26 +18,34 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.authentication.AnonymousAuthenticationToken; 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.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.ServerRedirectStrategy; import org.springframework.security.web.server.WebFilterExchange; -import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler; -import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository; import org.springframework.security.web.server.csrf.CsrfToken; +import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestAttributeHandler; import org.springframework.security.web.server.csrf.XorServerCsrfTokenRequestAttributeHandler; 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.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; +import org.springframework.web.util.UriComponentsBuilder; +import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; @Configuration @@ -42,18 +54,18 @@ public class SecurityConf { /** *

- * Security filter-chain for resources needing sessions with CSRF protection enabled and CSRF token cookie accessible to Angular - * application. + * Security filter-chain for resources needing sessions with CSRF protection enabled and CSRF token cookie accessible to Angular application. *

*

* It is defined with low order (high precedence) and security-matcher to limit the resources it applies to. *

- * + * * @param http * @param clientRegistrationRepository * @param securityMatchers * @param permitAll * @return + * @throws URISyntaxException */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) @@ -61,31 +73,40 @@ SecurityWebFilterChain clientFilterCHain( ServerHttpSecurity http, ServerProperties serverProperties, ReactiveClientRegistrationRepository clientRegistrationRepository, + @Value("${gateway-uri}") URI gatewayUri, @Value("${client-security-matchers:[]}") String[] securityMatchers, @Value("${client-permit-all:[]}") String[] permitAll, - @Value("${post-logout-redirect-uri}") String postLogoutRedirectUri) { + @Value("${pre-authorization-status:FOUND}") HttpStatus preAuthorizationStatus, + @Value("${post-authorization-status:FOUND}") HttpStatus postAuthorizationStatus, + @Value("${post-logout-redirect-uri}") String postLogoutRedirectUri) + throws URISyntaxException { // Apply this filter-chain only to resources needing sessions final var clientRoutes = Stream.of(securityMatchers).map(PathPatternParserServerWebExchangeMatcher::new).map(ServerWebExchangeMatcher.class::cast).toList(); http.securityMatcher(new OrServerWebExchangeMatcher(clientRoutes)); - // Set post-login URI to Angular app (login being successful or not) + // The following handlers answer with NO_CONTENT HTTP status so that single page and mobile apps can handle the redirection by themselves http.oauth2Login(login -> { - login.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/ui/")); - login.authenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/ui/")); + login.authorizationRedirectStrategy(new C4OAuth2ServerRedirectStrategy(preAuthorizationStatus)); + + // Set post-login URI to Angular app (login being successful or not) + final var uiUri = UriComponentsBuilder.fromUri(gatewayUri).path("/ui/").build().toUri(); + login.authenticationSuccessHandler(new C4Oauth2ServerAuthenticationSuccessHandler(postAuthorizationStatus, uiUri)); + login.authenticationFailureHandler(new C4Oauth2ServerAuthenticationFailureHandler(postAuthorizationStatus, uiUri)); }); - // Keycloak fully complies with RP-Initiated Logout + // Keycloak fully complies with RP-Initiated Logout but we need an answer in the 2xx range for single page and mobile apps to handle the redirection by + // themselves + // The following is a wrapper around the OidcClientInitiatedServerLogoutSuccessHandler to change the response status. http.logout(logout -> { - logout.logoutSuccessHandler(new AngularLogoutSucessHandler(clientRegistrationRepository, postLogoutRedirectUri)); + logout.logoutSuccessHandler(new SpaLogoutSucessHandler(clientRegistrationRepository, postLogoutRedirectUri)); }); // Sessions being necessary, configure CSRF protection to work with Angular. // Note the csrfCookieWebFilter below which actually attaches the CSRF token cookie to responses http.csrf(csrf -> { - var delegate = new XorServerCsrfTokenRequestAttributeHandler(); - csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(delegate::handle); + csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()); }); // If SSL enabled, disable http (https only) @@ -102,6 +123,9 @@ SecurityWebFilterChain clientFilterCHain( return http.build(); } + /** + * @return second half of CSRF handling for SPAs + */ @Bean WebFilter csrfCookieWebFilter() { return (exchange, chain) -> { @@ -115,10 +139,10 @@ WebFilter csrfCookieWebFilter() { * Security filter-chain for resources for which sessions are not needed. *

*

- * It is defined with lower precedence (higher order) than the client filter-chain and no security matcher => this one acts as default for - * all requests that do not match the client filter-chain secutiy-matcher. + * It is defined with lower precedence (higher order) than the client filter-chain and no security matcher => this one acts as default for all requests that + * do not match the client filter-chain secutiy-matcher. *

- * + * * @param http * @param serverProperties * @param permitAll @@ -166,23 +190,102 @@ SecurityWebFilterChain resourceServerFilterCHain( return http.build(); } - static class AngularLogoutSucessHandler implements ServerLogoutSuccessHandler { + @RequiredArgsConstructor + static class C4OAuth2ServerRedirectStrategy implements ServerRedirectStrategy { + private final HttpStatus defaultStatus; + + @Override + public Mono sendRedirect(ServerWebExchange exchange, URI location) { + return Mono.fromRunnable(() -> { + ServerHttpResponse response = exchange.getResponse(); + // @formatter:off + final var status = Optional.ofNullable(exchange.getRequest().getHeaders().get("X-RESPONSE-STATUS")) + .map(List::stream) + .orElse(Stream.empty()) + .filter(StringUtils::hasLength) + .findAny() + .map(statusStr -> { + try { + final var statusCode = Integer.parseInt(statusStr); + return HttpStatus.valueOf(statusCode); + } catch(NumberFormatException e) { + return HttpStatus.valueOf(statusStr.toUpperCase()); + } + }) + .orElse(defaultStatus); + // @formatter:on + response.setStatusCode(status); + response.getHeaders().setLocation(location); + }); + } + + } + + static class C4Oauth2ServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler { + private final URI redirectUri; + private final C4OAuth2ServerRedirectStrategy redirectStrategy; + + public C4Oauth2ServerAuthenticationSuccessHandler(HttpStatus status, URI location) { + this.redirectUri = location; + this.redirectStrategy = new C4OAuth2ServerRedirectStrategy(status); + } + + @Override + public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) { + return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), redirectUri); + } + + } + + static class C4Oauth2ServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler { + private final URI redirectUri; + private final C4OAuth2ServerRedirectStrategy redirectStrategy; + + public C4Oauth2ServerAuthenticationFailureHandler(HttpStatus status, URI location) { + this.redirectUri = location; + this.redirectStrategy = new C4OAuth2ServerRedirectStrategy(status); + } + + @Override + public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) { + return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), redirectUri); + } + } + + static class SpaLogoutSucessHandler implements ServerLogoutSuccessHandler { private final OidcClientInitiatedServerLogoutSuccessHandler delegate; - - public AngularLogoutSucessHandler(ReactiveClientRegistrationRepository clientRegistrationRepository, String postLogoutRedirectUri) { + + public SpaLogoutSucessHandler(ReactiveClientRegistrationRepository clientRegistrationRepository, String postLogoutRedirectUri) { this.delegate = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository); this.delegate.setPostLogoutRedirectUri(postLogoutRedirectUri); } @Override - public - Mono< - Void> - onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) { + public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) { return delegate.onLogoutSuccess(exchange, authentication).then(Mono.fromRunnable(() -> { exchange.getExchange().getResponse().setStatusCode(HttpStatus.ACCEPTED); })); } + } + /** + * Adapted from https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa + */ + static final class SpaCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler { + private final ServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler(); + + @Override + public void handle(ServerWebExchange exchange, Mono csrfToken) { + /* + * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body. + */ + this.delegate.handle(exchange, csrfToken); + } + + @Override + public Mono resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) { + final var hasHeader = exchange.getRequest().getHeaders().get(csrfToken.getHeaderName()).stream().filter(StringUtils::hasText).count() > 0; + return hasHeader ? super.resolveCsrfTokenValue(exchange, csrfToken) : this.delegate.resolveCsrfTokenValue(exchange, csrfToken); + } } } diff --git a/samples/tutorials/bff/backend/official/bff-official/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/samples/tutorials/bff/backend/official/bff-official/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 7929e786d..455fb2b4a 100644 --- a/samples/tutorials/bff/backend/official/bff-official/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/samples/tutorials/bff/backend/official/bff-official/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -58,5 +58,17 @@ "name": "post-logout-redirect-uri", "type": "java.lang.String", "description": "Where to redirect the user after he logged out from the authorization server (probably the UI URI on the gateway)" + }, + { + "name": "pre-authorization-status", + "type": "org.springframework.http.HttpStatus", + "description": "HTTP status for the 1st response in authorization_code flow, with location to the authorization server authorization endpoint", + "defaultValue": "FOUND" + }, + { + "name": "post-authorization-status", + "type": "org.springframework.http.HttpStatus", + "description": "HTTP status for the last response in authorization_code flow, with location back to the UI", + "defaultValue": "FOUND" } ]} \ No newline at end of file diff --git a/samples/tutorials/bff/backend/official/bff-official/src/main/resources/application.yml b/samples/tutorials/bff/backend/official/bff-official/src/main/resources/application.yml index 43d1db7ee..108acaef7 100644 --- a/samples/tutorials/bff/backend/official/bff-official/src/main/resources/application.yml +++ b/samples/tutorials/bff/backend/official/bff-official/src/main/resources/application.yml @@ -88,13 +88,23 @@ logging: boot: DEBUG --- +spring: + config: + activate: + on-profile: ssl + +scheme: https server: ssl: enabled: true - + +--- spring: config: activate: - on-profile: ssl + on-profile: mobile + +pre-authorization-status: NO_CONTENT +post-authorization-status: NO_CONTENT -scheme: https \ No newline at end of file +# gateway-uri: ${scheme}://10.0.2.2:${server.port} \ No newline at end of file diff --git a/samples/tutorials/bff/backend/official/greeting-api-official/src/main/resources/application.yml b/samples/tutorials/bff/backend/official/greeting-api-official/src/main/resources/application.yml index b25c4d7c7..2345125f4 100644 --- a/samples/tutorials/bff/backend/official/greeting-api-official/src/main/resources/application.yml +++ b/samples/tutorials/bff/backend/official/greeting-api-official/src/main/resources/application.yml @@ -1,3 +1,5 @@ +issuer: https://oidc.c4-soft.com/auth/realms/master + server: port: 7084 ssl: @@ -13,7 +15,7 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: https://oidc.c4-soft.com/auth/realms/master + issuer-uri: ${issuer} logging: level: diff --git a/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/java/com/c4_soft/dzone_oauth2_spring/c4_bff/SecurityConf.java b/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/java/com/c4_soft/dzone_oauth2_spring/c4_bff/SecurityConf.java index 52d855f24..6124525d1 100644 --- a/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/java/com/c4_soft/dzone_oauth2_spring/c4_bff/SecurityConf.java +++ b/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/java/com/c4_soft/dzone_oauth2_spring/c4_bff/SecurityConf.java @@ -1,35 +1,7 @@ package com.c4_soft.dzone_oauth2_spring.c4_bff; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.web.server.WebFilterExchange; -import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; -import org.springframework.stereotype.Component; - -import com.c4_soft.springaddons.security.oidc.starter.LogoutRequestUriBuilder; -import com.c4_soft.springaddons.security.oidc.starter.reactive.client.SpringAddonsServerLogoutSuccessHandler; - -import reactor.core.publisher.Mono; @Configuration public class SecurityConf { - - @Component - static class AngularLogoutSucessHandler implements ServerLogoutSuccessHandler { - private final SpringAddonsServerLogoutSuccessHandler delegate; - - public AngularLogoutSucessHandler(LogoutRequestUriBuilder logoutUriBuilder, ReactiveClientRegistrationRepository clientRegistrationRepo) { - this.delegate = new SpringAddonsServerLogoutSuccessHandler(logoutUriBuilder, clientRegistrationRepo); - } - - @Override - public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) { - return delegate.onLogoutSuccess(exchange, authentication).then(Mono.fromRunnable(() -> { - exchange.getExchange().getResponse().setStatusCode(HttpStatus.ACCEPTED); - })); - } - - } } diff --git a/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/resources/application.yml b/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/resources/application.yml index 00ad79b39..bcd1bc2e1 100644 --- a/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/resources/application.yml +++ b/samples/tutorials/bff/backend/with-c4-soft/bff-c4/src/main/resources/application.yml @@ -88,6 +88,8 @@ com: login-path: /ui/ post-login-redirect-path: /ui/ post-logout-redirect-path: /ui/ + oauth2-redirections: + rp-initiated-logout: NO_CONTENT resourceserver: permit-all: - / @@ -106,16 +108,31 @@ logging: level: org: springframework: - security: DEBUG + security: TRACE --- +spring: + config: + activate: + on-profile: ssl + +scheme: https server: ssl: enabled: true - + +--- spring: config: activate: - on-profile: ssl - -scheme: https \ No newline at end of file + on-profile: mobile +com: + c4-soft: + springaddons: + oidc: + client: + oauth2-redirections: + pre-authorization-code: NO_CONTENT + post-authorization-code: NO_CONTENT + +# gateway-uri: ${scheme}://10.0.2.2:${server.port} \ No newline at end of file diff --git a/samples/tutorials/bff/backend/with-c4-soft/greeting-api-c4/src/main/resources/application.yml b/samples/tutorials/bff/backend/with-c4-soft/greeting-api-c4/src/main/resources/application.yml index b646dff5d..e4a85fbf1 100644 --- a/samples/tutorials/bff/backend/with-c4-soft/greeting-api-c4/src/main/resources/application.yml +++ b/samples/tutorials/bff/backend/with-c4-soft/greeting-api-c4/src/main/resources/application.yml @@ -1,3 +1,5 @@ +issuer: https://oidc.c4-soft.com/auth/realms/master + server: port: 7084 ssl: @@ -8,7 +10,7 @@ com: springaddons: oidc: ops: - - iss: https://oidc.c4-soft.com/auth/realms/master + - iss: ${issuer} authorities: - path: $.realm_access.roles username-claim: preferred_username 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 a82fcc0d0..37c0e2a7f 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 @@ -7,6 +7,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.http.HttpStatus; import org.springframework.web.util.UriComponentsBuilder; import lombok.Data; @@ -56,6 +57,12 @@ public class SpringAddonsOidcClientProperties { */ private Optional postLoginRedirectPath = Optional.empty(); + /** + * HTTP status for redirections in OAuth2 login and logout. You might set this to something in 2xx range (like OK, ACCEPTED, NO_CONTENT, ...) for single + * page and mobile applications to handle this redirection as it wishes (change the user-agent, clear some headers, ...). + */ + private OAuth2RedirectionProperties oauth2Redirections = new OAuth2RedirectionProperties(); + public URI getPostLoginRedirectHost() { return postLoginRedirectHost.orElse(clientUri); } @@ -192,6 +199,26 @@ public static class RequestParam { private String value; } + @ConfigurationProperties + @Data + public static class OAuth2RedirectionProperties { + + /** + * Status for the 1st response in authorization code flow, with location to get authorization code from authorization server + */ + private HttpStatus preAuthorizationCode = HttpStatus.FOUND; + + /** + * Status for the response after authorization code, with location to the UI + */ + private HttpStatus postAuthorizationCode = HttpStatus.FOUND; + + /** + * Status for the response after BFF logout, with location to authorization server logout endpoint + */ + private HttpStatus rpInitiatedLogout = HttpStatus.FOUND; + } + public Optional getLogoutProperties(String clientRegistrationId) { return Optional.ofNullable(oauth2Logout.get(clientRegistrationId)); } diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/C4Oauth2ServerRedirectStrategy.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/C4Oauth2ServerRedirectStrategy.java new file mode 100644 index 000000000..6985c67c1 --- /dev/null +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/C4Oauth2ServerRedirectStrategy.java @@ -0,0 +1,53 @@ +package com.c4_soft.springaddons.security.oidc.starter.reactive.client; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.web.server.ServerRedirectStrategy; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; + +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; + +/** + * A redirect strategy that might not actually redirect: the HTTP status is taken from com.c4-soft.springaddons.oidc.client.oauth2-redirect-status property. + * User-agents will auto redirect only if the status is in 3xx range. This gives single page and mobile applications a chance to intercept the redirection and + * choose to follow the redirection (or not), with which agent and potentially by clearing some headers. + * + * @author Jerome Wacongne ch4mp@c4-soft.com + */ +@RequiredArgsConstructor +public class C4Oauth2ServerRedirectStrategy implements ServerRedirectStrategy { + private final HttpStatus defaultStatus; + + @Override + public Mono sendRedirect(ServerWebExchange exchange, URI location) { + return Mono.fromRunnable(() -> { + ServerHttpResponse response = exchange.getResponse(); + // @formatter:off + final var status = Optional.ofNullable(exchange.getRequest().getHeaders().get("X-RESPONSE-STATUS")) + .map(List::stream) + .orElse(Stream.empty()) + .filter(StringUtils::hasLength) + .findAny() + .map(statusStr -> { + try { + final var statusCode = Integer.parseInt(statusStr); + return HttpStatus.valueOf(statusCode); + } catch(NumberFormatException e) { + return HttpStatus.valueOf(statusStr.toUpperCase()); + } + }) + .orElse(defaultStatus); + // @formatter:on + response.setStatusCode(status); + response.getHeaders().setLocation(location); + }); + } + +} \ 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/ReactiveSpringAddonsOidcClientBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java index c1beb306a..dc165e02d 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,5 +1,6 @@ package com.c4_soft.springaddons.security.oidc.starter.reactive.client; +import java.net.URI; import java.util.stream.Stream; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -12,13 +13,16 @@ import org.springframework.core.annotation.Order; 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.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.WebFilterExchange; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint; -import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler; -import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +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; @@ -134,10 +138,9 @@ SecurityWebFilterChain clientFilterChain( http.oauth2Login(oauth2 -> { oauth2.authorizationRequestResolver(authorizationRequestResolver); addonsProperties.getClient().getPostLoginRedirectUri().ifPresent(postLoginRedirectUri -> { - oauth2.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler(postLoginRedirectUri.toString())); - }); - addonsProperties.getClient().getLoginPath().ifPresent(loginPath -> { - oauth2.authenticationFailureHandler(new RedirectServerAuthenticationFailureHandler(UriComponentsBuilder.fromUri(addonsProperties.getClient().getClientUri()).path(loginPath).build().toString())); + oauth2.authorizationRedirectStrategy(new C4Oauth2ServerRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode())); + oauth2.authenticationSuccessHandler(new C4Oauth2ServerAuthenticationSuccessHandler(addonsProperties, postLoginRedirectUri)); + oauth2.authenticationFailureHandler(new C4Oauth2ServerAuthenticationFailureHandler(addonsProperties, postLoginRedirectUri)); }); }); @@ -182,8 +185,8 @@ LogoutRequestUriBuilder logoutRequestUriBuilder(SpringAddonsOidcProperties addon @ConditionalOnMissingBean @Bean ServerLogoutSuccessHandler logoutSuccessHandler(LogoutRequestUriBuilder logoutUriBuilder, - ReactiveClientRegistrationRepository clientRegistrationRepo) { - return new SpringAddonsServerLogoutSuccessHandler(logoutUriBuilder, clientRegistrationRepo); + ReactiveClientRegistrationRepository clientRegistrationRepo, SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsServerLogoutSuccessHandler(logoutUriBuilder, clientRegistrationRepo, addonsProperties); } /** @@ -240,4 +243,35 @@ ServerLogoutHandler logoutHandler() { new WebSessionServerLogoutHandler(), new SecurityContextServerLogoutHandler()); } + + static class C4Oauth2ServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler { + private final URI redirectUri; + private final C4Oauth2ServerRedirectStrategy redirectStrategy; + + public C4Oauth2ServerAuthenticationSuccessHandler(SpringAddonsOidcProperties addonsProperties, URI redirectUri) { + this.redirectUri = redirectUri; + this.redirectStrategy = new C4Oauth2ServerRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode()); + } + + @Override + public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) { + return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), redirectUri); + } + + } + + static class C4Oauth2ServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler { + private final URI redirectUri; + private final C4Oauth2ServerRedirectStrategy redirectStrategy; + + public C4Oauth2ServerAuthenticationFailureHandler(SpringAddonsOidcProperties addonsProperties, URI redirectUri) { + this.redirectUri = redirectUri; + this.redirectStrategy = new C4Oauth2ServerRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode()); + } + + @Override + public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) { + return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), redirectUri); + } + } } \ 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 01a93e159..0f07f1cce 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 @@ -7,7 +7,6 @@ import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.web.server.DefaultServerRedirectStrategy; import org.springframework.security.web.server.ServerRedirectStrategy; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; @@ -15,9 +14,8 @@ 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 lombok.Data; -import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; /** @@ -44,12 +42,19 @@ * @see SpringAddonsOAuth2LogoutRequestUriBuilder * @see SpringAddonsOidcClientProperties */ -@Data -@RequiredArgsConstructor public class SpringAddonsServerLogoutSuccessHandler implements ServerLogoutSuccessHandler { private final LogoutRequestUriBuilder uriBuilder; private final ReactiveClientRegistrationRepository clientRegistrationRepo; - private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); + private final ServerRedirectStrategy redirectStrategy; + + public SpringAddonsServerLogoutSuccessHandler( + LogoutRequestUriBuilder uriBuilder, + ReactiveClientRegistrationRepository clientRegistrationRepo, + SpringAddonsOidcProperties addonsProperties) { + this.uriBuilder = uriBuilder; + this.clientRegistrationRepo = clientRegistrationRepo; + this.redirectStrategy = new C4Oauth2ServerRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getRpInitiatedLogout()); + } @Override public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) { diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/C4Oauth2RedirectStrategy.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/C4Oauth2RedirectStrategy.java new file mode 100644 index 000000000..3711f5692 --- /dev/null +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/C4Oauth2RedirectStrategy.java @@ -0,0 +1,45 @@ +package com.c4_soft.springaddons.security.oidc.starter.synchronised.client; + +import java.io.IOException; +import java.util.Optional; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.util.StringUtils; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +/** + * A redirect strategy that might not actually redirect: the HTTP status is taken from com.c4-soft.springaddons.oidc.client.oauth2-redirect-status property. + * User-agents will auto redirect only if the status is in 3xx range. This gives single page and mobile applications a chance to intercept the redirection and + * choose to follow the redirection (or not), with which agent and potentially by clearing some headers. + * + * @author Jerome Wacongne ch4mp@c4-soft.com + */ +@RequiredArgsConstructor +public class C4Oauth2RedirectStrategy implements RedirectStrategy { + private final HttpStatus defaultStatus; + + @Override + public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String location) throws IOException { + // @formatter:off + final var status = Optional.ofNullable(request.getHeader("X-RESPONSE-STATUS")) + .filter(StringUtils::hasLength) + .map(statusStr -> { + try { + final var statusCode = Integer.parseInt(statusStr); + return HttpStatus.valueOf(statusCode); + } catch(NumberFormatException e) { + return HttpStatus.valueOf(statusStr.toUpperCase()); + } + }) + .orElse(defaultStatus); + // @formatter:on + response.setStatus(status.value()); + response.setHeader(HttpHeaders.LOCATION, location); + } + +} \ No newline at end of file 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 1e3b0a541..b0595d51f 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 @@ -1,5 +1,7 @@ package com.c4_soft.springaddons.security.oidc.starter.synchronised.client; +import java.io.IOException; + import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; @@ -10,12 +12,12 @@ 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 jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; /** *

@@ -41,12 +43,20 @@ * @see SpringAddonsOAuth2LogoutRequestUriBuilder * @see SpringAddonsOidcClientProperties */ -@Data -@RequiredArgsConstructor @EqualsAndHashCode(callSuper = true) public class SpringAddonsLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { private final LogoutRequestUriBuilder uriBuilder; private final ClientRegistrationRepository clientRegistrationRepository; + private final C4Oauth2RedirectStrategy redirectStrategy; + + public SpringAddonsLogoutSuccessHandler( + LogoutRequestUriBuilder uriBuilder, + ClientRegistrationRepository clientRegistrationRepository, + SpringAddonsOidcProperties addonsProperties) { + this.uriBuilder = uriBuilder; + this.clientRegistrationRepository = clientRegistrationRepository; + this.redirectStrategy = new C4Oauth2RedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getRpInitiatedLogout()); + } @Override protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { @@ -56,4 +66,9 @@ protected String determineTargetUrl(HttpServletRequest request, HttpServletRespo } return null; } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + this.redirectStrategy.sendRedirect(request, response, determineTargetUrl(request, response)); + } } \ No newline at end of file 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 0c556745a..ccb5672e3 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,5 +1,8 @@ package com.c4_soft.springaddons.security.oidc.starter.synchronised.client; +import java.io.IOException; +import java.net.URI; + import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -12,12 +15,15 @@ import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; 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.web.util.UriComponentsBuilder; import com.c4_soft.springaddons.security.oidc.starter.ClaimSetAuthoritiesConverter; import com.c4_soft.springaddons.security.oidc.starter.ConfigurableClaimSetAuthoritiesConverter; @@ -29,6 +35,9 @@ import com.c4_soft.springaddons.security.oidc.starter.synchronised.ServletConfigurationSupport; import com.c4_soft.springaddons.security.oidc.starter.synchronised.SpringAddonsOidcBeans; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; /** @@ -111,12 +120,14 @@ SecurityFilterChain springAddonsClientFilterChain( http.securityMatcher(addonsProperties.getClient().getSecurityMatchers()); http.oauth2Login(login -> { - login.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.authorizationRequestResolver(authorizationRequestResolver)); - addonsProperties.getClient().getLoginPath().ifPresent(loginPath -> { - login.loginPage(UriComponentsBuilder.fromUri(addonsProperties.getClient().getClientUri()).path(loginPath).build().toString()); - }); + login.authorizationEndpoint(authorizationEndpoint -> { + authorizationEndpoint.authorizationRedirectStrategy(new C4Oauth2RedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPreAuthorizationCode())); + authorizationEndpoint.authorizationRequestResolver(authorizationRequestResolver); + }); + addonsProperties.getClient().getLoginPath().ifPresent(login::loginPage); addonsProperties.getClient().getPostLoginRedirectUri().ifPresent(postLoginRedirectUri -> { - login.defaultSuccessUrl(postLoginRedirectUri.toString(), true); + login.successHandler(new C4Oauth2AuthenticationSuccessHandler(addonsProperties, postLoginRedirectUri)); + login.failureHandler(new C4Oauth2AuthenticationFailureHandler(addonsProperties, postLoginRedirectUri)); }); }); @@ -168,12 +179,16 @@ LogoutRequestUriBuilder logoutRequestUriBuilder(SpringAddonsOidcProperties addon * * @param logoutRequestUriBuilder delegate doing the smart job * @param clientRegistrationRepository + * @param addonsProperties * @return {@link SpringAddonsLogoutSuccessHandler} */ @ConditionalOnMissingBean @Bean - LogoutSuccessHandler logoutSuccessHandler(LogoutRequestUriBuilder logoutRequestUriBuilder, ClientRegistrationRepository clientRegistrationRepository) { - return new SpringAddonsLogoutSuccessHandler(logoutRequestUriBuilder, clientRegistrationRepository); + LogoutSuccessHandler logoutSuccessHandler( + LogoutRequestUriBuilder logoutRequestUriBuilder, + ClientRegistrationRepository clientRegistrationRepository, + SpringAddonsOidcProperties addonsProperties) { + return new SpringAddonsLogoutSuccessHandler(logoutRequestUriBuilder, clientRegistrationRepository, addonsProperties); } /** @@ -194,4 +209,39 @@ ClientExpressionInterceptUrlRegistryPostProcessor clientAuthorizePostProcessor() ClientHttpSecurityPostProcessor clientHttpPostProcessor() { return http -> http; } + + static class C4Oauth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler { + private final String redirectUri; + private final C4Oauth2RedirectStrategy redirectStrategy; + + public C4Oauth2AuthenticationSuccessHandler(SpringAddonsOidcProperties addonsProperties, URI redirectUri) { + this.redirectUri = redirectUri.toString(); + this.redirectStrategy = new C4Oauth2RedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode()); + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, + ServletException { + redirectStrategy.sendRedirect(request, response, redirectUri); + + } + } + + static class C4Oauth2AuthenticationFailureHandler implements AuthenticationFailureHandler { + private final String redirectUri; + private final C4Oauth2RedirectStrategy redirectStrategy; + + public C4Oauth2AuthenticationFailureHandler(SpringAddonsOidcProperties addonsProperties, URI redirectUri) { + this.redirectUri = redirectUri.toString(); + this.redirectStrategy = new C4Oauth2RedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode()); + } + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) + throws IOException, + ServletException { + redirectStrategy.sendRedirect(request, response, redirectUri); + } + } } \ No newline at end of file