Add Factory Authority When Authentication Succeeds

Issue gh-17933
This commit is contained in:
Josh Cummings 2025-09-19 09:26:41 -06:00
parent 9eaadcc70d
commit e8accd0499
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
10 changed files with 102 additions and 14 deletions

View File

@ -17,14 +17,18 @@
package org.springframework.security.config.annotation.web.configurers;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
@ -37,6 +41,7 @@ import org.springframework.security.web.authentication.preauth.x509.SubjectDnX50
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
/**
* Adds X509 based pre authentication to an application. Since validating the certificate
@ -177,8 +182,12 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
public void init(H http) {
PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));
http.authenticationProvider(authenticationProvider)
http.authenticationProvider(new AuthorityGrantingAuthenticationProvider(authenticationProvider))
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptions != null) {
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE);
}
}
@Override
@ -225,4 +234,31 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
return context.getBeanProvider(type).getIfUnique();
}
private static final class AuthorityGrantingAuthenticationProvider implements AuthenticationProvider {
private final AuthenticationProvider delegate;
private AuthorityGrantingAuthenticationProvider(AuthenticationProvider delegate) {
this.delegate = delegate;
}
@Override
public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication result = this.delegate.authenticate(authentication);
if (result == null) {
return result;
}
return result
.toBuilder()
.authorities((a) -> a.add(new SimpleGrantedAuthority("FACTOR_X509")))
.build();
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
}

View File

@ -34,6 +34,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
@ -94,6 +95,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private static final String AUTHORITY = "FACTOR_PASSWORD";
/**
* Allows subclasses to perform any additional checks of a returned (or cached)
* <code>UserDetails</code> for a given authentication request. Generally a subclass
@ -197,8 +200,12 @@ public abstract class AbstractUserDetailsAuthenticationProvider
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken
.authenticated(principal, authentication.getCredentials(),
this.authoritiesMapper.mapAuthorities(user.getAuthorities()))
.toBuilder()
.authorities((a) -> a.add(new SimpleGrantedAuthority(AUTHORITY)))
.build();
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;

View File

@ -16,10 +16,15 @@
package org.springframework.security.authentication.ott;
import java.util.Collection;
import java.util.HashSet;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@ -35,6 +40,8 @@ import org.springframework.util.Assert;
*/
public final class OneTimeTokenAuthenticationProvider implements AuthenticationProvider {
private static final String AUTHORITY = "FACTOR_OTT";
private final OneTimeTokenService oneTimeTokenService;
private final UserDetailsService userDetailsService;
@ -56,7 +63,9 @@ public final class OneTimeTokenAuthenticationProvider implements AuthenticationP
}
try {
UserDetails user = this.userDetailsService.loadUserByUsername(consumed.getUsername());
OneTimeTokenAuthentication authenticated = new OneTimeTokenAuthentication(user, user.getAuthorities());
Collection<GrantedAuthority> authorities = new HashSet<>(user.getAuthorities());
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
OneTimeTokenAuthentication authenticated = new OneTimeTokenAuthentication(user, authorities);
authenticated.setDetails(otpAuthenticationToken.getDetails());
return authenticated;
}

View File

