Skip to content

Commit

Permalink
Fix post login/logout headers & params detection in reactive apps
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Jan 18, 2024
1 parent c8cbb3d commit 7d2fa93
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 195 deletions.
Original file line number Diff line number Diff line change
@@ -1,50 +1,22 @@
package com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean;

import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Conditional;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;

public class DefaultAuthenticationFailureHandlerCondition extends AllNestedConditions {
public class DefaultAuthenticationFailureHandlerCondition extends NoneNestedConditions {

public DefaultAuthenticationFailureHandlerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
public DefaultAuthenticationFailureHandlerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@Conditional(NoAuthenticationFailureHandlerCondition.class)
static class AuthenticationFailureHandlerMissingCondition {}
@ConditionalOnBean(AuthenticationFailureHandler.class)
static class AuthenticationFailureHandlerProvidedCondition {
}

@Conditional(PostLoginRedirectUriCondition.class)
static class PostLoginRedirectUriProvidedCondition {}

static class NoAuthenticationFailureHandlerCondition extends NoneNestedConditions {

public NoAuthenticationFailureHandlerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnBean(AuthenticationFailureHandler.class)
static class AuthenticationFailureHandlerProvidedCondition {}

@ConditionalOnBean(ServerAuthenticationFailureHandler.class)
static class ServerAuthenticationFailureHandlerProvidedCondition {}
}

static class PostLoginRedirectUriCondition extends AnyNestedCondition {

public PostLoginRedirectUriCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnProperty(name = "com.c4-soft.springaddons.oidc.client.post-login-redirect-host")
static class PostLoginRedirectHostCondition {}

@ConditionalOnProperty(name = "com.c4-soft.springaddons.oidc.client.post-login-redirect-path")
static class PostLoginRedirectPathCondition {}
}
@ConditionalOnBean(ServerAuthenticationFailureHandler.class)
static class ServerAuthenticationFailureHandlerProvidedCondition {
}

}
Original file line number Diff line number Diff line change
@@ -1,49 +1,21 @@
package com.c4_soft.springaddons.security.oidc.starter.properties.condition.bean;

import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Conditional;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;

public class DefaultAuthenticationSuccessHandlerCondition extends AllNestedConditions {
public class DefaultAuthenticationSuccessHandlerCondition extends NoneNestedConditions {

public DefaultAuthenticationSuccessHandlerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
public DefaultAuthenticationSuccessHandlerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@Conditional(NoAuthenticationSuccessHandlerCondition.class)
static class AuthenticationSuccessHandlerMissingCondition {}
@ConditionalOnBean(AuthenticationSuccessHandler.class)
static class AuthenticationSuccessHandlerProvidedCondition {
}

@Conditional(PostLoginRedirectUriCondition.class)
static class PostLoginRedirectUriProvidedCondition {}

static class NoAuthenticationSuccessHandlerCondition extends NoneNestedConditions {

public NoAuthenticationSuccessHandlerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnBean(AuthenticationSuccessHandler.class)
static class AuthenticationSuccessHandlerProvidedCondition {}

@ConditionalOnBean(ServerAuthenticationSuccessHandler.class)
static class ServerAuthenticationSuccessHandlerProvidedCondition {}
}

static class PostLoginRedirectUriCondition extends AnyNestedCondition {

public PostLoginRedirectUriCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnProperty(name = "com.c4-soft.springaddons.oidc.client.post-login-redirect-host")
static class PostLoginRedirectHostCondition {}

@ConditionalOnProperty(name = "com.c4-soft.springaddons.oidc.client.post-login-redirect-path")
static class PostLoginRedirectPathCondition {}
}
@ConditionalOnBean(ServerAuthenticationSuccessHandler.class)
static class ServerAuthenticationSuccessHandlerProvidedCondition {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,134 +27,118 @@
/**
* Serves three purposes:
* <ul>
* <li>Use the {@link SpringAddonsOidcClientProperties#clientUri SpringAddonsOidcClientProperties#client-uri} to set the base URI of authorization-code callback
* (of interest for instance when using an ingress or another gateway in front of the OAuth2 client with oauth2Login)</li>
* <li>Use the {@link SpringAddonsOidcClientProperties#clientUri SpringAddonsOidcClientProperties#client-uri} to set the base URI of
* authorization-code callback (of interest for instance when using an ingress or another gateway in front of the OAuth2 client with
* oauth2Login)</li>
* <li>Add the query params taken from authorization-request-params in application properties</li>
* <li>Save in session post-login URIs provided as header ({@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_HEADER} and
* {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_HEADER}) or request param
* <li>Save in session post-login URIs provided as header ({@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_HEADER}
* and {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_HEADER}) or request param
* ({@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_PARAM} and
* {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_PARAM}). If both are provided, header wins. The key used in session are
* {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE} and
* {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_PARAM}). If both are provided, header wins. The key used in
* session are {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE} and
* {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE}.</li>
* </ul>
* The post-login URIs are used by the default {@link ServerAuthenticationSuccessHandler} and {@link ServerAuthenticationFailureHandler}
*
* @author Jerome Wacongne ch4mp&#64;c4-soft.com
* @see SpringAddonsOidcClientProperties for header and request parameter constants definitions
* @see SpringAddonsOauth2ServerAuthenticationSuccessHandler
* @see SpringAddonsOauth2ServerAuthenticationFailureHandler
* @see SpringAddonsOidcClientProperties for header and request parameter constants definitions
* @see SpringAddonsOauth2ServerAuthenticationSuccessHandler
* @see SpringAddonsOauth2ServerAuthenticationFailureHandler
*/
public class SpringAddonsServerOAuth2AuthorizationRequestResolver extends DefaultServerOAuth2AuthorizationRequestResolver {

private static final Pattern authorizationRequestPattern = Pattern.compile("\\/oauth2\\/authorization\\/([^\\/]+)");
private static final Consumer<OAuth2AuthorizationRequest.Builder> noOpCustomizer = builder -> {};

private final URI clientUri;
private final Map<String, Consumer<OAuth2AuthorizationRequest.Builder>> authRequestCustomizers;

public SpringAddonsServerOAuth2AuthorizationRequestResolver(
ReactiveClientRegistrationRepository clientRegistrationRepository,
SpringAddonsOidcClientProperties addonsClientProperties) {
super(clientRegistrationRepository);
this.clientUri = addonsClientProperties.getClientUri();
authRequestCustomizers = addonsClientProperties.getAuthorizationRequestParams().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> {
final var params = addonsClientProperties.getAuthorizationRequestParams().get(e.getKey());
return e.getValue() == null ? null : requestParamAuthorizationRequestCustomizer(params);
}));
}

private Mono<WebSession> savePostLoginUrisInSession(ServerWebExchange exchange) {
return exchange.getSession().map(session -> {
Optional
.ofNullable(
Optional
.ofNullable(exchange.getRequest().getHeaders().getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_HEADER))
.orElse(
Optional
.ofNullable(
exchange.getRequest().getQueryParams().getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_PARAM))
.orElse(null)))
.filter(StringUtils::hasText)
.map(URI::create)
.ifPresent(postLoginSuccessUri -> {
session.getAttributes().put(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE, postLoginSuccessUri);
});

Optional
.ofNullable(
Optional
.ofNullable(exchange.getRequest().getHeaders().getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_HEADER))
.orElse(
Optional
.ofNullable(
exchange.getRequest().getQueryParams().getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_PARAM))
.orElse(null)))
.filter(StringUtils::hasText)
.map(URI::create)
.ifPresent(postLoginFailureUri -> {
session.getAttributes().put(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE, postLoginFailureUri);
});

return session;
});
}

