Skip to content

Commit ced9ad0

Browse files
committed
Provider Factors and Entry Points
1 parent 2b87e3c commit ced9ad0

File tree

12 files changed

+384
-16
lines changed

12 files changed

+384
-16
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,47 @@
1616

1717
package org.springframework.security.config.annotation.web.configurers;
1818

19+
import java.io.IOException;
20+
import java.util.Collection;
1921
import java.util.LinkedHashMap;
22+
import java.util.Map;
23+
import java.util.function.Function;
24+
import java.util.stream.Collectors;
2025

26+
import jakarta.servlet.ServletException;
27+
import jakarta.servlet.http.HttpServletRequest;
28+
import jakarta.servlet.http.HttpServletResponse;
29+
30+
import org.springframework.security.access.AccessDeniedException;
31+
import org.springframework.security.authentication.InsufficientAuthenticationException;
32+
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
33+
import org.springframework.security.authorization.AuthorizationDeniedException;
2134
import org.springframework.security.config.Customizer;
2235
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2336
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
37+
import org.springframework.security.core.Authentication;
38+
import org.springframework.security.core.AuthenticationException;
39+
import org.springframework.security.core.GrantedAuthority;
40+
import org.springframework.security.core.context.SecurityContextHolder;
41+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
42+
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
2443
import org.springframework.security.web.AuthenticationEntryPoint;
44+
import org.springframework.security.web.FormPostRedirectStrategy;
45+
import org.springframework.security.web.RedirectStrategy;
2546
import org.springframework.security.web.access.AccessDeniedHandler;
2647
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
2748
import org.springframework.security.web.access.ExceptionTranslationFilter;
2849
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler;
2950
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
3051
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
52+
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
53+
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
54+
import org.springframework.security.web.csrf.CsrfToken;
3155
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
3256
import org.springframework.security.web.savedrequest.RequestCache;
3357
import org.springframework.security.web.util.matcher.RequestMatcher;
58+
import org.springframework.util.Assert;
59+
import org.springframework.web.util.UriComponentsBuilder;
3460