@ -33,6 +33,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
@ -50,6 +51,8 @@ import org.springframework.util.StringUtils;
*/
public abstract class AbstractLdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
private static final String AUTHORITY = "FACTOR_PASSWORD";
protected final Log logger = LogFactory.getLog(getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
@ -100,8 +103,11 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati
UserDetails user) {
Object password = this.useAuthenticationRequestCredentials ? authentication.getCredentials()
: user.getPassword();
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(user, password,
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken
.authenticated(user, password, this.authoritiesMapper.mapAuthorities(user.getAuthorities()))
.toBuilder()
.authorities((a) -> a.add(new SimpleGrantedAuthority(AUTHORITY)))
.build();
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;

View File

@ -17,12 +17,14 @@
package org.springframework.security.oauth2.client.authentication;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
@ -66,6 +68,8 @@ import org.springframework.util.Assert;
*/
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
private static final String AUTHORITY = "FACTOR_AUTHORIZATION_CODE";
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
@ -118,8 +122,9 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
Collection<GrantedAuthority> authorities = new HashSet<>(oauth2User.getAuthorities());
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper.mapAuthorities(authorities);
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());

View File

@ -17,10 +17,12 @@
package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Collection;
import java.util.HashSet;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.util.Assert;
@ -34,14 +36,16 @@ import org.springframework.util.Assert;
*/
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String AUTHORITY = "FACTOR_BEARER";
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private String principalClaimName = JwtClaimNames.SUB;
@Override
public final AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt);
Collection<GrantedAuthority> authorities = new HashSet<>(this.jwtGrantedAuthoritiesConverter.convert(jwt));
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.resource.authentication;
import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -28,6 +29,7 @@ import org.springframework.security.authentication.AuthenticationServiceExceptio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
@ -72,6 +74,8 @@ import org.springframework.util.Assert;
*/
public final class OpaqueTokenAuthenticationProvider implements AuthenticationProvider {
private static final String AUTHORITY = "FACTOR_BEARER";
private final Log logger = LogFactory.getLog(getClass());
private final OpaqueTokenIntrospector introspector;
@ -149,8 +153,9 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
Instant exp = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken,
iat, exp);
return new BearerTokenAuthentication(authenticatedPrincipal, accessToken,
authenticatedPrincipal.getAuthorities());
Collection<GrantedAuthority> authorities = new HashSet<>(authenticatedPrincipal.getAuthorities());
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
return new BearerTokenAuthentication(authenticatedPrincipal, accessToken, authorities);
}
/**

View File

@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@ -59,6 +60,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
@ -111,6 +113,8 @@ import org.springframework.util.StringUtils;
*/
public final class OpenSaml5AuthenticationProvider implements AuthenticationProvider {
private static final String AUTHORITY = "FACTOR_SAML_RESPONSE";
private final BaseOpenSamlAuthenticationProvider delegate;
/**
@ -899,7 +903,9 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
.attributes(BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion))
.build();
Saml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, accessor);
Collection<GrantedAuthority> authorities = this.grantedAuthoritiesConverter.convert(assertion);
Collection<GrantedAuthority> authorities = new HashSet<>(
this.grantedAuthoritiesConverter.convert(assertion));
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
return new Saml2AssertionAuthentication(principal, accessor, authorities, registrationId);
}

View File

@ -67,6 +67,7 @@ public class SecurityMockWithAuthoritiesMvcResultMatchersTests {
List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_SELLER"));
grantedAuthorities.add(new SimpleGrantedAuthority("FACTOR_PASSWORD"));
this.mockMvc.perform(formLogin()).andExpect(authenticated().withAuthorities(grantedAuthorities));
}

View File

@ -16,10 +16,15 @@
package org.springframework.security.web.webauthn.authentication;
import java.util.Collection;
import java.util.HashSet;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
@ -39,6 +44,8 @@ import org.springframework.util.Assert;
*/
public class WebAuthnAuthenticationProvider implements AuthenticationProvider {
private static final String AUTHORITY = "FACTOR_WEBAUTHN";
private final WebAuthnRelyingPartyOperations relyingPartyOperations;
private final UserDetailsService userDetailsService;
@ -65,7 +72,9 @@ public class WebAuthnAuthenticationProvider implements AuthenticationProvider {
.authenticate(webAuthnRequest.getWebAuthnRequest());
String username = userEntity.getName();
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
return new WebAuthnAuthentication(userEntity, userDetails.getAuthorities());
Collection<GrantedAuthority> authorities = new HashSet<>(userDetails.getAuthorities());
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
return new WebAuthnAuthentication(userEntity, authorities);
}
catch (RuntimeException ex) {
throw new BadCredentialsException(ex.getMessage(), ex);