Sfoglia il codice sorgente

Merge branch 'authentication-factors'

Closes gh-17933
Josh Cummings 1 settimana fa
parent
commit
25e413127c
34 ha cambiato i file con 324 aggiunte e 25 eliminazioni
  1. 11 2
      cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java
  2. 17 0
      cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java
  3. 7 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java
  4. 5 2
      config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java
  5. 11 1
      core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java
  6. 4 0
      core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java
  7. 10 1
      core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProvider.java
  8. 4 0
      core/src/test/java/org/springframework/security/authentication/SecurityAssertions.java
  9. 10 0
      core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java
  10. 11 1
      core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java
  11. 13 0
      core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProviderTests.java
  12. 1 1
      docs/modules/ROOT/pages/servlet/authentication/cas.adoc
  13. 5 1
      docs/modules/ROOT/pages/servlet/authentication/jaas.adoc
  14. 1 1
      docs/modules/ROOT/pages/servlet/authentication/passwords/dao-authentication-provider.adoc
  15. 1 1
      docs/modules/ROOT/pages/servlet/authentication/x509.adoc
  16. 5 0
      docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc
  17. 1 1
      docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc
  18. 1 1
      docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc
  19. 1 1
      docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc
  20. 8 1
      ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java
  21. 11 0
      ldap/src/test/java/org/springframework/security/ldap/authentication/LdapAuthenticationProviderTests.java
  22. 9 2
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java
  23. 17 1
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java
  24. 6 2
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java
  25. 7 2
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java
  26. 8 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java
  27. 11 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java
  28. 7 1
      saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java
  29. 8 0
      saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java
  30. 1 0
      test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockWithAuthoritiesMvcResultMatchersTests.java
  31. 19 1
      web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProvider.java
  32. 22 0
      web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProviderTests.java
  33. 10 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationProvider.java
  34. 61 0
      webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationProviderTests.java

+ 11 - 2
cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java

@@ -16,6 +16,9 @@
 
 package org.springframework.security.cas.authentication;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apereo.cas.client.validation.Assertion;
@@ -35,7 +38,9 @@ import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.cas.ServiceProperties;
 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.AuthenticationUserDetailsService;
@@ -64,6 +69,8 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
 
 	private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
 
+	private static final String AUTHORITY = "FACTOR_CAS";
+
 	@SuppressWarnings("NullAway.Init")
 	private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService;
 
@@ -141,8 +148,10 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
 			Assertion assertion = this.ticketValidator.validate(credentials.toString(), getServiceUrl(authentication));
 			UserDetails userDetails = loadUserByAssertion(assertion);
 			this.userDetailsChecker.check(userDetails);
-			return new CasAuthenticationToken(this.key, userDetails, credentials,
-					this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion);
+			Collection<GrantedAuthority> authorities = new ArrayList<>(
+					this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));
+			authorities.add(new SimpleGrantedAuthority(AUTHORITY));
+			return new CasAuthenticationToken(this.key, userDetails, credentials, authorities, userDetails, assertion);
 		}
 		catch (TicketValidationException ex) {
 			throw new BadCredentialsException(ex.getMessage(), ex);

+ 17 - 0
cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java

@@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
 
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.cas.ServiceProperties;
@@ -346,6 +347,22 @@ public class CasAuthenticationProviderTests {
 		assertThat(checkCount.get()).isEqualTo(1);
 	}
 
+	@Test
+	public void authenticateWhenSuccessfulThenIssuesFactor() throws Exception {
+		CasAuthenticationProvider cap = new CasAuthenticationProvider();
+		cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
+		cap.setKey("qwerty");
+		StatelessTicketCache cache = new MockStatelessTicketCache();
+		cap.setStatelessTicketCache(cache);
+		cap.setServiceProperties(makeServiceProperties());
+		cap.setTicketValidator(new MockTicketValidator(true));
+		cap.afterPropertiesSet();
+		CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("ST-123");
+		token.setDetails("details");
+		Authentication result = cap.authenticate(token);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_CAS");
+	}
+
 	private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService {
 
 		@Override

+ 7 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java

@@ -25,6 +25,7 @@ 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.authority.AuthorityUtils;
 import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
 import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
 import org.springframework.security.core.userdetails.UserDetailsService;
@@ -37,6 +38,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 +179,13 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
 	public void init(H http) {
 		PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
 		authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));
+		authenticationProvider.setGrantedAuthoritySupplier(() -> AuthorityUtils.createAuthorityList("FACTOR_X509"));
 		http.authenticationProvider(authenticationProvider)
 			.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
+		ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
+		if (exceptions != null) {
+			exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE);
+		}
 	}
 
 	@Override