3561
/**
3662
* Adds exception handling for Spring Security related exceptions to an application. All
@@ -225,13 +251,13 @@ AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
225251

226252
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
227253
if (this.defaultDeniedHandlerMappings.isEmpty()) {
228-
return new AccessDeniedHandlerImpl();
254+
return new AuthenticationFactorDelegatingAccessDeniedHandler();
229255
}
230256
if (this.defaultDeniedHandlerMappings.size() == 1) {
231257
return this.defaultDeniedHandlerMappings.values().iterator().next();
232258
}
233259
return new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings,
234-
new AccessDeniedHandlerImpl());
260+
new AuthenticationFactorDelegatingAccessDeniedHandler());
235261
}
236262

237263
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
@@ -263,4 +289,96 @@ private RequestCache getRequestCache(H http) {
263289
return new HttpSessionRequestCache();
264290
}
265291

292+
private static final class AuthenticationFactorDelegatingAccessDeniedHandler implements AccessDeniedHandler {
293+
294+
private final Map<String, AuthenticationEntryPoint> entryPoints = Map.of("FACTOR_PASSWORD",
295+
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_AUTHORIZATION_CODE",
296+
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_SAML_RESPONSE",
297+
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_WEBAUTHN",
298+
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_BEARER",
299+
new BearerTokenAuthenticationEntryPoint(), "FACTOR_OTT",
300+
new PostAuthenticationEntryPoint(GenerateOneTimeTokenFilter.DEFAULT_GENERATE_URL + "?username={u}",
301+
Map.of("u", Authentication::getName)));
302+
303+
private final AccessDeniedHandler defaults = new AccessDeniedHandlerImpl();
304+
305+
@Override
306+
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
307+
throws IOException, ServletException {
308+
Collection<String> needed = authorizationRequest(ex);
309+
if (needed == null) {
310+
this.defaults.handle(request, response, ex);
311+
return;
312+
}
313+
for (String authority : needed) {
314+
AuthenticationEntryPoint entryPoint = this.entryPoints.get(authority);
315+
if (entryPoint != null) {
316+
AuthenticationException insufficient = new InsufficientAuthenticationException(ex.getMessage(), ex);
317+
entryPoint.commence(request, response, insufficient);
318+
return;
319+
}
320+
}
321+
this.defaults.handle(request, response, ex);
322+
}
323+
324+
private Collection<String> authorizationRequest(AccessDeniedException access) {
325+
if (!(access instanceof AuthorizationDeniedException denied)) {
326+
return null;
327+
}
328+
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision decision)) {
329+
return null;
330+
}
331+
return decision.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
332+
}
333+
334+
}
335+
336+
private static final class PostAuthenticationEntryPoint implements AuthenticationEntryPoint {
337+
338+
private final String entryPointUri;
339+
340+
private final Map<String, Function<Authentication, String>> params;
341+
342+
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
343+
.getContextHolderStrategy();
344+
345+
private RedirectStrategy redirectStrategy = new FormPostRedirectStrategy();
346+
347+
private PostAuthenticationEntryPoint(String entryPointUri,
348+
Map<String, Function<Authentication, String>> params) {
349+
this.entryPointUri = entryPointUri;
350+
this.params = params;
351+
}
352+
353+
@Override
354+
public void commence(HttpServletRequest request, HttpServletResponse response,
355+
AuthenticationException authException) throws IOException, ServletException {
356+
Authentication authentication = getAuthentication(authException);
357+
Assert.notNull(authentication, "could not find authentication in order to perform post");
358+
Map<String, String> params = this.params.entrySet()
359+
.stream()
360+
.collect(Collectors.toMap(Map.Entry::getKey, (entry) -> entry.getValue().apply(authentication)));
361+
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(this.entryPointUri);
362+
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
363+
if (csrf != null) {
364+
builder.queryParam(csrf.getParameterName(), csrf.getToken());
365+
}
366+
String entryPointUrl = builder.build(false).expand(params).toUriString();
367+
this.redirectStrategy.sendRedirect(request, response, entryPointUrl);
368+
}
369+
370+
private Authentication getAuthentication(AuthenticationException authException) {
371+
Authentication authentication = authException.getAuthenticationRequest();
372+
if (authentication != null && authentication.isAuthenticated()) {
373+
return authentication;
374+
}
375+
authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
376+
if (authentication != null && authentication.isAuthenticated()) {
377+
return authentication;
378+
}
379+
return null;
380+
}
381+
382+
}
383+
266384
}

config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@
1717
package org.springframework.security.config.annotation.web.configurers;
1818

1919
import jakarta.servlet.http.HttpServletRequest;
20+
import org.jspecify.annotations.Nullable;
2021

2122
import org.springframework.context.ApplicationContext;
2223
import org.springframework.security.authentication.AuthenticationDetailsSource;
2324
import org.springframework.security.authentication.AuthenticationManager;
25+
import org.springframework.security.authentication.AuthenticationProvider;
2426
import org.springframework.security.config.Customizer;
2527
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2628
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2729
import org.springframework.security.core.Authentication;
30+
import org.springframework.security.core.AuthenticationException;
31+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2832
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
2933
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
3034
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -37,6 +41,7 @@
3741
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3842
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
3943
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
44+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
4045

4146
/**
4247
* Adds X509 based pre authentication to an application. Since validating the certificate
@@ -177,8 +182,12 @@ public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
177182
public void init(H http) {
178183
PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
179184
authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));
180-
http.authenticationProvider(authenticationProvider)
185+
http.authenticationProvider(new AuthorityGrantingAuthenticationProvider(authenticationProvider))
181186
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
187+
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
188+
if (exceptions != null) {
189+
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE);
190+
}
182191
}
183192

184193
@Override
@@ -225,4 +234,31 @@ private <C> C getSharedOrBean(H http, Class<C> type) {
225234
return context.getBeanProvider(type).getIfUnique();
226235
}
227236

237+
private static final class AuthorityGrantingAuthenticationProvider implements AuthenticationProvider {
238+
239+
private final AuthenticationProvider delegate;
240+
241+
private AuthorityGrantingAuthenticationProvider(AuthenticationProvider delegate) {
242+
this.delegate = delegate;
243+
}
244+
245+
@Override
246+
public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException {
247+
Authentication result = this.delegate.authenticate(authentication);
248+
if (result == null) {
249+
return result;
250+
}
251+
return result
252+
.toBuilder()
253+
.authorities((a) -> a.add(new SimpleGrantedAuthority("FACTOR_X509")))
254+
.build();
255+
}
256+
257+
@Override
258+
public boolean supports(Class<?> authentication) {
259+
return true;
260+
}
261+
262+
}
263+
228264
}

0 commit comments

Comments
 (0)