Skip to content

Commit

Permalink
WiP statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Aug 10, 2024
1 parent 29eb118 commit 6389660
Show file tree
Hide file tree
Showing 13 changed files with 829 additions and 606 deletions.

Large diffs are not rendered by default.

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

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;

public class DefaultAuthenticationEntryPointCondition extends NoneNestedConditions {

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

@ConditionalOnBean(AuthenticationEntryPoint.class)
static class AuthenticationEntryPointCondition {
}

@ConditionalOnBean(ServerAuthenticationEntryPoint.class)
static class ServerAuthenticationEntryPointCondition {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;

public class DefaultAuthenticationSuccessHandlerCondition extends NoneNestedConditions {

Expand All @@ -14,7 +15,7 @@ public DefaultAuthenticationSuccessHandlerCondition() {
static class AuthenticationSuccessHandlerProvidedCondition {
}

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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.net.URI;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
Expand All @@ -11,6 +13,8 @@
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
Expand All @@ -21,23 +25,36 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsServerOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
@Slf4j
public class SpringAddonsOauth2ServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
private final URI defaultRedirectUri;
private final SpringAddonsOauth2ServerRedirectStrategy redirectStrategy;
private final HttpStatus postAuthorizationFailureStatus;

public SpringAddonsOauth2ServerAuthenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) {
this.defaultRedirectUri = addonsProperties.getClient().getLoginErrorRedirectPath().orElse(URI.create("/"));
this.redirectStrategy = new SpringAddonsOauth2ServerRedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode());
this.postAuthorizationFailureStatus = addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationFailure();
}

@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
return webFilterExchange.getExchange().getSession().flatMap(session -> {
final var uri = UriComponentsBuilder.fromUri(
final var location = UriComponentsBuilder.fromUri(
session.getAttributeOrDefault(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE, defaultRedirectUri))
.queryParam(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, HtmlUtils.htmlEscape(exception.getMessage()))
.build().toUri();
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), uri);
.build().toUri().toString();

final var response = webFilterExchange.getExchange().getResponse();
response.setStatusCode(postAuthorizationFailureStatus);
response.getHeaders().add(HttpHeaders.LOCATION, location);
response.getHeaders().add(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, exception.getMessage());

log.debug("Login failure. Status: {}, location: {}, message: {}", postAuthorizationFailureStatus, location, exception.getMessage());

if (postAuthorizationFailureStatus.is4xxClientError() || postAuthorizationFailureStatus.is5xxServerError()) {
final var buffer = response.bufferFactory().wrap(exception.getMessage().getBytes());
return response.writeWith(Flux.just(buffer));
}
return response.setComplete();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

/**
Expand All @@ -19,6 +20,7 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsServerOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
@Slf4j
public class SpringAddonsOauth2ServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private final URI defaultRedirectUri;
private final SpringAddonsOauth2ServerRedirectStrategy redirectStrategy;
Expand All @@ -33,6 +35,8 @@ public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, A
return webFilterExchange.getExchange().getSession().flatMap(session -> {
final var uri =
session.getAttributeOrDefault(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE, defaultRedirectUri);

log.debug("Login success. Status: {}, location: {}", redirectStrategy.getDefaultStatus(), uri.toString());
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), uri);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,43 +13,40 @@

import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;

import lombok.Getter;
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.
* 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&#64;c4-soft.com
*/
@RequiredArgsConstructor
public class SpringAddonsOauth2ServerRedirectStrategy implements ServerRedirectStrategy {

private final HttpStatus defaultStatus;

@Override
public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) {
return Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
final var status = Optional
.ofNullable(exchange.getRequest().getHeaders().get(SpringAddonsOidcClientProperties.RESPONSE_STATUS_HEADER))
.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);
response.setStatusCode(status);

response.getHeaders().setLocation(location);
});
}
@Getter
private final HttpStatus defaultStatus;

@Override
public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) {
return Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
final var status = Optional.ofNullable(exchange.getRequest().getHeaders().get(SpringAddonsOidcClientProperties.RESPONSE_STATUS_HEADER))
.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);
response.setStatusCode(status);

response.getHeaders().setLocation(location);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.c4_soft.springaddons.security.oidc.starter.reactive.client;

import org.springframework.http.HttpHeaders;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;

import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Slf4j
public class SpringAddonsServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
private final SpringAddonsOidcClientProperties clientProperties;

public SpringAddonsServerAuthenticationEntryPoint(SpringAddonsOidcClientProperties addonsProperties) {
this.clientProperties = addonsProperties;
}