private OAuth2AuthorizationRequest postProcess(OAuth2AuthorizationRequest request) {
final var modified = OAuth2AuthorizationRequest.from(request);

final var original = URI.create(request.getRedirectUri());
final var redirectUri = UriComponentsBuilder
.fromUri(clientUri)
.path(original.getPath())
.query(original.getQuery())
.fragment(original.getFragment())
.build()
.toString();
modified.redirectUri(redirectUri);

return modified.build();
}

@Override
public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange) {
setAuthorizationRequestCustomizer(authRequestCustomizer(exchange));
return savePostLoginUrisInSession(exchange).then(super.resolve(exchange).map(this::postProcess));
}

@Override
public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange, String clientRegistrationId) {
setAuthorizationRequestCustomizer(authRequestCustomizer(clientRegistrationId));
return savePostLoginUrisInSession(exchange).then(super.resolve(exchange, clientRegistrationId).map(this::postProcess));
}

Consumer<OAuth2AuthorizationRequest.Builder> authRequestCustomizer(ServerWebExchange exchange) {
return authRequestCustomizer(resolveRegistrationId(exchange));
}

Consumer<OAuth2AuthorizationRequest.Builder> authRequestCustomizer(String registrationId) {
if (registrationId == null) {
return noOpCustomizer;
}
return authRequestCustomizers.getOrDefault(registrationId, noOpCustomizer);
}

static String resolveRegistrationId(ServerWebExchange exchange) {
final var requestPath = Optional.ofNullable(exchange.getRequest()).map(ServerHttpRequest::getPath).map(RequestPath::toString).orElse("");
return resolveRegistrationId(requestPath);
}