+ 5 - 2
config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java

@@ -30,6 +30,7 @@ import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
@@ -322,8 +323,10 @@ public class OAuth2LoginBeanDefinitionParserTests {
 		verify(this.authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
 		Authentication authentication = authenticationCaptor.getValue();
 		assertThat(authentication.getPrincipal()).isInstanceOf(OAuth2User.class);
-		assertThat(authentication.getAuthorities()).hasSize(1);
-		assertThat(authentication.getAuthorities()).first()
+		SecurityAssertions.assertThat(authentication)
+			.roles()
+			.hasSize(1)
+			.first()
 			.isInstanceOf(SimpleGrantedAuthority.class)
 			.hasToString("ROLE_OAUTH2_USER");
 		// re-setup for OIDC test

+ 11 - 1
core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java

@@ -16,6 +16,9 @@
 
 package org.springframework.security.authentication.dao;
 
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -33,7 +36,9 @@ import org.springframework.security.authentication.LockedException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 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.UserCache;
@@ -94,6 +99,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 +204,11 @@ 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
+		Collection<GrantedAuthority> authorities = new LinkedHashSet<>(
+				this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
+		authorities.add(new SimpleGrantedAuthority(AUTHORITY));
 		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
-				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
+				authentication.getCredentials(), authorities);
 		result.setDetails(authentication.getDetails());
 		this.logger.debug("Authenticated user");
 		return result;

+ 4 - 0
core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java

@@ -45,6 +45,7 @@ import org.springframework.security.authentication.jaas.event.JaasAuthentication
 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.context.SecurityContext;
 import org.springframework.security.core.session.SessionDestroyedEvent;
 import org.springframework.util.Assert;
@@ -120,6 +121,8 @@ import org.springframework.util.ObjectUtils;
 public abstract class AbstractJaasAuthenticationProvider implements AuthenticationProvider,
 		ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDestroyedEvent> {
 
+	private static final String AUTHORITY = "FACTOR_PASSWORD";
+
 	private ApplicationEventPublisher applicationEventPublisher = (event) -> {
 	};
 
@@ -210,6 +213,7 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati
 				}
 			}
 		}
+		authorities.add(new SimpleGrantedAuthority(AUTHORITY));
 		return authorities;
 	}
 

+ 10 - 1
core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProvider.java

@@ -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;
 		}

+ 4 - 0
core/src/test/java/org/springframework/security/authentication/SecurityAssertions.java

@@ -75,6 +75,10 @@ public final class SecurityAssertions {
 			return authorities().has(new Condition<>(test, "contains %s", Arrays.toString(authorities)));
 		}
 
+		public CollectionAssert<GrantedAuthority> roles() {
+			return authorities().filteredOn((authority) -> authority.getAuthority().startsWith("ROLE_"));
+		}
+
 		public CollectionAssert<GrantedAuthority> authorities() {
 			return new CollectionAssert<>(this.authentication.getAuthorities());
 		}

+ 10 - 0
core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java

@@ -31,6 +31,7 @@ import org.springframework.security.authentication.CredentialsExpiredException;
 import org.springframework.security.authentication.DisabledException;
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
 import org.springframework.security.authentication.LockedException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.password.CompromisedPasswordChecker;
@@ -504,6 +505,15 @@ public class DaoAuthenticationProviderTests {
 		assertThat(authentication).isNotNull();
 	}
 
+	@Test
+	void authenticateWhenSuccessThenIssuesFactor() {
+		UserDetails user = PasswordEncodedUser.user();
+		DaoAuthenticationProvider provider = new DaoAuthenticationProvider(withUsers(user));
+		Authentication request = new UsernamePasswordAuthenticationToken("user", "password");
+		Authentication result = provider.authenticate(request);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_PASSWORD");
+	}
+
 	private UserDetailsService withUsers(UserDetails... users) {
 		return new InMemoryUserDetailsManager(users);
 	}

