Skip to content

Commit 25e4131

Browse files
committed
Merge branch 'authentication-factors'
Closes spring-projectsgh-17933
2 parents 9eaadcc + 1e1cb00 commit 25e4131

File tree

34 files changed

+324
-25
lines changed

34 files changed

+324
-25
lines changed

cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.security.cas.authentication;
1818

19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
1922
import org.apache.commons.logging.Log;
2023
import org.apache.commons.logging.LogFactory;
2124
import org.apereo.cas.client.validation.Assertion;
@@ -35,7 +38,9 @@
3538
import org.springframework.security.cas.ServiceProperties;
3639
import org.springframework.security.core.Authentication;
3740
import org.springframework.security.core.AuthenticationException;
41+
import org.springframework.security.core.GrantedAuthority;
3842
import org.springframework.security.core.SpringSecurityMessageSource;
43+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3944
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
4045
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
4146
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
@@ -64,6 +69,8 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
6469

6570
private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
6671

72+
private static final String AUTHORITY = "FACTOR_CAS";
73+
6774
@SuppressWarnings("NullAway.Init")
6875
private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService;
6976

@@ -141,8 +148,10 @@ private CasAuthenticationToken authenticateNow(final Authentication authenticati
141148
Assertion assertion = this.ticketValidator.validate(credentials.toString(), getServiceUrl(authentication));
142149
UserDetails userDetails = loadUserByAssertion(assertion);
143150
this.userDetailsChecker.check(userDetails);
144-
return new CasAuthenticationToken(this.key, userDetails, credentials,
145-
this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion);
151+
Collection<GrantedAuthority> authorities = new ArrayList<>(
152+
this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));
153+
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
154+
return new CasAuthenticationToken(this.key, userDetails, credentials, authorities, userDetails, assertion);
146155
}
147156
catch (TicketValidationException ex) {
148157
throw new BadCredentialsException(ex.getMessage(), ex);

cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.springframework.mock.web.MockHttpServletRequest;
2929
import org.springframework.security.authentication.BadCredentialsException;
30+
import org.springframework.security.authentication.SecurityAssertions;
3031
import org.springframework.security.authentication.TestingAuthenticationToken;
3132
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3233
import org.springframework.security.cas.ServiceProperties;
@@ -346,6 +347,22 @@ public void check(UserDetails user) {
346347
assertThat(checkCount.get()).isEqualTo(1);
347348
}
348349

350+
@Test
351+
public void authenticateWhenSuccessfulThenIssuesFactor() throws Exception {
352+
CasAuthenticationProvider cap = new CasAuthenticationProvider();
353+
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
354+
cap.setKey("qwerty");
355+
StatelessTicketCache cache = new MockStatelessTicketCache();
356+
cap.setStatelessTicketCache(cache);
357+
cap.setServiceProperties(makeServiceProperties());
358+
cap.setTicketValidator(new MockTicketValidator(true));
359+
cap.afterPropertiesSet();
360+
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("ST-123");
361+
token.setDetails("details");
362+
Authentication result = cap.authenticate(token);
363+
SecurityAssertions.assertThat(result).hasAuthority("FACTOR_CAS");
364+
}
365+
349366
private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService {
350367

351368
@Override

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2626
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2727
import org.springframework.security.core.Authentication;
28+
import org.springframework.security.core.authority.AuthorityUtils;
2829
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
2930
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
3031
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -37,6 +38,7 @@
3738
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3839
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
3940
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
41+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
4042

4143
/**
4244
* Adds X509 based pre authentication to an application. Since validating the certificate
@@ -177,8 +179,13 @@ public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
177179
public void init(H http) {
178180
PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
179181
authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));
182+
authenticationProvider.setGrantedAuthoritySupplier(() -> AuthorityUtils.createAuthorityList("FACTOR_X509"));
180183
http.authenticationProvider(authenticationProvider)
181184
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
185+
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
186+
if (exceptions != null) {
187+
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE);
188+
}
182189
}
183190

184191
@Override

config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.mock.web.MockHttpServletRequest;
3131
import org.springframework.mock.web.MockHttpServletResponse;
3232
import org.springframework.mock.web.MockHttpSession;
33+
import org.springframework.security.authentication.SecurityAssertions;
3334
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
3435
import org.springframework.security.config.test.SpringTestContext;
3536
import org.springframework.security.config.test.SpringTestContextExtension;
@@ -322,8 +323,10 @@ public void requestWhenCustomGrantedAuthoritiesMapperThenCalled() throws Excepti
322323
verify(this.authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
323324
Authentication authentication = authenticationCaptor.getValue();
324325
assertThat(authentication.getPrincipal()).isInstanceOf(OAuth2User.class);
325-
assertThat(authentication.getAuthorities()).hasSize(1);
326-
assertThat(authentication.getAuthorities()).first()
326+
SecurityAssertions.assertThat(authentication)
327+
.roles()
328+
.hasSize(1)
329+
.first()
327330
.isInstanceOf(SimpleGrantedAuthority.class)
328331
.hasToString("ROLE_OAUTH2_USER");
329332
// re-setup for OIDC test

core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.security.authentication.dao;
1818

19+
import java.util.Collection;
20+
import java.util.LinkedHashSet;
21+
1922
import org.apache.commons.logging.Log;
2023
import org.apache.commons.logging.LogFactory;
2124

@@ -33,7 +36,9 @@
3336
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3437
import org.springframework.security.core.Authentication;
3538
import org.springframework.security.core.AuthenticationException;
39+
import org.springframework.security.core.GrantedAuthority;
3640
import org.springframework.security.core.SpringSecurityMessageSource;
41+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3742
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
3843
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
3944
import org.springframework.security.core.userdetails.UserCache;
@@ -94,6 +99,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
9499

95100
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
96101

102+
private static final String AUTHORITY = "FACTOR_PASSWORD";
103+
97104
/**
98105
* Allows subclasses to perform any additional checks of a returned (or cached)
99106
* <code>UserDetails</code> for a given authentication request. Generally a subclass
@@ -197,8 +204,11 @@ protected Authentication createSuccessAuthentication(Object principal, Authentic
197204
// so subsequent attempts are successful even with encoded passwords.
198205
// Also ensure we return the original getDetails(), so that future
199206
// authentication events after cache expiry contain the details
207+
Collection<GrantedAuthority> authorities = new LinkedHashSet<>(
208+
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
209+
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
200210
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
201-
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
211+
authentication.getCredentials(), authorities);
202212
result.setDetails(authentication.getDetails());
203213
this.logger.debug("Authenticated user");
204214
return result;

core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.security.core.Authentication;
4646
import org.springframework.security.core.AuthenticationException;
4747
import org.springframework.security.core.GrantedAuthority;
48+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
4849
import org.springframework.security.core.context.SecurityContext;
4950
import org.springframework.security.core.session.SessionDestroyedEvent;
5051
import org.springframework.util.Assert;
@@ -120,6 +121,8 @@
120121
public abstract class AbstractJaasAuthenticationProvider implements AuthenticationProvider,
121122
ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDestroyedEvent> {
122123

124+
private static final String AUTHORITY = "FACTOR_PASSWORD";
125+
123126
private ApplicationEventPublisher applicationEventPublisher = (event) -> {
124127
};
125128

@@ -210,6 +213,7 @@ private Set<GrantedAuthority> getAuthorities(Set<Principal> principals) {
210213
}
211214
}
212215
}
216+
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
213217
return authorities;
214218
}
215219

core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProvider.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616

1717
package org.springframework.security.authentication.ott;
1818

19+
import java.util.Collection;
20+
import java.util.HashSet;
21+
1922
import org.springframework.security.authentication.AuthenticationProvider;
2023
import org.springframework.security.authentication.BadCredentialsException;
2124
import org.springframework.security.core.Authentication;
2225
import org.springframework.security.core.AuthenticationException;
26+
import org.springframework.security.core.GrantedAuthority;
27+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2328
import org.springframework.security.core.userdetails.UserDetails;
2429
import org.springframework.security.core.userdetails.UserDetailsService;
2530
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -35,6 +40,8 @@
3540
*/
3641
public final class OneTimeTokenAuthenticationProvider implements AuthenticationProvider {
3742

43+
private static final String AUTHORITY = "FACTOR_OTT";
44+
3845
private final OneTimeTokenService oneTimeTokenService;
3946

4047
private final UserDetailsService userDetailsService;
@@ -56,7 +63,9 @@ public Authentication authenticate(Authentication authentication) throws Authent
5663
}
5764
try {
5865
UserDetails user = this.userDetailsService.loadUserByUsername(consumed.getUsername());
59-
OneTimeTokenAuthentication authenticated = new OneTimeTokenAuthentication(user, user.getAuthorities());
66+
Collection<GrantedAuthority> authorities = new HashSet<>(user.getAuthorities());
67+
authorities.add(new SimpleGrantedAuthority(AUTHORITY));
68+
OneTimeTokenAuthentication authenticated = new OneTimeTokenAuthentication(user, authorities);
6069
authenticated.setDetails(otpAuthenticationToken.getDetails());
6170
return authenticated;
6271
}

core/src/test/java/org/springframework/security/authentication/SecurityAssertions.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public CollectionAssert<GrantedAuthority> hasAuthorities(String... authorities)
7575
return authorities().has(new Condition<>(test, "contains %s", Arrays.toString(authorities)));
7676
}
7777

78+
public CollectionAssert<GrantedAuthority> roles() {
79+
return authorities().filteredOn((authority) -> authority.getAuthority().startsWith("ROLE_"));
80+
}
81+
7882
public CollectionAssert<GrantedAuthority> authorities() {
7983
return new CollectionAssert<>(this.authentication.getAuthorities());
8084
}

core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.security.authentication.DisabledException;
3232
import org.springframework.security.authentication.InternalAuthenticationServiceException;
3333
import org.springframework.security.authentication.LockedException;
34+
import org.springframework.security.authentication.SecurityAssertions;
3435
import org.springframework.security.authentication.TestingAuthenticationToken;
3536
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3637
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
@@ -504,6 +505,15 @@ void authenticateWhenPasswordNotLeakedThenNoException() {
504505
assertThat(authentication).isNotNull();
505506
}
506507

508+
@Test
509+
void authenticateWhenSuccessThenIssuesFactor() {
510+
UserDetails user = PasswordEncodedUser.user();
511+
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(withUsers(user));
512+
Authentication request = new UsernamePasswordAuthenticationToken("user", "password");
513+
Authentication result = provider.authenticate(request);
514+
SecurityAssertions.assertThat(result).hasAuthority("FACTOR_PASSWORD");
515+
}
516+
507517
private UserDetailsService withUsers(UserDetails... users) {
508518
return new InMemoryUserDetailsManager(users);
509519
}

core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.context.support.ClassPathXmlApplicationContext;
3636
import org.springframework.core.io.FileSystemResource;
3737
import org.springframework.security.authentication.LockedException;
38+
import org.springframework.security.authentication.SecurityAssertions;
3839
import org.springframework.security.authentication.TestingAuthenticationToken;
3940
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
4041
import org.springframework.security.core.Authentication;
@@ -224,7 +225,9 @@ public void testNullDefaultAuthorities() {
224225
"password");
225226
assertThat(this.jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
226227
Authentication auth = this.jaasProvider.authenticate(token);
227-
assertThat(auth.getAuthorities()).withFailMessage("Only ROLE_TEST1 and ROLE_TEST2 should have been returned")
228+
SecurityAssertions.assertThat(auth)
229+
.roles()
230+
.withFailMessage("Only ROLE_TEST1 and ROLE_TEST2 should have been returned")
228231
.hasSize(2);
229232
}
230233

@@ -234,6 +237,13 @@ public void testUnsupportedAuthenticationObjectReturnsNull() {
234237
.authenticate(new TestingAuthenticationToken("foo", "bar", AuthorityUtils.NO_AUTHORITIES))).isNull();
235238
}
236239

240+
@Test
241+
public void authenticateWhenSuccessThenIssuesFactor() {
242+
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", "password");
243+
Authentication result = this.jaasProvider.authenticate(token);
244+
SecurityAssertions.assertThat(result).hasAuthority("FACTOR_PASSWORD");
245+
}
246+
237247
private static class MockLoginContext extends LoginContext {
238248

239249
boolean loggedOut = false;

0 commit comments

Comments
 (0)