Skip to content

Commit

Permalink
gh-155 Configurable HTTP status in OAuth2 flows responses
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Nov 22, 2023
1 parent 8cc7974 commit d2a3109
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.springframework.security.config.Customizer.withDefaults;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.stream.Stream;

Expand All @@ -14,26 +16,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
Expand All @@ -42,50 +52,59 @@ public class SecurityConf {

/**
* <p>
* 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.
* </p>
* <p>
* It is defined with low order (high precedence) and security-matcher to limit the resources it applies to.
* </p>
*
*
* @param http
* @param clientRegistrationRepository
* @param securityMatchers
* @param permitAll
* @return
* @throws URISyntaxException
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
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)
Expand All @@ -102,6 +121,9 @@ SecurityWebFilterChain clientFilterCHain(
return http.build();
}

/**
* @return second half of CSRF handling for SPAs
*/
@Bean
WebFilter csrfCookieWebFilter() {
return (exchange, chain) -> {
Expand All @@ -115,10 +137,10 @@ WebFilter csrfCookieWebFilter() {
* Security filter-chain for resources for which sessions are not needed.
* </p>
* <p>
* 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.
* </p>
*
*
* @param http
* @param serverProperties
* @param permitAll
Expand Down Expand Up @@ -166,23 +188,86 @@ SecurityWebFilterChain resourceServerFilterCHain(
return http.build();
}

static class AngularLogoutSucessHandler implements ServerLogoutSuccessHandler {
@RequiredArgsConstructor
static class C4OAuth2ServerRedirectStrategy implements ServerRedirectStrategy {
private final HttpStatus status;

@Override
public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) {
return Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
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<Void> 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<Void> 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<Void> 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> 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<String> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]}
Original file line number Diff line number Diff line change
Expand Up @@ -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
# gateway-uri: ${scheme}://10.0.2.2:${server.port}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
issuer: https://oidc.c4-soft.com/auth/realms/master

server:
port: 7084
ssl:
Expand All @@ -13,7 +15,7 @@ spring:
oauth2:
resourceserver:
jwt:
issuer-uri: https://oidc.c4-soft.com/auth/realms/master
issuer-uri: ${issuer}

logging:
level:
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
return delegate.onLogoutSuccess(exchange, authentication).then(Mono.fromRunnable(() -> {
exchange.getExchange().getResponse().setStatusCode(HttpStatus.ACCEPTED);
}));
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- /
Expand 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
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}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
issuer: https://oidc.c4-soft.com/auth/realms/master

server:
port: 7084
ssl:
Expand All @@ -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
Expand Down
Loading

0 comments on commit d2a3109

Please sign in to comment.