static String resolveRegistrationId(String requestPath) {
final var matcher = authorizationRequestPattern.matcher(requestPath);
return matcher.matches() ? matcher.group(1) : null;
}

private static Consumer<OAuth2AuthorizationRequest.Builder> requestParamAuthorizationRequestCustomizer(RequestParam[] additionalParams) {
return customizer -> customizer.authorizationRequestUri(authorizationRequestUri -> {
for (var reqParam : additionalParams) {
authorizationRequestUri.queryParam(reqParam.getName(), reqParam.getValue());
}
return authorizationRequestUri.build();
});
}
private static final Pattern authorizationRequestPattern = Pattern.compile("\\/oauth2\\/authorization\\/([^\\/]+)");
private static final Consumer<OAuth2AuthorizationRequest.Builder> noOpCustomizer = builder -> {
};

private final URI clientUri;
private final Map<String, Consumer<OAuth2AuthorizationRequest.Builder>> authRequestCustomizers;

public SpringAddonsServerOAuth2AuthorizationRequestResolver(
ReactiveClientRegistrationRepository clientRegistrationRepository,
SpringAddonsOidcClientProperties addonsClientProperties) {
super(clientRegistrationRepository);
this.clientUri = addonsClientProperties.getClientUri();
authRequestCustomizers = addonsClientProperties.getAuthorizationRequestParams().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> {
final var params = addonsClientProperties.getAuthorizationRequestParams().get(e.getKey());
return e.getValue() == null ? null : requestParamAuthorizationRequestCustomizer(params);
}));
}

private Mono<WebSession> savePostLoginUrisInSession(ServerWebExchange exchange) {
final var request = exchange.getRequest();
final var headers = request.getHeaders();
final var params = request.getQueryParams();
return exchange.getSession().map(session -> {
Optional.ofNullable(
Optional.ofNullable(headers.getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_HEADER))
.orElse(Optional.ofNullable(params.getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_PARAM)).orElse(null)))
.filter(StringUtils::hasText).map(URI::create).ifPresent(postLoginSuccessUri -> {
session.getAttributes().put(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE, postLoginSuccessUri);
});

Optional.ofNullable(
Optional.ofNullable(headers.getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_HEADER))
.orElse(Optional.ofNullable(params.getFirst(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_PARAM)).orElse(null)))
.filter(StringUtils::hasText).map(URI::create).ifPresent(postLoginFailureUri -> {
session.getAttributes().put(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE, postLoginFailureUri);
});

return session;
});
}

private OAuth2AuthorizationRequest postProcess(OAuth2AuthorizationRequest request) {
final var modified = OAuth2AuthorizationRequest.from(request);

final var original = URI.create(request.getRedirectUri());
final var redirectUri =
UriComponentsBuilder.fromUri(clientUri).path(original.getPath()).query(original.getQuery()).fragment(original.getFragment()).build().toString();
modified.redirectUri(redirectUri);

return modified.build();
}

@Override
public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange) {
setAuthorizationRequestCustomizer(authRequestCustomizer(exchange));
return savePostLoginUrisInSession(exchange).then(super.resolve(exchange).map(this::postProcess));
}

@Override
public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange, String clientRegistrationId) {
setAuthorizationRequestCustomizer(authRequestCustomizer(clientRegistrationId));
return savePostLoginUrisInSession(exchange).then(super.resolve(exchange, clientRegistrationId).map(this::postProcess));
}

Consumer<OAuth2AuthorizationRequest.Builder> authRequestCustomizer(ServerWebExchange exchange) {
return authRequestCustomizer(resolveRegistrationId(exchange));
}

Consumer<OAuth2AuthorizationRequest.Builder> authRequestCustomizer(String registrationId) {
if (registrationId == null) {
return noOpCustomizer;
}
return authRequestCustomizers.getOrDefault(registrationId, noOpCustomizer);
}

static String resolveRegistrationId(ServerWebExchange exchange) {
final var requestPath = Optional.ofNullable(exchange.getRequest()).map(ServerHttpRequest::getPath).map(RequestPath::toString).orElse("");
return resolveRegistrationId(requestPath);
}

static String resolveRegistrationId(String requestPath) {
final var matcher = authorizationRequestPattern.matcher(requestPath);
return matcher.matches() ? matcher.group(1) : null;
}

private static Consumer<OAuth2AuthorizationRequest.Builder> requestParamAuthorizationRequestCustomizer(RequestParam[] additionalParams) {
return customizer -> customizer.authorizationRequestUri(authorizationRequestUri -> {
for (var reqParam : additionalParams) {
authorizationRequestUri.queryParam(reqParam.getName(), reqParam.getValue());
}
return authorizationRequestUri.build();
});
}

}

0 comments on commit 7d2fa93

Please sign in to comment.