Explorar o código

Merge branch 'authentication-factors'

Closes gh-17933
Josh Cummings hai 2 meses
pai
achega
25e413127c
Modificáronse 34 ficheiros con 324 adicións e 25 borrados
  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;
 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.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.commons.logging.LogFactory;
 import org.apereo.cas.client.validation.Assertion;
 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.cas.ServiceProperties;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.SpringSecurityMessageSource;
 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.GrantedAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
 import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
 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 Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
 
 
+	private static final String AUTHORITY = "FACTOR_CAS";
+
 	@SuppressWarnings("NullAway.Init")
 	@SuppressWarnings("NullAway.Init")
 	private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService;
 	private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService;
 
 
@@ -141,8 +148,10 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
 			Assertion assertion = this.ticketValidator.validate(credentials.toString(), getServiceUrl(authentication));
 			Assertion assertion = this.ticketValidator.validate(credentials.toString(), getServiceUrl(authentication));
 			UserDetails userDetails = loadUserByAssertion(assertion);
 			UserDetails userDetails = loadUserByAssertion(assertion);
 			this.userDetailsChecker.check(userDetails);
 			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) {
 		catch (TicketValidationException ex) {
 			throw new BadCredentialsException(ex.getMessage(), 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.mock.web.MockHttpServletRequest;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.cas.ServiceProperties;
 import org.springframework.security.cas.ServiceProperties;
@@ -346,6 +347,22 @@ public class CasAuthenticationProviderTests {
 		assertThat(checkCount.get()).isEqualTo(1);
 		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 {
 	private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService {
 
 
 		@Override
 		@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.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.core.Authentication;
 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.AuthenticationUserDetailsService;
 import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
 import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
 import org.springframework.security.core.userdetails.UserDetailsService;
 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.X509AuthenticationFilter;
 import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
 import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
 import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
 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
  * 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) {
 	public void init(H http) {
 		PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
 		PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
 		authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));
 		authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));
+		authenticationProvider.setGrantedAuthoritySupplier(() -> AuthorityUtils.createAuthorityList("FACTOR_X509"));
 		http.authenticationProvider(authenticationProvider)
 		http.authenticationProvider(authenticationProvider)
 			.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
 			.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
+		ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
+		if (exceptions != null) {
+			exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE);
+		}
 	}
 	}
 
 
 	@Override
 	@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.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpSession;
 import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
 import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.config.test.SpringTestContextExtension;