+ 11 - 1
core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java

@@ -35,6 +35,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.support.ClassPathXmlApplicationContext;
 import org.springframework.core.io.FileSystemResource;
 import org.springframework.security.authentication.LockedException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -224,7 +225,9 @@ public class JaasAuthenticationProviderTests {
 				"password");
 		assertThat(this.jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
 		Authentication auth = this.jaasProvider.authenticate(token);
-		assertThat(auth.getAuthorities()).withFailMessage("Only ROLE_TEST1 and ROLE_TEST2 should have been returned")
+		SecurityAssertions.assertThat(auth)
+			.roles()
+			.withFailMessage("Only ROLE_TEST1 and ROLE_TEST2 should have been returned")
 			.hasSize(2);
 	}
 
@@ -234,6 +237,13 @@ public class JaasAuthenticationProviderTests {
 			.authenticate(new TestingAuthenticationToken("foo", "bar", AuthorityUtils.NO_AUTHORITIES))).isNull();
 	}
 
+	@Test
+	public void authenticateWhenSuccessThenIssuesFactor() {
+		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password");
+		Authentication result = this.jaasProvider.authenticate(token);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_PASSWORD");
+	}
+
 	private static class MockLoginContext extends LoginContext {
 
 		boolean loggedOut = false;

+ 13 - 0
core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProviderTests.java

@@ -26,6 +26,7 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
@@ -98,6 +99,18 @@ public class OneTimeTokenAuthenticationProviderTests {
 		assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.provider.authenticate(token));
 	}
 