@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
final var location = clientProperties.getLoginUri().orElse(
UriComponentsBuilder.fromUri(clientProperties.getClientUri()).pathSegment(clientProperties.getClientUri().getPath(), "/login").build().toUri())
.toString();
log.debug("Status: {}, location: {}", clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().value(), location);

final var response = exchange.getResponse();
response.setStatusCode(clientProperties.getOauth2Redirections().getAuthenticationEntryPoint());
response.getHeaders().add(HttpHeaders.LOCATION, location.toString());

if (clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().is4xxClientError()
|| clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().is5xxServerError()) {
final var buffer = response.bufferFactory().wrap("Unauthorized. Please authenticate at %s".formatted(location.toString()).getBytes());
return response.writeWith(Flux.just(buffer));
}

return response.setComplete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.c4_soft.springaddons.security.oidc.starter.synchronised.client;

import java.io.IOException;

import org.springframework.http.HttpHeaders;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.web.util.UriComponentsBuilder;

import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SpringAddonsAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final SpringAddonsOidcClientProperties clientProperties;

public SpringAddonsAuthenticationEntryPoint(SpringAddonsOidcClientProperties addonsProperties) {
this.clientProperties = addonsProperties;
}

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
final var location = clientProperties.getLoginUri().orElse(
UriComponentsBuilder.fromUri(clientProperties.getClientUri()).pathSegment(clientProperties.getClientUri().getPath(), "/login").build().toUri())
.toString();
log.debug("Status: {}, location: {}", clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().value(), location);

response.setStatus(clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().value());
response.setHeader(HttpHeaders.LOCATION, location.toString());
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "OAuth realm=\"Restricted Content\"");

if (clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().is4xxClientError()
|| clientProperties.getOauth2Redirections().getAuthenticationEntryPoint().is5xxServerError()) {
response.getOutputStream().write("Unauthorized. Please authenticate at %s".formatted(location.toString()).getBytes());
}

response.flushBuffer();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.net.URI;
import java.util.Optional;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.util.HtmlUtils;
Expand All @@ -15,6 +17,7 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

/**
* An authentication failure handler reading post-login failure URI in session (set by the frontend with a header or request param when
Expand All @@ -24,24 +27,36 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
@Slf4j
public class SpringAddonsOauth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String redirectUri;
private final SpringAddonsOauth2RedirectStrategy redirectStrategy;
private final HttpStatus postAuthorizationFailureStatus;

public SpringAddonsOauth2AuthenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) {
this.redirectUri = addonsProperties.getClient().getLoginErrorRedirectPath().map(URI::toString).orElse("/");
this.redirectStrategy = new SpringAddonsOauth2RedirectStrategy(addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationCode());
this.postAuthorizationFailureStatus = addonsProperties.getClient().getOauth2Redirections().getPostAuthorizationFailure();
}

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException,
ServletException {
final var uri = UriComponentsBuilder.fromUriString(
final var location = UriComponentsBuilder.fromUriString(
Optional.ofNullable(request.getSession().getAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE))
.map(Object::toString).orElse(redirectUri))
.queryParam(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, HtmlUtils.htmlEscape(exception.getMessage())).build()
.toUri();
redirectStrategy.sendRedirect(request, response, uri.toString());
.toUri().toString();

log.debug("Authentication failure. Status: {}, location: {}, message: {}", postAuthorizationFailureStatus.value(), location, exception.getMessage());

response.setStatus(postAuthorizationFailureStatus.value());
response.setHeader(HttpHeaders.LOCATION, location);
response.setHeader(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_CAUSE_ATTRIBUTE, exception.getMessage());

if (postAuthorizationFailureStatus.is4xxClientError() || postAuthorizationFailureStatus.is5xxServerError()) {
response.getOutputStream().write(exception.getMessage().getBytes());
}

response.flushBuffer();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

/**
* An authentication success handler reading post-login success URI in session (set by the frontend with a header or request param when
Expand All @@ -22,6 +23,7 @@
* @see SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE for constant used as session attribute keys
* @see SpringAddonsOAuth2AuthorizationRequestResolver which sets the post-login URI session attribute
*/
@Slf4j
public class SpringAddonsOauth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String redirectUri;
private final SpringAddonsOauth2RedirectStrategy redirectStrategy;
Expand All @@ -38,7 +40,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
final var uri =
Optional.ofNullable(request.getSession().getAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE))
.map(Object::toString).orElse(redirectUri);
redirectStrategy.sendRedirect(request, response, uri);

log.debug("Authentication success. Status: {}, location: {}", redirectStrategy.getDefaultStatus(), uri);

redirectStrategy.sendRedirect(request, response, uri);
}
}
Loading

0 comments on commit 6389660

Please sign in to comment.