@@ -322,8 +323,10 @@ public class OAuth2LoginBeanDefinitionParserTests {
 		verify(this.authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
 		verify(this.authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
 		Authentication authentication = authenticationCaptor.getValue();
 		Authentication authentication = authenticationCaptor.getValue();
 		assertThat(authentication.getPrincipal()).isInstanceOf(OAuth2User.class);
 		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)
 			.isInstanceOf(SimpleGrantedAuthority.class)
 			.hasToString("ROLE_OAUTH2_USER");
 			.hasToString("ROLE_OAUTH2_USER");
 		// re-setup for OIDC test
 		// 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;
 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.Log;
 import org.apache.commons.logging.LogFactory;
 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.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.SpringSecurityMessageSource;
 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.GrantedAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
 import org.springframework.security.core.userdetails.UserCache;
 import org.springframework.security.core.userdetails.UserCache;
@@ -94,6 +99,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
 
 
 	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
 	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
 
 
+	private static final String AUTHORITY = "FACTOR_PASSWORD";
+
 	/**
 	/**
 	 * Allows subclasses to perform any additional checks of a returned (or cached)
 	 * Allows subclasses to perform any additional checks of a returned (or cached)
 	 * <code>UserDetails</code> for a given authentication request. Generally a subclass
 	 * <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.
 		// so subsequent attempts are successful even with encoded passwords.
 		// Also ensure we return the original getDetails(), so that future
 		// Also ensure we return the original getDetails(), so that future
 		// authentication events after cache expiry contain the details
 		// 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,
 		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
-				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
+				authentication.getCredentials(), authorities);
 		result.setDetails(authentication.getDetails());
 		result.setDetails(authentication.getDetails());
 		this.logger.debug("Authenticated user");
 		this.logger.debug("Authenticated user");
 		return result;
 		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.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 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.context.SecurityContext;
 import org.springframework.security.core.session.SessionDestroyedEvent;
 import org.springframework.security.core.session.SessionDestroyedEvent;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
@@ -120,6 +121,8 @@ import org.springframework.util.ObjectUtils;
 public abstract class AbstractJaasAuthenticationProvider implements AuthenticationProvider,
 public abstract class AbstractJaasAuthenticationProvider implements AuthenticationProvider,
 		ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDestroyedEvent> {
 		ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDestroyedEvent> {
 
 
+	private static final String AUTHORITY = "FACTOR_PASSWORD";
+
 	private ApplicationEventPublisher applicationEventPublisher = (event) -> {
 	private ApplicationEventPublisher applicationEventPublisher = (event) -> {
 	};
 	};
 
 
@@ -210,6 +213,7 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati
 				}
 				}
 			}
 			}
 		}
 		}
+		authorities.add(new SimpleGrantedAuthority(AUTHORITY));
 		return authorities;
 		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;
 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.AuthenticationProvider;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 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.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -35,6 +40,8 @@ import org.springframework.util.Assert;
  */
  */
 public final class OneTimeTokenAuthenticationProvider implements AuthenticationProvider {
 public final class OneTimeTokenAuthenticationProvider implements AuthenticationProvider {
 
 
+	private static final String AUTHORITY = "FACTOR_OTT";
+
 	private final OneTimeTokenService oneTimeTokenService;
 	private final OneTimeTokenService oneTimeTokenService;
 
 
 	private final UserDetailsService userDetailsService;
 	private final UserDetailsService userDetailsService;
@@ -56,7 +63,9 @@ public final class OneTimeTokenAuthenticationProvider implements AuthenticationP
 		}
 		}
 		try {
 		try {
 			UserDetails user = this.userDetailsService.loadUserByUsername(consumed.getUsername());
 			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());
 			authenticated.setDetails(otpAuthenticationToken.getDetails());
 			return authenticated;
 			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)));
 			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() {
 		public CollectionAssert<GrantedAuthority> authorities() {
 			return new CollectionAssert<>(this.authentication.getAuthorities());
 			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.DisabledException;
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
 import org.springframework.security.authentication.LockedException;
 import org.springframework.security.authentication.LockedException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.password.CompromisedPasswordChecker;
 import org.springframework.security.authentication.password.CompromisedPasswordChecker;
@@ -504,6 +505,15 @@ public class DaoAuthenticationProviderTests {
 		assertThat(authentication).isNotNull();
 		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) {
 	private UserDetailsService withUsers(UserDetails... users) {
 		return new InMemoryUserDetailsManager(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.context.support.ClassPathXmlApplicationContext;
 import org.springframework.core.io.FileSystemResource;
 import org.springframework.core.io.FileSystemResource;
 import org.springframework.security.authentication.LockedException;
 import org.springframework.security.authentication.LockedException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
@@ -224,7 +225,9 @@ public class JaasAuthenticationProviderTests {
 				"password");
 				"password");
 		assertThat(this.jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
 		assertThat(this.jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
 		Authentication auth = this.jaasProvider.authenticate(token);
 		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);
 			.hasSize(2);
 	}
 	}
 
 
@@ -234,6 +237,13 @@ public class JaasAuthenticationProviderTests {
 			.authenticate(new TestingAuthenticationToken("foo", "bar", AuthorityUtils.NO_AUTHORITIES))).isNull();
 			.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 {
 	private static class MockLoginContext extends LoginContext {
 
 
 		boolean loggedOut = false;
 		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.mockito.junit.jupiter.MockitoExtension;
 
 
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UserDetailsService;
@@ -98,6 +99,18 @@ public class OneTimeTokenAuthenticationProviderTests {
 		assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.provider.authenticate(token));
 		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
 	@Test
 	void constructorWhenOneTimeTokenServiceIsNullThenThrowIllegalArgumentException() {
 	void constructorWhenOneTimeTokenServiceIsNullThenThrowIllegalArgumentException() {
 		// @formatter:off
 		// @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`.
 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.
 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`.
 * `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.
 * 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).
 * 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.
 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.
 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]]
 [[jaas-defaultjaasauthenticationprovider]]
 == 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_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`.
 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.
 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.
 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]]
 [[servlet-x509-config]]
 == Adding X.509 Authentication to Your Web Application
 == 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]]
 [[oauth2login-advanced-map-authorities-oauth2userservice]]
 ==== Delegation-based Strategy with 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]]
 [[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_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`.
 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]]
 [[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]]
 [[oauth2resourceserver-opaque-architecture-introspector]]
 image:{icondir}/number_3.png[] `OpaqueTokenAuthenticationProvider` introspects the opaque token and adds granted authorities using an <<oauth2resourceserver-opaque-introspector,`OpaqueTokenIntrospector`>>.
 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`.
 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]]
 [[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.
 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>>`.
 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`.
 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`.
 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;
 package org.springframework.security.ldap.authentication;
 
 
 import java.util.Collection;
 import java.util.Collection;
+import java.util.LinkedHashSet;
 
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 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.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.SpringSecurityMessageSource;
 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.GrantedAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetails;
@@ -50,6 +52,8 @@ import org.springframework.util.StringUtils;
  */
  */
 public abstract class AbstractLdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
 public abstract class AbstractLdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
 
 
+	private static final String AUTHORITY = "FACTOR_PASSWORD";
+
 	protected final Log logger = LogFactory.getLog(getClass());
 	protected final Log logger = LogFactory.getLog(getClass());
 
 
 	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
 	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
@@ -100,8 +104,11 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati
 			UserDetails user) {
 			UserDetails user) {
 		Object password = this.useAuthenticationRequestCredentials ? authentication.getCredentials()
 		Object password = this.useAuthenticationRequestCredentials ? authentication.getCredentials()
 				: user.getPassword();
 				: user.getPassword();
-		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(user, password,
+		Collection<GrantedAuthority> authorities = new LinkedHashSet<>(
 				this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
 				this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
+		authorities.add(new SimpleGrantedAuthority(AUTHORITY));
+		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(user, password,
+				authorities);
 		result.setDetails(authentication.getDetails());
 		result.setDetails(authentication.getDetails());
 		this.logger.debug("Authenticated user");
 		this.logger.debug("Authenticated user");
 		return result;
 		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.ldap.support.LdapNameBuilder;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.GrantedAuthority;
@@ -156,6 +157,16 @@ public class LdapAuthenticationProviderTests {
 			.isSameAs(expectedCause);
 			.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 {
 	class MockAuthenticator implements LdapAuthenticator {
 
 
 		@Override
 		@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;
 package org.springframework.security.oauth2.client.authentication;
 
 
 import java.util.Collection;
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Map;
 
 
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 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.core.authority.mapping.GrantedAuthoritiesMapper;
 import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
 import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
 import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
 import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
@@ -66,6 +69,8 @@ import org.springframework.util.Assert;
  */
  */
 public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
 public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
 
 
+	private static final String AUTHORITY = "FACTOR_AUTHORIZATION_CODE";
+
 	private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
 	private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
 
 
 	private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
 	private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
@@ -118,8 +123,10 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
 		Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
 		Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
 		OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
 		OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
 				loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
 				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(
 		OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
 				loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
 				loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
 				oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
 				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.ArgumentCaptor;
 import org.mockito.stubbing.Answer;
 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.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
 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.TestOAuth2AuthorizationRequests;
 import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
 import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
 import org.springframework.security.oauth2.core.user.OAuth2User;
 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.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 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.ArgumentMatchers.anyCollection;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 
 /**
 /**
  * Tests for {@link OAuth2LoginAuthenticationProvider}.
  * Tests for {@link OAuth2LoginAuthenticationProvider}.
@@ -187,7 +191,8 @@ public class OAuth2LoginAuthenticationProviderTests {
 		this.authenticationProvider.setAuthoritiesMapper(authoritiesMapper);
 		this.authenticationProvider.setAuthoritiesMapper(authoritiesMapper);
 		OAuth2LoginAuthenticationToken authentication = (OAuth2LoginAuthenticationToken) this.authenticationProvider
 		OAuth2LoginAuthenticationToken authentication = (OAuth2LoginAuthenticationToken) this.authenticationProvider
 			.authenticate(new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
 			.authenticate(new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange));
-		assertThat(authentication.getAuthorities()).isEqualTo(mappedAuthorities);
+		verify(authoritiesMapper).mapAuthorities(any());
+		SecurityAssertions.assertThat(authentication).authorities().containsAll(mappedAuthorities);
 	}
 	}
 
 
 	// gh-5368
 	// gh-5368
@@ -206,6 +211,17 @@ public class OAuth2LoginAuthenticationProviderTests {
 			.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
 			.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() {
 	private OAuth2AccessTokenResponse accessTokenSuccessResponse() {
 		Instant expiresAt = Instant.now().plusSeconds(5);
 		Instant expiresAt = Instant.now().plusSeconds(5);
 		Set<String> scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2"));
 		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;
 package org.springframework.security.oauth2.server.resource.authentication;
 
 
 import java.util.Collection;
 import java.util.Collection;
+import java.util.HashSet;
 
 
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.GrantedAuthority;
 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.Jwt;
 import org.springframework.security.oauth2.jwt.JwtClaimNames;
 import org.springframework.security.oauth2.jwt.JwtClaimNames;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
@@ -34,14 +36,16 @@ import org.springframework.util.Assert;
  */
  */
 public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
 public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
 
 
+	private static final String AUTHORITY = "FACTOR_BEARER";
+
 	private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
 	private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
 
 
 	private String principalClaimName = JwtClaimNames.SUB;
 	private String principalClaimName = JwtClaimNames.SUB;
 
 
 	@Override
 	@Override
 	public final AbstractAuthenticationToken convert(Jwt jwt) {
 	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);
 		String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
 		return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
 		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.time.Instant;
 import java.util.Collection;
 import java.util.Collection;
+import java.util.HashSet;
 
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 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.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 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.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
@@ -72,6 +74,8 @@ import org.springframework.util.Assert;
  */
  */
 public final class OpaqueTokenAuthenticationProvider implements AuthenticationProvider {
 public final class OpaqueTokenAuthenticationProvider implements AuthenticationProvider {
 
 
+	private static final String AUTHORITY = "FACTOR_BEARER";
+
 	private final Log logger = LogFactory.getLog(getClass());
 	private final Log logger = LogFactory.getLog(getClass());
 
 
 	private final OpaqueTokenIntrospector introspector;
 	private final OpaqueTokenIntrospector introspector;
@@ -149,8 +153,9 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
 		Instant exp = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
 		Instant exp = authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP);
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken,
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken,
 				iat, exp);
 				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.core.convert.converter.Converter;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.SecurityAssertions;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -110,4 +111,11 @@ public class JwtAuthenticationConverterTests {
 		assertThat(authentication.getName()).isEqualTo("100");
 		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);
 		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() {
 	static Predicate<GrantedAuthority> isScope() {
 		return (a) -> a.getAuthority().startsWith("SCOPE_");
 		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.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.function.Consumer;
 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.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 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.Saml2Error;
 import org.springframework.security.saml2.core.Saml2ErrorCodes;
 import org.springframework.security.saml2.core.Saml2ErrorCodes;
 import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
 import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
@@ -111,6 +113,8 @@ import org.springframework.util.StringUtils;
  */
  */
 public final class OpenSaml5AuthenticationProvider implements AuthenticationProvider {
 public final class OpenSaml5AuthenticationProvider implements AuthenticationProvider {
 
 
+	private static final String AUTHORITY = "FACTOR_SAML_RESPONSE";
+
 	private final BaseOpenSamlAuthenticationProvider delegate;
 	private final BaseOpenSamlAuthenticationProvider delegate;
 
 
 	/**
 	/**
@@ -899,7 +903,9 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 				.attributes(BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion))
 				.attributes(BaseOpenSamlAuthenticationProvider.getAssertionAttributes(assertion))
 				.build();
 				.build();
 			Saml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(username, accessor);
 			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);
 			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));
 		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) {
 	private <T extends XMLObject> T build(QName qName) {
 		return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(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<>();
 		List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
 		grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
 		grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
 		grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_SELLER"));
 		grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_SELLER"));
+		grantedAuthorities.add(new SimpleGrantedAuthority("FACTOR_PASSWORD"));
 		this.mockMvc.perform(formLogin()).andExpect(authenticated().withAuthorities(grantedAuthorities));
 		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;
 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.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.commons.logging.LogFactory;
 import org.jspecify.annotations.Nullable;
 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.authentication.BadCredentialsException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 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.AuthenticationUserDetailsService;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsChecker;
 import org.springframework.security.core.userdetails.UserDetailsChecker;
@@ -57,6 +63,8 @@ public class PreAuthenticatedAuthenticationProvider implements AuthenticationPro
 
 
 	private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
 	private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
 
 
+	private Supplier<Collection<GrantedAuthority>> grantedAuthoritySupplier = List::of;
+
 	private boolean throwExceptionWhenTokenRejected;
 	private boolean throwExceptionWhenTokenRejected;
 
 
 	private int order = -1; // default: same as non-ordered
 	private int order = -1; // default: same as non-ordered
@@ -98,8 +106,10 @@ public class PreAuthenticatedAuthenticationProvider implements AuthenticationPro
 		UserDetails userDetails = this.preAuthenticatedUserDetailsService
 		UserDetails userDetails = this.preAuthenticatedUserDetailsService
 			.loadUserDetails((PreAuthenticatedAuthenticationToken) authentication);
 			.loadUserDetails((PreAuthenticatedAuthenticationToken) authentication);
 		this.userDetailsChecker.check(userDetails);
 		this.userDetailsChecker.check(userDetails);
+		Collection<GrantedAuthority> authorities = new LinkedHashSet<>(userDetails.getAuthorities());
+		authorities.addAll(this.grantedAuthoritySupplier.get());
 		PreAuthenticatedAuthenticationToken result = new PreAuthenticatedAuthenticationToken(userDetails,
 		PreAuthenticatedAuthenticationToken result = new PreAuthenticatedAuthenticationToken(userDetails,
-				authentication.getCredentials(), userDetails.getAuthorities());
+				authentication.getCredentials(), authorities);
 		result.setDetails(authentication.getDetails());
 		result.setDetails(authentication.getDetails());
 		return result;
 		return result;
 	}
 	}
@@ -142,6 +152,14 @@ public class PreAuthenticatedAuthenticationProvider implements AuthenticationPro
 		this.userDetailsChecker = userDetailsChecker;
 		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
 	@Override
 	public int getOrder() {
 	public int getOrder() {
 		return this.order;
 		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;
 package org.springframework.security.web.authentication.preauth;
 
 
+import java.util.Collection;
+import java.util.function.Supplier;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 
 
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 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.AuthorityUtils;
 import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
 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.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 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.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 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
  * @author TSARDD
@@ -89,6 +98,19 @@ public class PreAuthenticatedAuthenticationProviderTests {
 		assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(() -> provider.authenticate(request));
 		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
 	@Test
 	public final void supportsArbitraryObject() throws Exception {
 	public final void supportsArbitraryObject() throws Exception {
 		PreAuthenticatedAuthenticationProvider provider = getProvider(null);
 		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;
 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.AuthenticationProvider;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 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.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
@@ -39,6 +44,8 @@ import org.springframework.util.Assert;
  */
  */
 public class WebAuthnAuthenticationProvider implements AuthenticationProvider {
 public class WebAuthnAuthenticationProvider implements AuthenticationProvider {
 
 
+	private static final String AUTHORITY = "FACTOR_WEBAUTHN";
+
 	private final WebAuthnRelyingPartyOperations relyingPartyOperations;
 	private final WebAuthnRelyingPartyOperations relyingPartyOperations;
 
 
 	private final UserDetailsService userDetailsService;
 	private final UserDetailsService userDetailsService;
@@ -65,7 +72,9 @@ public class WebAuthnAuthenticationProvider implements AuthenticationProvider {
 				.authenticate(webAuthnRequest.getWebAuthnRequest());
 				.authenticate(webAuthnRequest.getWebAuthnRequest());
 			String username = userEntity.getName();
 			String username = userEntity.getName();
 			UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
 			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) {
 		catch (RuntimeException ex) {
 			throw new BadCredentialsException(ex.getMessage(), 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");
+	}
+
+}