+	@Test
+	void authenticateWhenSuccessThenIssuesFactor() {
+		given(this.oneTimeTokenService.consume(any()))
+			.willReturn(new DefaultOneTimeToken(TOKEN, USERNAME, Instant.now().plusSeconds(120)));
+		given(this.userDetailsService.loadUserByUsername(anyString()))
+			.willReturn(new User(USERNAME, PASSWORD, List.of()));
+		OneTimeTokenAuthenticationToken token = new OneTimeTokenAuthenticationToken(TOKEN);
+
+		Authentication authentication = this.provider.authenticate(token);
+		SecurityAssertions.assertThat(authentication).hasAuthority("FACTOR_OTT");
+	}
+
 	@Test
 	void constructorWhenOneTimeTokenServiceIsNullThenThrowIllegalArgumentException() {
 		// @formatter:off

+ 1 - 1
docs/modules/ROOT/pages/servlet/authentication/cas.adoc

@@ -79,7 +79,7 @@ The `CasProxyDecider` indicates whether the proxy list in the `TicketResponse` i
 Several implementations are provided with Spring Security: `RejectProxyTickets`, `AcceptAnyCasProxy` and `NamedCasProxyDecider`.
 These names are largely self-explanatory, except `NamedCasProxyDecider` which allows a `List` of trusted proxies to be provided.
 * `CasAuthenticationProvider` will next request a `AuthenticationUserDetailsService` to load the `GrantedAuthority` objects that apply to the user contained in the `Assertion`.
-* If there were no problems, `CasAuthenticationProvider` constructs a `CasAuthenticationToken` including the details contained in the `TicketResponse` and the ``GrantedAuthority``s.
+* If there were no problems, `CasAuthenticationProvider` constructs a `CasAuthenticationToken` including the details contained in the `TicketResponse` and a set of ``GrantedAuthority``s that contains at least `FACTOR_BEARER`.
 * Control then returns to `CasAuthenticationFilter`, which places the created `CasAuthenticationToken` in the security context.
 * The user's browser is redirected to the original page that caused the `AuthenticationException` (or a custom destination depending on the configuration).
 

+ 5 - 1
docs/modules/ROOT/pages/servlet/authentication/jaas.adoc

@@ -43,8 +43,12 @@ The `AbstractJaasAuthenticationProvider` obtains the JAAS principals by first su
 A call to `LoginContext.getSubject().getPrincipals()` is made, with each resulting principal passed to each `AuthorityGranter` defined against the `AbstractJaasAuthenticationProvider.setAuthorityGranters(List)` property.
 
 Spring Security does not include any production `AuthorityGranter` instances, given that every JAAS principal has an implementation-specific meaning.
-However, there is a `TestAuthorityGranter` in the unit tests that demonstrates a simple `AuthorityGranter` implementation.
+However, Spring Security does issue the `FACTOR_PASSWORD` authority by default when authentication suceeds.
 
+[TIP]
+====
+There is a `TestAuthorityGranter` in the unit tests that demonstrates a simple `AuthorityGranter` implementation.
+====
 
 [[jaas-defaultjaasauthenticationprovider]]
 == DefaultJaasAuthenticationProvider

+ 1 - 1
docs/modules/ROOT/pages/servlet/authentication/passwords/dao-authentication-provider.adoc

@@ -19,5 +19,5 @@ image:{icondir}/number_3.png[] `DaoAuthenticationProvider` looks up the `UserDet
 
 image:{icondir}/number_4.png[] `DaoAuthenticationProvider` uses the xref:servlet/authentication/passwords/password-encoder.adoc#servlet-authentication-password-storage[`PasswordEncoder`] to validate the password on the `UserDetails` returned in the previous step.
 
-image:{icondir}/number_5.png[] When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `UsernamePasswordAuthenticationToken` and has a principal that is the `UserDetails` returned by the configured `UserDetailsService`.
+image:{icondir}/number_5.png[] When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `UsernamePasswordAuthenticationToken` and has a principal that is the `UserDetails` returned by the configured `UserDetailsService` and a set of authorities containing at least `FACTOR_PASSWORD`.
 Ultimately, the returned `UsernamePasswordAuthenticationToken` is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] by the authentication `Filter`.

+ 1 - 1
docs/modules/ROOT/pages/servlet/authentication/x509.adoc

@@ -12,7 +12,7 @@ For example, if you use Tomcat, you should read the https://tomcat.apache.org/to
 You should get this working before trying it out with Spring Security.
 
 The Spring Security X.509 module extracts the certificate by using a filter.
-It maps the certificate to an application user and loads that user's set of granted authorities for use with the standard Spring Security infrastructure.
+It maps the certificate to an application user and loads that user's set of granted authorities for use with the standard Spring Security infrastructure, specifically including at least the `FACTOR_X509` authority when <<servlet-x509-config, using the `HttpSecurity` DSL>>.
 
 [[servlet-x509-config]]
 == Adding X.509 Authentication to Your Web Application

+ 5 - 0
docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc

@@ -613,6 +613,11 @@ class OAuth2LoginSecurityConfig {
 ----
 ======
 
+[TIP]
+====
+Once authentication completes, it also contains the `FACTOR_AUTHORIZATION_CODE` granted authority.
+====
+
 [[oauth2login-advanced-map-authorities-oauth2userservice]]
 ==== Delegation-based Strategy with OAuth2UserService
 

+ 1 - 1
docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc

@@ -105,7 +105,7 @@ image:{icondir}/number_3.png[] `JwtAuthenticationProvider` decodes, verifies, an
 [[oauth2resourceserver-jwt-architecture-jwtauthenticationconverter]]
 image:{icondir}/number_4.png[] `JwtAuthenticationProvider` then uses the <<oauth2resourceserver-jwt-authorization-extraction,`JwtAuthenticationConverter`>> to convert the `Jwt` into a `Collection` of granted authorities.
 
-image:{icondir}/number_5.png[] When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `JwtAuthenticationToken` and has a principal that is the `Jwt` returned by the configured `JwtDecoder`.
+image:{icondir}/number_5.png[] When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `JwtAuthenticationToken` and has a principal that is the `Jwt` returned by the configured `JwtDecoder` and a set of authorities that contains at least `FACTOR_BEARER`.
 Ultimately, the returned `JwtAuthenticationToken` will be set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] by the authentication `Filter`.
 
 [[oauth2resourceserver-jwt-jwkseturi]]

+ 1 - 1
docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc

@@ -96,7 +96,7 @@ image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xre
 
 [[oauth2resourceserver-opaque-architecture-introspector]]
 image:{icondir}/number_3.png[] `OpaqueTokenAuthenticationProvider` introspects the opaque token and adds granted authorities using an <<oauth2resourceserver-opaque-introspector,`OpaqueTokenIntrospector`>>.
-When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `BearerTokenAuthentication` and has a principal that is the `OAuth2AuthenticatedPrincipal` returned by the configured <<oauth2resourceserver-opaque-introspector,`OpaqueTokenIntrospector`>>.
+When authentication is successful, the xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] that is returned is of type `BearerTokenAuthentication` and has a principal that is the `OAuth2AuthenticatedPrincipal` returned by the configured <<oauth2resourceserver-opaque-introspector,`OpaqueTokenIntrospector`>> and a set of authorities that contains at least `FACTOR_BEARER`.
 Ultimately, the returned `BearerTokenAuthentication` will be set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] by the authentication `Filter`.
 
 [[oauth2resourceserver-opaque-attributes]]

+ 1 - 1
docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc

@@ -222,7 +222,7 @@ image:{icondir}/number_8.png[] Next, the provider validates each assertion's `Ex
 If any validations fail, authentication fails.
 
 image:{icondir}/number_9.png[] Following that, the provider takes the first assertion's `AttributeStatement` and maps it to a `Map<String, List<Object>>`.
-It also grants the `ROLE_USER` granted authority.
+It also grants the `FACTOR_SAML_RESPONSE` and `ROLE_USER` granted authorities.
 
 image:{icondir}/number_10.png[] And finally, it takes the `NameID` from the first assertion, the `Map` of attributes, and the `GrantedAuthority` and constructs a `Saml2AuthenticatedPrincipal`.
 Then, it places that principal and the authorities into a `Saml2Authentication`.

+ 8 - 1
ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java

@@ -17,6 +17,7 @@
 package org.springframework.security.ldap.authentication;
 
 import java.util.Collection;
+import java.util.LinkedHashSet;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -33,6 +34,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 +52,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 +104,11 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati
 			UserDetails user) {
 		Object password = this.useAuthenticationRequestCredentials ? authentication.getCredentials()
 				: user.getPassword();
-		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(user, password,
+		Collection<GrantedAuthority> authorities = new LinkedHashSet<>(
 				this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
+		authorities.add(new SimpleGrantedAuthority(AUTHORITY));
+		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(user, password,
+				authorities);
 		result.setDetails(authentication.getDetails());
 		this.logger.debug("Authenticated user");
 		return result;

+ 11 - 0
ldap/src/test/java/org/springframework/security/ldap/authentication/LdapAuthenticationProviderTests.java

@@ -26,6 +26,7 @@ import org.springframework.ldap.core.DirContextOperations;
 import org.springframework.ldap.support.LdapNameBuilder;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
@@ -156,6 +157,16 @@ public class LdapAuthenticationProviderTests {
 			.isSameAs(expectedCause);
 	}
 
+	@Test
+	void authenticateWhenSuccessThenIssuesFactor() {
+		MockAuthenticator authenticator = new MockAuthenticator();
+		MockAuthoritiesPopulator populator = new MockAuthoritiesPopulator();
+		LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(authenticator, populator);
+		UsernamePasswordAuthenticationToken request = new UsernamePasswordAuthenticationToken("ben", "benspassword");
+		Authentication result = ldapProvider.authenticate(request);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_PASSWORD");
+	}
+
 	class MockAuthenticator implements LdapAuthenticator {
 
 		@Override

+ 9 - 2
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java

@@ -17,12 +17,15 @@
 package org.springframework.security.oauth2.client.authentication;
 
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 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 +69,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 +123,10 @@ 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());
+		Collection<GrantedAuthority> mappedAuthorities = new LinkedHashSet<>(
+				this.authoritiesMapper.mapAuthorities(authorities));
+		mappedAuthorities.add(new SimpleGrantedAuthority(AUTHORITY));
 		OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
 				loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
 				oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());

+ 17 - 1
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java

@@ -29,6 +29,8 @@ import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.stubbing.Answer;
 
+import org.springframework.security.authentication.SecurityAssertions;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
@@ -48,6 +50,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
 import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
 import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
 import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.core.user.TestOAuth2Users;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -56,6 +59,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyCollection;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 /**
  * Tests for {@link OAuth2LoginAuthenticationProvider}.
@@ -187,7 +191,8 @@ public class OAuth2LoginAuthenticationProviderTests {
 		this.authenticationProvider.setAuthoritiesMapper(authoritiesMapper);
 		OAuth2LoginAuthenticationToken authentication = (OAuth2LoginAuthenticationToken) this.authenticationProvider
 			.authenticate(new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
-		assertThat(authentication.getAuthorities()).isEqualTo(mappedAuthorities);
+		verify(authoritiesMapper).mapAuthorities(any());
+		SecurityAssertions.assertThat(authentication).authorities().containsAll(mappedAuthorities);
 	}
 
 	// gh-5368
@@ -206,6 +211,17 @@ public class OAuth2LoginAuthenticationProviderTests {
 			.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
 	}
 
+	@Test
+	public void authenticateWhenLoginSuccessThenIssuesFactor() {
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenSuccessResponse();
+		given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse);
+		given(this.userService.loadUser(any())).willReturn(TestOAuth2Users.create());
+		Authentication request = new OAuth2LoginAuthenticationToken(this.clientRegistration,
+				this.authorizationExchange);
+		Authentication result = this.authenticationProvider.authenticate(request);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_AUTHORIZATION_CODE");
+	}
+
 	private OAuth2AccessTokenResponse accessTokenSuccessResponse() {
 		Instant expiresAt = Instant.now().plusSeconds(5);
 		Set<String> scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2"));

+ 6 - 2
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java

@@ -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);
 	}

+ 7 - 2
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java

@@ -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);
 	}
 
 	/**

+ 8 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java

@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.SecurityAssertions;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -110,4 +111,11 @@ public class JwtAuthenticationConverterTests {
 		assertThat(authentication.getName()).isEqualTo("100");
 	}
 
+	@Test
+	public void convertWhenDefaultsThenIssuesFactor() {
+		Jwt jwt = TestJwts.jwt().build();
+		Authentication result = this.jwtAuthenticationConverter.convert(jwt);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_BEARER");
+	}
+
 }

+ 11 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java

@@ -146,6 +146,17 @@ public class OpaqueTokenAuthenticationProviderTests {
 		verifyNoMoreInteractions(introspector, authenticationConverter);
 	}
 
+	@Test
+	void authenticateWhenSuccessThenIssuesFactor() {
+		OAuth2AuthenticatedPrincipal principal = TestOAuth2AuthenticatedPrincipals.active();
+		OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class);
+		given(introspector.introspect(any())).willReturn(principal);
+		OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector);
+		Authentication request = new BearerTokenAuthenticationToken("token");
+		Authentication result = provider.authenticate(request);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_BEARER");
+	}
+
 	static Predicate<GrantedAuthority> isScope() {
 		return (a) -> a.getAuthority().startsWith("SCOPE_");
 	}

+ 7 - 1
saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java

@@ -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);
 		}
 

+ 8 - 0
saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java

@@ -985,6 +985,14 @@ public class OpenSaml5AuthenticationProviderTests {
 		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token));
 	}
 
+	@Test
+	public void authenticateWhenSuccessThenIssuesFactor() {
+		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
+		Authentication request = token(response, verifying(registration()));
+		Authentication result = this.provider.authenticate(request);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_SAML_RESPONSE");
+	}
+
 	private <T extends XMLObject> T build(QName qName) {
 		return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
 	}

+ 1 - 0
test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockWithAuthoritiesMvcResultMatchersTests.java

@@ -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));
 	}
 

+ 19 - 1
web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProvider.java

@@ -16,6 +16,11 @@
 
 package org.springframework.security.web.authentication.preauth;
 
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.function.Supplier;
+
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.jspecify.annotations.Nullable;
@@ -28,6 +33,7 @@ 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.userdetails.AuthenticationUserDetailsService;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsChecker;
@@ -57,6 +63,8 @@ public class PreAuthenticatedAuthenticationProvider implements AuthenticationPro
 
 	private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
 
+	private Supplier<Collection<GrantedAuthority>> grantedAuthoritySupplier = List::of;
+
 	private boolean throwExceptionWhenTokenRejected;
 
 	private int order = -1; // default: same as non-ordered
@@ -98,8 +106,10 @@ public class PreAuthenticatedAuthenticationProvider implements AuthenticationPro
 		UserDetails userDetails = this.preAuthenticatedUserDetailsService
 			.loadUserDetails((PreAuthenticatedAuthenticationToken) authentication);
 		this.userDetailsChecker.check(userDetails);
+		Collection<GrantedAuthority> authorities = new LinkedHashSet<>(userDetails.getAuthorities());
+		authorities.addAll(this.grantedAuthoritySupplier.get());
 		PreAuthenticatedAuthenticationToken result = new PreAuthenticatedAuthenticationToken(userDetails,
-				authentication.getCredentials(), userDetails.getAuthorities());
+				authentication.getCredentials(), authorities);
 		result.setDetails(authentication.getDetails());
 		return result;
 	}
@@ -142,6 +152,14 @@ public class PreAuthenticatedAuthenticationProvider implements AuthenticationPro
 		this.userDetailsChecker = userDetailsChecker;
 	}
 
+	/**
+	 * Sets authorities that this provider should grant once authentication completes
+	 * @param grantedAuthoritySupplier the supplier that grants authorities
+	 */
+	public void setGrantedAuthoritySupplier(Supplier<Collection<GrantedAuthority>> grantedAuthoritySupplier) {
+		this.grantedAuthoritySupplier = grantedAuthoritySupplier;
+	}
+
 	@Override
 	public int getOrder() {
 		return this.order;

+ 22 - 0
web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProviderTests.java

@@ -16,12 +16,18 @@
 
 package org.springframework.security.web.authentication.preauth;
 
+import java.util.Collection;
+import java.util.function.Supplier;
+
 import org.junit.jupiter.api.Test;
 
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
+import org.springframework.security.core.userdetails.PasswordEncodedUser;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -29,6 +35,9 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 /**
  * @author TSARDD
@@ -89,6 +98,19 @@ public class PreAuthenticatedAuthenticationProviderTests {
 		assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(() -> provider.authenticate(request));
 	}
 
+	@Test
+	void authenticateWhenSuccessThenIssuesFactor() {
+		UserDetails ud = PasswordEncodedUser.user();
+		PreAuthenticatedAuthenticationProvider provider = getProvider(ud);
+		Supplier<Collection<GrantedAuthority>> authorities = mock(Supplier.class);
+		given(authorities.get()).willReturn(AuthorityUtils.createAuthorityList("FACTOR"));
+		provider.setGrantedAuthoritySupplier(authorities);
+		Authentication request = new PreAuthenticatedAuthenticationToken(ud.getUsername(), ud.getPassword());
+		Authentication result = provider.authenticate(request);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR");
+		verify(authorities).get();
+	}
+
 	@Test
 	public final void supportsArbitraryObject() throws Exception {
 		PreAuthenticatedAuthenticationProvider provider = getProvider(null);

+ 10 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationProvider.java

@@ -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);

+ 61 - 0
webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationProviderTests.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.webauthn.authentication;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.SecurityAssertions;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.PasswordEncodedUser;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
+import org.springframework.security.web.webauthn.api.PublicKeyCredential;
+import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
+import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
+import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
+import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
+import org.springframework.security.web.webauthn.api.TestPublicKeyCredentials;
+import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
+import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+class WebAuthnAuthenticationProviderTests {
+
+	@Test
+	void authenticateWhenSuccessThenIssuesFactor() {
+		WebAuthnRelyingPartyOperations operations = mock(WebAuthnRelyingPartyOperations.class);
+		UserDetailsService users = mock(UserDetailsService.class);
+		PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create().build();
+		AuthenticatorAssertionResponse response = TestAuthenticationAssertionResponses
+			.createAuthenticatorAssertionResponse()
+			.build();
+		PublicKeyCredential<AuthenticatorAssertionResponse> credentials = TestPublicKeyCredentials
+			.createPublicKeyCredential(response)
+			.build();
+		Authentication request = new WebAuthnAuthenticationRequestToken(
+				new RelyingPartyAuthenticationRequest(options, credentials));
+		WebAuthnAuthenticationProvider provider = new WebAuthnAuthenticationProvider(operations, users);
+		given(users.loadUserByUsername(any())).willReturn(PasswordEncodedUser.user());
+		given(operations.authenticate(any())).willReturn(TestPublicKeyCredentialUserEntities.userEntity().build());
+		Authentication result = provider.authenticate(request);
+		SecurityAssertions.assertThat(result).hasAuthority("FACTOR_WEBAUTHN");
+	}
+
+}