Merge remote-tracking branch 'jzheaux/authentication-builder'

Issue gh-17861
Issue gh-17862
This commit is contained in:
Josh Cummings 2025-09-09 15:43:26 -06:00
commit 6689798257
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
53 changed files with 1739 additions and 26 deletions

View File

@ -20,6 +20,7 @@ import java.io.Serializable;
import java.util.Collection;
import org.apereo.cas.client.validation.Assertion;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
@ -104,6 +105,19 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
setAuthenticated(true);
}
protected CasAuthenticationToken(Builder<?> builder) {
super(builder);
Assert.isTrue(!"".equals(builder.principal), "principal cannot be null or empty");
Assert.notNull(!"".equals(builder.credentials), "credentials cannot be null or empty");
Assert.notNull(builder.userDetails, "userDetails cannot be null");
Assert.notNull(builder.assertion, "assertion cannot be null");
this.keyHash = builder.keyHash;
this.principal = builder.principal;
this.credentials = builder.credentials;
this.userDetails = builder.userDetails;
this.assertion = builder.assertion;
}
private static Integer extractKeyHash(String key) {
Assert.hasLength(key, "key cannot be null or empty");
return key.hashCode();
@ -153,6 +167,11 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
return this.userDetails;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@ -162,4 +181,81 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
return (sb.toString());
}
/**
* A builder of {@link CasAuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private Integer keyHash;
private Object principal;
private Object credentials;
private UserDetails userDetails;
private Assertion assertion;
protected Builder(CasAuthenticationToken token) {
super(token);
this.keyHash = token.keyHash;
this.principal = token.principal;
this.credentials = token.credentials;
this.userDetails = token.userDetails;
this.assertion = token.assertion;
}
/**
* Use this key
* @param key the key to use
* @return the {@link Builder} for further configurations
*/
public B key(String key) {
this.keyHash = key.hashCode();
return (B) this;
}
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
@Override
public B credentials(@Nullable Object credentials) {
Assert.notNull(credentials, "credentials cannot be null");
this.credentials = credentials;
return (B) this;
}
/**
* Use this {@link UserDetails}
* @param userDetails the {@link UserDetails} to use
* @return the {@link Builder} for further configurations
*/
public B userDetails(UserDetails userDetails) {
this.userDetails = userDetails;
return (B) this;
}
/**
* Use this {@link Assertion}
* @param assertion the {@link Assertion} to use
* @return the {@link Builder} for further configurations
*/
public B assertion(Assertion assertion) {
this.assertion = assertion;
return (B) this;
}
@Override
public CasAuthenticationToken build() {
return new CasAuthenticationToken(this);
}
}
}

View File

@ -52,7 +52,7 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT
*
*/
public CasServiceTicketAuthenticationToken(String identifier, Object credentials) {
super(null);
super((Collection<? extends GrantedAuthority>) null);
this.identifier = identifier;
this.credentials = credentials;
setAuthenticated(false);
@ -75,6 +75,12 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT
super.setAuthenticated(true);
}
protected CasServiceTicketAuthenticationToken(Builder<?> builder) {
super(builder);
this.identifier = builder.principal;
this.credentials = builder.credentials;
}
public static CasServiceTicketAuthenticationToken stateful(Object credentials) {
return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials);
}
@ -110,4 +116,46 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT
this.credentials = null;
}
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link CasServiceTicketAuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private String principal;
private @Nullable Object credentials;
protected Builder(CasServiceTicketAuthenticationToken token) {
super(token);
this.principal = token.identifier;
this.credentials = token.credentials;
}
@Override
public B principal(@Nullable Object principal) {
Assert.isInstanceOf(String.class, principal, "principal must be of type String");
this.principal = (String) principal;
return (B) this;
}
@Override
public B credentials(@Nullable Object credentials) {
Assert.notNull(credentials, "credentials cannot be null");
this.credentials = credentials;
return (B) this;
}
@Override
public CasServiceTicketAuthenticationToken build() {
return new CasServiceTicketAuthenticationToken(this);
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.cas.authentication;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apereo.cas.client.validation.Assertion;
import org.apereo.cas.client.validation.AssertionImpl;
@ -26,6 +27,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
@ -155,4 +157,29 @@ public class CasAuthenticationTokenTests {
assertThat(result.lastIndexOf("Credentials (Service/Proxy Ticket):") != -1).isTrue();
}
@Test
public void toBuilderWhenApplyThenCopies() {
Assertion assertionOne = new AssertionImpl("test");
CasAuthenticationToken factorOne = new CasAuthenticationToken("key", "alice", "pass",
AuthorityUtils.createAuthorityList("FACTOR_ONE"), PasswordEncodedUser.user(), assertionOne);
Assertion assertionTwo = new AssertionImpl("test");
CasAuthenticationToken factorTwo = new CasAuthenticationToken("yek", "bob", "ssap",
AuthorityUtils.createAuthorityList("FACTOR_TWO"), PasswordEncodedUser.admin(), assertionTwo);
CasAuthenticationToken authentication = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.key("yek")
.principal(factorTwo.getPrincipal())
.credentials(factorTwo.getCredentials())
.userDetails(factorTwo.getUserDetails())
.assertion(factorTwo.getAssertion())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash());
assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal());
assertThat(authentication.getCredentials()).isEqualTo(factorTwo.getCredentials());
assertThat(authentication.getUserDetails()).isEqualTo(factorTwo.getUserDetails());
assertThat(authentication.getAssertion()).isEqualTo(factorTwo.getAssertion());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.config.annotation.web.configurers;
import java.util.Collection;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -31,6 +33,7 @@ import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.web.servlet.MockMvc;
@ -113,7 +116,7 @@ public class SessionManagementConfigurerTransientAuthenticationTests {
static class SomeTransientAuthentication extends AbstractAuthenticationToken {
SomeTransientAuthentication() {
super(null);
super((Collection<? extends GrantedAuthority>) null);
}
@Override

View File

@ -16,6 +16,8 @@
package org.springframework.security.config.http;
import java.util.Collection;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -26,6 +28,7 @@ import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@ -82,7 +85,7 @@ public class SessionManagementConfigTransientAuthenticationTests {
static class SomeTransientAuthentication extends AbstractAuthenticationToken {
SomeTransientAuthentication() {
super(null);
super((Collection<? extends GrantedAuthority>) null);
}
@Override

View File

@ -16,10 +16,13 @@
package org.springframework.security.authentication;
import java.io.Serial;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
@ -41,6 +44,9 @@ import org.springframework.util.Assert;
*/
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
@Serial
private static final long serialVersionUID = -3194696462184782834L;
private final Collection<GrantedAuthority> authorities;
private @Nullable Object details;
@ -63,6 +69,12 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
this.authorities = Collections.unmodifiableList(new ArrayList<>(authorities));
}
protected AbstractAuthenticationToken(AbstractAuthenticationBuilder<?> builder) {
this(builder.authorities);
this.authenticated = builder.authenticated;
this.details = builder.details;
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
return this.authorities;
@ -185,4 +197,48 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
return sb.toString();
}
/**
* A common abstract implementation of {@link Authentication.Builder}. It implements
* the builder methods that correspond to the {@link Authentication} methods that
* {@link AbstractAuthenticationToken} implements
*
* @param <B>
* @since 7.0
*/
protected abstract static class AbstractAuthenticationBuilder<B extends AbstractAuthenticationBuilder<B>>
implements Authentication.Builder<B> {
private boolean authenticated;
private @Nullable Object details;
private final Collection<GrantedAuthority> authorities;
protected AbstractAuthenticationBuilder(AbstractAuthenticationToken token) {
this.authorities = new LinkedHashSet<>(token.getAuthorities());
this.authenticated = token.isAuthenticated();
this.details = token.getDetails();
}
@Override
public B authenticated(boolean authenticated) {
this.authenticated = authenticated;
return (B) this;
}
@Override
public B details(@Nullable Object details) {
this.details = details;
return (B) this;
}
@Override
public B authorities(Consumer<Collection<GrantedAuthority>> authorities) {
authorities.accept(this.authorities);
this.authenticated = true;
return (B) this;
}
}
}

View File

@ -61,7 +61,6 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti
Function<ReactiveAuthenticationManager, Mono<Authentication>> logging = (m) -> m.authenticate(authentication)
.doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication))
.doOnError(this.logger::debug);
return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next();
}

View File

@ -182,7 +182,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
result = copyDetails(authentication, result);
break;
}
}
@ -277,10 +277,14 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
* @param source source authentication
* @param dest the destination authentication object
*/
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
token.setDetails(source.getDetails());
private Authentication copyDetails(Authentication source, Authentication dest) {
if (source.getDetails() == null) {
return dest;
}
if (dest.getDetails() != null) {
return dest;
}
return dest.toBuilder().details(source.getDetails()).build();
}
public List<AuthenticationProvider> getProviders() {

View File

@ -18,7 +18,10 @@ package org.springframework.security.authentication;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/**
* Represents a remembered <code>Authentication</code>.
@ -70,6 +73,12 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
setAuthenticated(true);
}
protected RememberMeAuthenticationToken(Builder<?> builder) {
super(builder);
this.keyHash = builder.keyHash;
this.principal = builder.principal;
}
/**
* Always returns an empty <code>String</code>
* @return an empty String
@ -88,6 +97,11 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
return this.principal;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) {
@ -106,4 +120,45 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
return result;
}
/**
* A builder of {@link RememberMeAuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private Integer keyHash;
private Object principal;
protected Builder(RememberMeAuthenticationToken token) {
super(token);
this.keyHash = token.getKeyHash();
this.principal = token.getPrincipal();
}
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
/**
* Use this key
* @param key the key to use
* @return the {@link Builder} for further configurations
*/
public B key(String key) {
this.keyHash = key.hashCode();
return (B) this;
}
@Override
public RememberMeAuthenticationToken build() {
return new RememberMeAuthenticationToken(this);
}
}
}

View File

@ -19,8 +19,11 @@ package org.springframework.security.authentication;
import java.util.Collection;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.util.Assert;
/**
* An {@link org.springframework.security.core.Authentication} implementation that is
@ -39,7 +42,7 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public TestingAuthenticationToken(Object principal, Object credentials) {
super(null);
super((Collection<? extends GrantedAuthority>) null);
this.principal = principal;
this.credentials = credentials;
}
@ -61,6 +64,12 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
setAuthenticated(true);
}
protected TestingAuthenticationToken(Builder<?> builder) {
super(builder);
this.principal = builder.principal;
this.credentials = builder.credentials;
}
@Override
public Object getCredentials() {
return this.credentials;
@ -71,4 +80,47 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
return this.principal;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link TestingAuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private Object principal;
private Object credentials;
protected Builder(TestingAuthenticationToken token) {
super(token);
this.principal = token.principal;
this.credentials = token.credentials;
}
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
@Override
public B credentials(@Nullable Object credentials) {
Assert.notNull(credentials, "credentials cannot be null");
this.credentials = credentials;
return (B) this;
}
@Override
public TestingAuthenticationToken build() {
return new TestingAuthenticationToken(this);
}
}
}

View File

@ -50,7 +50,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
*
*/
public UsernamePasswordAuthenticationToken(@Nullable Object principal, @Nullable Object credentials) {
super(null);
super((Collection<? extends GrantedAuthority>) null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
@ -73,6 +73,12 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
super.setAuthenticated(true); // must use super, as we override
}
protected UsernamePasswordAuthenticationToken(Builder<?> builder) {
super(builder);
this.principal = builder.principal;
this.credentials = builder.credentials;
}
/**
* This factory method can be safely used by any code that wishes to create a
* unauthenticated <code>UsernamePasswordAuthenticationToken</code>.
@ -124,4 +130,46 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
this.credentials = null;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link UsernamePasswordAuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private @Nullable Object principal;
private @Nullable Object credentials;
protected Builder(UsernamePasswordAuthenticationToken token) {
super(token);
this.principal = token.principal;
this.credentials = token.credentials;
}
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
@Override
public B credentials(@Nullable Object credentials) {
this.credentials = credentials;
return (B) this;
}
@Override
public UsernamePasswordAuthenticationToken build() {
return new UsernamePasswordAuthenticationToken(this);
}
}
}

View File

@ -48,8 +48,49 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
this.loginContext = loginContext;
}
protected JaasAuthenticationToken(Builder<?> builder) {
super(builder);
this.loginContext = builder.loginContext;
}
public LoginContext getLoginContext() {
return this.loginContext;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link JaasAuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends UsernamePasswordAuthenticationToken.Builder<B> {
private LoginContext loginContext;
protected Builder(JaasAuthenticationToken token) {
super(token);
this.loginContext = token.getLoginContext();
}
/**
* Use this {@link LoginContext}
* @param loginContext the {@link LoginContext} to use
* @return the {@link Builder} for further configurations
*/
public B loginContext(LoginContext loginContext) {
this.loginContext = loginContext;
return (B) this;
}
@Override
public JaasAuthenticationToken build() {
return new JaasAuthenticationToken(this);
}
}
}

View File

@ -23,6 +23,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/**
* The result of a successful one-time-token authentication
@ -43,6 +44,11 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
setAuthenticated(true);
}
protected OneTimeTokenAuthentication(Builder<?> builder) {
super(builder);
this.principal = builder.principal;
}
@Override
public Object getPrincipal() {
return this.principal;
@ -53,4 +59,41 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
return null;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link OneTimeTokenAuthentication} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private Object principal;
protected Builder(OneTimeTokenAuthentication token) {
super(token);
this.principal = token.principal;
}
/**
* Use this principal.
* @return the {@link Builder} for further configuration
*/
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
@Override
public OneTimeTokenAuthentication build() {
return new OneTimeTokenAuthentication(this);
}
}
}

View File

@ -19,6 +19,7 @@ package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
@ -136,4 +137,110 @@ public interface Authentication extends Principal, Serializable {
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
/**
* Return an {@link Builder} based on this instance. By default, returns a builder
* that builds a {@link SimpleAuthentication}.
* <p>
* Although a {@code default} method, all {@link Authentication} implementations
* should implement this. The reason is to ensure that the {@link Authentication} type
* is preserved when {@link Builder#build} is invoked. This is especially important in
* the event that your authentication implementation contains custom fields.
* </p>
* <p>
* This isn't strictly necessary since it is recommended that applications code to the
* {@link Authentication} interface and that custom information is often contained in
* the {@link Authentication#getPrincipal} value.
* </p>
* @return an {@link Builder} for building a new {@link Authentication} based on this
* instance
* @since 7.0
*/
default Builder<?> toBuilder() {
return new SimpleAuthentication.Builder(this);
}
/**
* A builder based on a given {@link Authentication} instance
*
* @author Josh Cummings
* @since 7.0
*/
interface Builder<B extends Builder<B>> {
/**
* Mutate the authorities with this {@link Consumer}.
* <p>
* Note that since a non-empty set of authorities implies an
* {@link Authentication} is authenticated, this method also marks the
* authentication as {@link #authenticated} by default.
* </p>
* @param authorities a consumer that receives the full set of authorities
* @return the {@link Builder} for additional configuration
* @see Authentication#getAuthorities
*/
B authorities(Consumer<Collection<GrantedAuthority>> authorities);
/**
* Use this credential.
* <p>
* Note that since some credentials are insecure to store, this method is
* implemented as unsupported by default. Only implement or use this method if you
* support secure storage of the credential or if your implementation also
* implements {@link CredentialsContainer} and the credentials are thereby erased.
* </p>
* @param credentials the credentials to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getCredentials
*/
default B credentials(@Nullable Object credentials) {
throw new UnsupportedOperationException(
String.format("%s does not store credentials", this.getClass().getSimpleName()));
}
/**
* Use this details object.
* <p>
* Implementations may choose to use these {@code details} in combination with any
* principal from the pre-existing {@link Authentication} instance.
* </p>
* @param details the details to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getDetails
*/
B details(@Nullable Object details);
/**
* Use this principal.
* <p>
* Note that in many cases, the principal is strongly-typed. Implementations may
* choose to do a type check and are not necessarily expected to allow any object
* as a principal.
* </p>
* <p>
* Implementations may choose to use this {@code principal} in combination with
* any principal from the pre-existing {@link Authentication} instance.
* </p>
* @param principal the principal to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getPrincipal
*/
B principal(@Nullable Object principal);
/**
* Mark this authentication as authenticated or not
* @param authenticated whether this is an authenticated {@link Authentication}
* instance
* @return the {@link Builder} for additional configuration
* @see Authentication#isAuthenticated
*/
B authenticated(boolean authenticated);
/**
* Build an {@link Authentication} instance
* @return the {@link Authentication} instance
*/
Authentication build();
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.core;
import java.io.Serial;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
@Transient
final class SimpleAuthentication implements Authentication {
@Serial
private static final long serialVersionUID = 3194696462184782814L;
private final @Nullable Object principal;
private final @Nullable Object credentials;
private final Collection<GrantedAuthority> authorities;
private final @Nullable Object details;
private final boolean authenticated;
private SimpleAuthentication(Builder builder) {
this.principal = builder.principal;
this.credentials = builder.credentials;
this.authorities = builder.authorities;
this.details = builder.details;
this.authenticated = builder.authenticated;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public @Nullable Object getCredentials() {
return this.credentials;
}
@Override
public @Nullable Object getDetails() {
return this.details;
}
@Override
public @Nullable Object getPrincipal() {
return this.principal;
}
@Override
public boolean isAuthenticated() {
return this.authenticated;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
throw new IllegalArgumentException(
"Instead of calling this setter, please call toBuilder to create a new instance");
}
@Override
public String getName() {
return (this.principal == null) ? "" : this.principal.toString();
}
static final class Builder implements Authentication.Builder<Builder> {
private final Log logger = LogFactory.getLog(getClass());
private final Collection<GrantedAuthority> authorities = new LinkedHashSet<>();
private @Nullable Object principal;
private @Nullable Object credentials;
private @Nullable Object details;
private boolean authenticated;
Builder(Authentication authentication) {
this.logger.debug("Creating a builder which will result in exchanging an authentication of type "
+ authentication.getClass() + " for " + SimpleAuthentication.class.getSimpleName() + ";"
+ " consider implementing " + authentication.getClass().getSimpleName() + "#toBuilder");
this.authorities.addAll(authentication.getAuthorities());
this.principal = authentication.getPrincipal();
this.credentials = authentication.getCredentials();
this.details = authentication.getDetails();
this.authenticated = authentication.isAuthenticated();
}
@Override
public Builder authorities(Consumer<Collection<GrantedAuthority>> authorities) {
authorities.accept(this.authorities);
return this;
}
@Override
public Builder details(@Nullable Object details) {
this.details = details;
return this;
}
@Override
public Builder principal(@Nullable Object principal) {
this.principal = principal;
return this;
}
@Override
public Builder credentials(@Nullable Object credentials) {
this.credentials = credentials;
return this;
}
@Override
public Builder authenticated(boolean authenticated) {
this.authenticated = authenticated;
return this;
}
@Override
public Authentication build() {
return new SimpleAuthentication(this);
}
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.authentication;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import static org.assertj.core.api.Assertions.assertThat;
class AbstractAuthenticationBuilderTests {
@Test
void applyWhenAuthoritiesThenAdds() {
TestingAuthenticationToken factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
TestAbstractAuthenticationBuilder builder = new TestAbstractAuthenticationBuilder(factorOne);
Authentication result = builder.authorities((a) -> a.addAll(factorTwo.getAuthorities())).build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
private static final class TestAbstractAuthenticationBuilder
extends TestingAuthenticationToken.Builder<TestAbstractAuthenticationBuilder> {
private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) {
super(token);
}
@Override
public TestingAuthenticationToken build() {
return new TestingAuthenticationToken(this);
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.authentication;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.junit.jupiter.api.Test;
@ -25,6 +26,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.context.MessageSource;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ -47,7 +49,7 @@ public class ProviderManagerTests {
@Test
void authenticationFailsWithUnsupportedToken() {
Authentication token = new AbstractAuthenticationToken(null) {
Authentication token = new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
@Override
public Object getCredentials() {
return "";
@ -78,24 +80,24 @@ public class ProviderManagerTests {
@Test
void authenticationSucceedsWithSupportedTokenAndReturnsExpectedObject() {
Authentication a = mock(Authentication.class);
Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR");
ProviderManager mgr = new ProviderManager(createProviderWhichReturns(a));
AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class);
mgr.setAuthenticationEventPublisher(publisher);
Authentication result = mgr.authenticate(a);
assertThat(result).isEqualTo(a);
assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal());
verify(publisher).publishAuthenticationSuccess(result);
}
@Test
void authenticationSucceedsWhenFirstProviderReturnsNullButSecondAuthenticates() {
Authentication a = mock(Authentication.class);
Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR");
ProviderManager mgr = new ProviderManager(
Arrays.asList(createProviderWhichReturns(null), createProviderWhichReturns(a)));
AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class);
mgr.setAuthenticationEventPublisher(publisher);
Authentication result = mgr.authenticate(a);
assertThat(result).isSameAs(a);
assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal());
verify(publisher).publishAuthenticationSuccess(result);
}
@ -162,11 +164,12 @@ public class ProviderManagerTests {
@Test
void authenticationExceptionIsIgnoredIfLaterProviderAuthenticates() {
Authentication authReq = mock(Authentication.class);
Authentication result = new TestingAuthenticationToken("user", "pass", "FACTOR");
ProviderManager mgr = new ProviderManager(
createProviderWhichThrows(new BadCredentialsException("", new Throwable())),
createProviderWhichReturns(authReq));
assertThat(mgr.authenticate(mock(Authentication.class))).isSameAs(authReq);
createProviderWhichReturns(result));
Authentication request = new TestingAuthenticationToken("user", "pass");
assertThat(mgr.authenticate(request).getPrincipal()).isEqualTo(result.getPrincipal());
}
@Test

View File

@ -17,9 +17,11 @@
package org.springframework.security.authentication;
import java.util.Arrays;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import static org.assertj.core.api.Assertions.assertThat;
@ -49,4 +51,21 @@ public class TestingAuthenticationTokenTests {
assertThat(authenticated.isAuthenticated()).isTrue();
}
@Test
public void toBuilderWhenApplyThenCopies() {
TestingAuthenticationToken factorOne = new TestingAuthenticationToken("alice", "pass",
AuthorityUtils.createAuthorityList("FACTOR_ONE"));
TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("bob", "ssap",
AuthorityUtils.createAuthorityList("FACTOR_TWO"));
TestingAuthenticationToken result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.credentials(factorTwo.getCredentials())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.authentication;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.AuthorityUtils;
@ -85,4 +87,21 @@ public class UsernamePasswordAuthenticationTokenTests {
assertThat(grantedToken.isAuthenticated()).isTrue();
}
@Test
public void toBuilderWhenApplyThenCopies() {
UsernamePasswordAuthenticationToken factorOne = new UsernamePasswordAuthenticationToken("alice", "pass",
AuthorityUtils.createAuthorityList("FACTOR_ONE"));
UsernamePasswordAuthenticationToken factorTwo = new UsernamePasswordAuthenticationToken("bob", "ssap",
AuthorityUtils.createAuthorityList("FACTOR_TWO"));
UsernamePasswordAuthenticationToken result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.credentials(factorTwo.getCredentials())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isEqualTo("bob");
assertThat(result.getCredentials()).isEqualTo("ssap");
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.authentication.jaas;
import java.util.Set;
import javax.security.auth.login.LoginContext;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.AuthorityUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class JaasAuthenticationTokenTests {
@Test
void toBuilderWhenApplyThenCopies() {
JaasAuthenticationToken factorOne = new JaasAuthenticationToken("alice", "pass",
AuthorityUtils.createAuthorityList("FACTOR_ONE"), mock(LoginContext.class));
JaasAuthenticationToken factorTwo = new JaasAuthenticationToken("bob", "ssap",
AuthorityUtils.createAuthorityList("FACTOR_TWO"), mock(LoginContext.class));
JaasAuthenticationToken result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.credentials(factorTwo.getCredentials())
.loginContext(factorTwo.getLoginContext())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
assertThat(result.getLoginContext()).isSameAs(factorTwo.getLoginContext());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.authentication.ott;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.AuthorityUtils;
import static org.assertj.core.api.Assertions.assertThat;
class OneTimeTokenAuthenticationTests {
@Test
void toBuilderWhenApplyThenCopies() {
OneTimeTokenAuthentication factorOne = new OneTimeTokenAuthentication("alice",
AuthorityUtils.createAuthorityList("FACTOR_ONE"));
OneTimeTokenAuthentication factorTwo = new OneTimeTokenAuthentication("bob",
AuthorityUtils.createAuthorityList("FACTOR_TWO"));
OneTimeTokenAuthentication result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.authentication.rememberme;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
@ -25,6 +26,7 @@ import org.springframework.security.authentication.RememberMeAuthenticationToken
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -96,4 +98,21 @@ public class RememberMeAuthenticationTokenTests {
assertThat(!token.isAuthenticated()).isTrue();
}
@Test
public void toBuilderWhenApplyThenCopies() {
RememberMeAuthenticationToken factorOne = new RememberMeAuthenticationToken("key", PasswordEncodedUser.user(),
AuthorityUtils.createAuthorityList("FACTOR_ONE"));
RememberMeAuthenticationToken factorTwo = new RememberMeAuthenticationToken("yek", PasswordEncodedUser.admin(),
AuthorityUtils.createAuthorityList("FACTOR_TWO"));
RememberMeAuthenticationToken authentication = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.key("yek")
.principal(factorTwo.getPrincipal())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash());
assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -140,6 +140,11 @@ In many cases, this is cleared after the user is authenticated, to ensure that i
* `authorities`: The <<servlet-authentication-granted-authority,`GrantedAuthority`>> instances are high-level permissions the user is granted.
Two examples are roles and scopes.
It is also equipped with a `Builder` that allows you to mutate an existing `Authentication` instance and potentially merge it with another.
This is useful in scenarios like taking the authorities from one authentication step, like form login, and applying them to another, like one-time-token login, like so:
include-code::./CopyAuthoritiesTests[tag=springSecurity,indent=0]
[[servlet-authentication-granted-authority]]
== GrantedAuthority
javadoc:org.springframework.security.core.GrantedAuthority[] instances are high-level permissions that the user is granted.
@ -231,8 +236,6 @@ In other cases, a client makes an unauthenticated request to a resource that the
In this case, an implementation of `AuthenticationEntryPoint` is used to request credentials from the client.
The `AuthenticationEntryPoint` implementation might perform a xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form[redirect to a log in page], respond with an xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[WWW-Authenticate] header, or take other action.
// FIXME: authenticationsuccesshandler
// FIXME: authenticationfailurehandler
@ -266,6 +269,8 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
* `SessionAuthenticationStrategy` is notified of a new login.
See the javadoc:org.springframework.security.web.authentication.session.SessionAuthenticationStrategy[] interface.
* Any already-authenticated `Authentication` in the <<servlet-authentication-securitycontextholder>> is loaded and its
authorities are added to the returned <<servlet-authentication-authentication>>.
* The <<servlet-authentication-authentication>> is set on the <<servlet-authentication-securitycontextholder>>.
Later, if you need to save the `SecurityContext` so that it can be automatically set on future requests, `SecurityContextRepository#saveContext` must be explicitly invoked.
See the javadoc:org.springframework.security.web.context.SecurityContextHolderFilter[] class.

View File

@ -56,6 +56,8 @@ See the javadoc:org.springframework.security.web.AuthenticationEntryPoint[] inte
image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
* Any already-authenticated `Authentication` in the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] is loaded and its
authorities are added to the returned xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`].
. The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
. `RememberMeServices.loginSuccess` is invoked.
If remember me is not configured, this is a no-op.

View File

@ -56,5 +56,7 @@ image:{icondir}/number_3.png[] If authentication fails, then __Failure__
image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
* Any already-authenticated `Authentication` in the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] is loaded and its
authorities are added to the returned xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`].
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
* The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic.

View File

@ -13,6 +13,7 @@ Each section that follows will indicate the more notable removals as well as the
* Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
* Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components
* Added `Authentication.Builder` for mutating and merging `Authentication` instances
== Config

View File

@ -0,0 +1,41 @@
package org.springframework.security.docs.servlet.authentication.servletauthenticationauthentication;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.SecurityAssertions;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.ott.OneTimeTokenAuthentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
public class CopyAuthoritiesTests {
@Test
void toBuilderWhenApplyThenCopies() {
UsernamePasswordAuthenticationToken previous = new UsernamePasswordAuthenticationToken("alice", "pass",
AuthorityUtils.createAuthorityList("FACTOR_PASSWORD"));
SecurityContextHolder.getContext().setAuthentication(previous);
Authentication latest = new OneTimeTokenAuthentication("bob",
AuthorityUtils.createAuthorityList("FACTOR_OTT"));
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
given(authenticationManager.authenticate(any())).willReturn(latest);
Authentication authenticationRequest = new TestingAuthenticationToken("user", "pass");
// tag::springSecurity[]
Authentication lastestResult = authenticationManager.authenticate(authenticationRequest);
Authentication previousResult = SecurityContextHolder.getContext().getAuthentication();
if (previousResult != null && previousResult.isAuthenticated()) {
lastestResult = lastestResult.toBuilder()
.authorities((a) -> a.addAll(previous.getAuthorities()))
.build();
}
// end::springSecurity[]
SecurityAssertions.assertThat(lastestResult).hasAuthorities("FACTOR_PASSWORD", "FACTOR_OTT");
SecurityContextHolder.clearContext();
}
}

View File

@ -0,0 +1,39 @@
package org.springframework.security.kt.docs.servlet.authentication.servletauthenticationauthentication
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers
import org.mockito.BDDMockito
import org.mockito.Mockito
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.SecurityAssertions
import org.springframework.security.authentication.TestingAuthenticationToken
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.authentication.ott.OneTimeTokenAuthentication
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.core.context.SecurityContextHolder
class CopyAuthoritiesTests {
@Test
fun toBuilderWhenApplyThenCopies() {
val previous: Authentication = UsernamePasswordAuthenticationToken("alice", "pass",
AuthorityUtils.createAuthorityList("FACTOR_PASSWORD"))
SecurityContextHolder.getContext().authentication = previous
var latest: Authentication = OneTimeTokenAuthentication("bob",
AuthorityUtils.createAuthorityList("FACTOR_OTT"))
val authenticationManager: AuthenticationManager = Mockito.mock(AuthenticationManager::class.java)
BDDMockito.given(authenticationManager.authenticate(ArgumentMatchers.any())).willReturn(latest)
val authenticationRequest: Authentication = TestingAuthenticationToken("user", "pass")
// tag::springSecurity[]
var latestResult: Authentication = authenticationManager.authenticate(authenticationRequest)
val previousResult = SecurityContextHolder.getContext().authentication;
if (previousResult?.isAuthenticated == true) {
latestResult = latestResult.toBuilder().authorities { a ->
a.addAll(previousResult.authorities)
}.build()
}
// end::springSecurity[]
SecurityAssertions.assertThat(latestResult).hasAuthorities("FACTOR_PASSWORD", "FACTOR_OTT")
SecurityContextHolder.clearContext()
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.security.oauth2.client;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
@ -25,6 +26,7 @@ import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@ -157,7 +159,7 @@ public final class OAuth2AuthorizeRequest {
private static Authentication createAuthentication(final String principalName) {
Assert.hasText(principalName, "principalName cannot be empty");
return new AbstractAuthenticationToken(null) {
return new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
@Override
public Object getCredentials() {

View File

@ -18,6 +18,8 @@ package org.springframework.security.oauth2.client.authentication;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@ -65,6 +67,14 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
this.setAuthenticated(true);
}
protected OAuth2AuthenticationToken(Builder<?> builder) {
super(builder);
Assert.notNull(builder.principal, "principal cannot be null");
Assert.hasText(builder.authorizedClientRegistrationId, "authorizedClientRegistrationId cannot be empty");
this.principal = builder.principal;
this.authorizedClientRegistrationId = builder.authorizedClientRegistrationId;
}
@Override
public OAuth2User getPrincipal() {
return this.principal;
@ -85,4 +95,53 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
return this.authorizedClientRegistrationId;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link OAuth2AuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private OAuth2User principal;
private String authorizedClientRegistrationId;
protected Builder(OAuth2AuthenticationToken token) {
super(token);
this.principal = token.principal;
this.authorizedClientRegistrationId = token.authorizedClientRegistrationId;
}
@Override
public B principal(@Nullable Object principal) {
Assert.isInstanceOf(OAuth2User.class, principal, "principal must be of type OAuth2User");
this.principal = (OAuth2User) principal;
return (B) this;
}
/**
* Use this
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration}
* {@code registrationId}.
* @param authorizedClientRegistrationId the registration id to use
* @return the {@link Builder} for further configurations
* @see OAuth2AuthenticationToken#getAuthorizedClientRegistrationId
*/
public B authorizedClientRegistrationId(String authorizedClientRegistrationId) {
this.authorizedClientRegistrationId = authorizedClientRegistrationId;
return (B) this;
}
@Override
public OAuth2AuthenticationToken build() {
return new OAuth2AuthenticationToken(this);
}
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.security.oauth2.client.web.reactive.function.client;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
@ -36,6 +37,7 @@ import org.springframework.http.HttpStatusCode;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
@ -551,7 +553,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
private static Authentication createAuthentication(final String principalName) {
Assert.hasText(principalName, "principalName cannot be empty");
return new AbstractAuthenticationToken(null) {
return new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
@Override
public Object getCredentials() {

View File

@ -18,12 +18,15 @@ package org.springframework.security.oauth2.client.authentication;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.assertThatIllegalArgumentException;
@ -82,4 +85,21 @@ public class OAuth2AuthenticationTokenTests {
assertThat(authentication.isAuthenticated()).isEqualTo(true);
}
@Test
public void toBuilderWhenApplyThenCopies() {
OAuth2AuthenticationToken factorOne = new OAuth2AuthenticationToken(TestOAuth2Users.create(),
AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice");
OAuth2AuthenticationToken factorTwo = new OAuth2AuthenticationToken(TestOAuth2Users.create(),
AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
OAuth2AuthenticationToken result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.authorizedClientRegistrationId(factorTwo.getAuthorizedClientRegistrationId())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(result.getAuthorizedClientRegistrationId()).isSameAs(factorTwo.getAuthorizedClientRegistrationId());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Collection;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@ -83,6 +85,15 @@ public abstract class AbstractOAuth2TokenAuthenticationToken<T extends OAuth2Tok
this.token = token;
}
protected AbstractOAuth2TokenAuthenticationToken(AbstractOAuth2TokenAuthenticationBuilder<T, ?> builder) {
super(builder);
Assert.notNull(builder.credentials, "token cannot be null");
Assert.notNull(builder.principal, "principal cannot be null");
this.principal = builder.principal;
this.credentials = builder.credentials;
this.token = builder.token;
}
@Override
public Object getPrincipal() {
return this.principal;
@ -106,4 +117,53 @@ public abstract class AbstractOAuth2TokenAuthenticationToken<T extends OAuth2Tok
*/
public abstract Map<String, Object> getTokenAttributes();
/**
* A builder for {@link AbstractOAuth2TokenAuthenticationToken} implementations
*
* @param <B>
* @since 7.0
*/
public abstract static class AbstractOAuth2TokenAuthenticationBuilder<T extends OAuth2Token, B extends AbstractOAuth2TokenAuthenticationBuilder<T, B>>
extends AbstractAuthenticationBuilder<B> {
private Object principal;
private Object credentials;
private T token;
protected AbstractOAuth2TokenAuthenticationBuilder(AbstractOAuth2TokenAuthenticationToken<T> token) {
super(token);
this.principal = token.getPrincipal();
this.credentials = token.getCredentials();
this.token = token.getToken();
}
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
@Override
public B credentials(@Nullable Object credentials) {
Assert.notNull(credentials, "credentials cannot be null");
this.credentials = credentials;
return (B) this;
}
/**
* The OAuth 2.0 Token to use
* @param token the token to use
* @return the {@link Builder} for further configurations
*/
public B token(T token) {
Assert.notNull(token, "token cannot be null");
this.token = token;
return (B) this;
}
}
}

View File

@ -21,6 +21,9 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
@ -56,9 +59,81 @@ public class BearerTokenAuthentication extends AbstractOAuth2TokenAuthentication
setAuthenticated(true);
}
protected BearerTokenAuthentication(Builder<?> builder) {
super(builder);
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(builder.attributes));
}
@Override
public Map<String, Object> getTokenAttributes() {
return this.attributes;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder preserving the concrete {@link Authentication} type
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>>
extends AbstractOAuth2TokenAuthenticationBuilder<OAuth2AccessToken, B> {
private Map<String, Object> attributes;
protected Builder(BearerTokenAuthentication token) {
super(token);
this.attributes = token.getTokenAttributes();
}
/**
* Use this principal. Must be of type {@link OAuth2AuthenticatedPrincipal}
* @param principal the principal to use
* @return the {@link Builder} for further configurations
*/
@Override
public B principal(@Nullable Object principal) {
Assert.isInstanceOf(OAuth2AuthenticatedPrincipal.class, principal,
"principal must be of type OAuth2AuthenticatedPrincipal");
this.attributes = ((OAuth2AuthenticatedPrincipal) principal).getAttributes();
return super.principal(principal);
}
/**
* A synonym for {@link #token(OAuth2AccessToken)}
* @param token the token to use
* @return the {@link Builder} for further configurations
*/
@Override
public B credentials(@Nullable Object token) {
Assert.isInstanceOf(OAuth2AccessToken.class, token, "token must be of type OAuth2AccessToken");
return token((OAuth2AccessToken) token);
}
/**
* Use this token. Must have a {@link OAuth2AccessToken#getTokenType()} as
* {@link OAuth2AccessToken.TokenType#BEARER}.
* @param token the token to use
* @return the {@link Builder} for further configurations
*/
@Override
public B token(OAuth2AccessToken token) {
Assert.isTrue(token.getTokenType() == OAuth2AccessToken.TokenType.BEARER, "token must be a bearer token");
super.credentials(token);
return super.token(token);
}
/**
* {@inheritDoc}
*/
@Override
public BearerTokenAuthentication build() {
return new BearerTokenAuthentication(this);
}
}
}

View File

@ -19,9 +19,13 @@ package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Collection;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* An implementation of an {@link AbstractOAuth2TokenAuthenticationToken} representing a
@ -71,6 +75,11 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
this.name = name;
}
protected JwtAuthenticationToken(Builder<?> builder) {
super(builder);
this.name = builder.name;
}
@Override
public Map<String, Object> getTokenAttributes() {
return this.getToken().getClaims();
@ -84,4 +93,74 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
return this.name;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder for {@link JwtAuthenticationToken} instances
*
* @since 7.0
* @see Authentication.Builder
*/
public static class Builder<B extends Builder<B>> extends AbstractOAuth2TokenAuthenticationBuilder<Jwt, B> {
private String name;
protected Builder(JwtAuthenticationToken token) {
super(token);
this.name = token.getName();
}
/**
* A synonym for {@link #token(Jwt)}
* @return the {@link Builder} for further configurations
*/
@Override
public B principal(@Nullable Object principal) {
Assert.isInstanceOf(Jwt.class, principal, "principal must be of type Jwt");
return token((Jwt) principal);
}
/**
* A synonym for {@link #token(Jwt)}
* @return the {@link Builder} for further configurations
*/
@Override
public B credentials(@Nullable Object credentials) {
Assert.isInstanceOf(Jwt.class, credentials, "credentials must be of type Jwt");
return token((Jwt) credentials);
}
/**
* Use this {@code token} as the token, principal, and credentials. Also sets the
* {@code name} to {@link Jwt#getSubject}.
* @param token the token to use
* @return the {@link Builder} for further configurations
*/
@Override
public B token(Jwt token) {
super.principal(token);
super.credentials(token);
return super.token(token).name(token.getSubject());
}
/**
* The name to use.
* @param name the name to use
* @return the {@link Builder} for further configurations
*/
public B name(String name) {
this.name = name;
return (B) this;
}
@Override
public JwtAuthenticationToken build() {
return new JwtAuthenticationToken(this);
}
}
}

View File

@ -180,6 +180,12 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
BearerTokenError error = BearerTokenErrors.invalidToken("Invalid bearer token");
throw new OAuth2AuthenticationException(error);
}
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
if (current != null && current.isAuthenticated()) {
authenticationResult = authenticationResult.toBuilder()
.authorities((a) -> a.addAll(current.getAuthorities()))
.build();
}
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authenticationResult);
this.securityContextHolderStrategy.setContext(context);

View File

@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minidev.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
@ -34,6 +35,7 @@ import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrinci
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -151,4 +153,24 @@ public class BearerTokenAuthenticationTests {
token.toString();
}
@Test
public void toBuilderWhenApplyThenCopies() {
BearerTokenAuthentication factorOne = new BearerTokenAuthentication(TestOAuth2AuthenticatedPrincipals.active(),
this.token, AuthorityUtils.createAuthorityList("FACTOR_ONE"));
BearerTokenAuthentication factorTwo = new BearerTokenAuthentication(
TestOAuth2AuthenticatedPrincipals.active((m) -> m.put("k", "v")),
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "nekot", Instant.now(),
Instant.now().plusSeconds(3600)),
AuthorityUtils.createAuthorityList("FACTOR_TWO"));
BearerTokenAuthentication authentication = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.token(factorTwo.getToken())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
assertThat(authentication.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(authentication.getToken()).isSameAs(factorTwo.getToken());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Collection;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -54,7 +55,7 @@ public class JwtAuthenticationTokenTests {
@Test
public void constructorWhenJwtIsNullThenThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new JwtAuthenticationToken(null))
assertThatIllegalArgumentException().isThrownBy(() -> new JwtAuthenticationToken((Jwt) null))
.withMessageContaining("token cannot be null");
}
@ -115,6 +116,23 @@ public class JwtAuthenticationTokenTests {
assertThat(new JwtAuthenticationToken(jwt).getName()).isNull();
}
@Test
public void toBuilderWhenApplyThenCopies() {
JwtAuthenticationToken factorOne = new JwtAuthenticationToken(builder().claim("c", "v").build(),
AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice");
JwtAuthenticationToken factorTwo = new JwtAuthenticationToken(builder().claim("d", "w").build(),
AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
JwtAuthenticationToken result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.name(factorTwo.getName())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(result.getName()).isSameAs(factorTwo.getName());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
private Jwt.Builder builder() {
return Jwt.withTokenValue("token").header("alg", JwsAlgorithms.RS256);
}

View File

@ -18,7 +18,9 @@ package org.springframework.security.oauth2.server.resource.web.authentication;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import jakarta.servlet.Filter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
@ -37,8 +39,11 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@ -240,6 +245,7 @@ public class BearerTokenAuthenticationFilterTests {
new BearerTokenAuthenticationFilter(this.authenticationManager));
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl());
given(strategy.getContext()).willReturn(new SecurityContextImpl());
filter.setSecurityContextHolderStrategy(strategy);
filter.doFilter(this.request, this.response, this.filterChain);
verify(strategy).setContext(any());
@ -339,6 +345,23 @@ public class BearerTokenAuthenticationFilterTests {
// @formatter:on
}
@Test
void authenticateWhenPreviousAuthenticationThenApplies() throws Exception {
Authentication first = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
Authentication second = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
Filter filter = addMocks(new BearerTokenAuthenticationFilter(this.authenticationManager));
given(this.bearerTokenResolver.resolve(this.request)).willReturn("token");
given(this.authenticationManager.authenticate(any())).willReturn(second);
SecurityContextHolder.getContext().setAuthentication(first);
filter.doFilter(this.request, this.response, this.filterChain);
Authentication result = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder.clearContext();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
private BearerTokenAuthenticationFilter addMocks(BearerTokenAuthenticationFilter filter) {
filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
filter.setBearerTokenResolver(this.bearerTokenResolver);

View File

@ -19,7 +19,11 @@ package org.springframework.security.saml2.provider.service.authentication;
import java.io.Serial;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.util.Assert;
/**
* An authentication based off of a SAML 2.0 Assertion
@ -53,6 +57,12 @@ public class Saml2AssertionAuthentication extends Saml2Authentication {
setAuthenticated(true);
}
protected Saml2AssertionAuthentication(Builder<?> builder) {
super(builder);
this.assertion = builder.assertion;
this.relyingPartyRegistrationId = builder.relyingPartyRegistrationId;
}
@Override
public Saml2ResponseAssertionAccessor getCredentials() {
return this.assertion;
@ -62,4 +72,59 @@ public class Saml2AssertionAuthentication extends Saml2Authentication {
return this.relyingPartyRegistrationId;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link Saml2AssertionAuthentication} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends Saml2Authentication.Builder<B> {
private Saml2ResponseAssertionAccessor assertion;
private String relyingPartyRegistrationId;
protected Builder(Saml2AssertionAuthentication token) {
super(token);
this.assertion = token.assertion;
this.relyingPartyRegistrationId = token.relyingPartyRegistrationId;
}
/**
* Use these credentials. They must be of type
* {@link Saml2ResponseAssertionAccessor}.
* @param credentials the credentials to use
* @return the {@link Builder} for further configurations
*/
@Override
public B credentials(@Nullable Object credentials) {
Assert.isInstanceOf(Saml2ResponseAssertionAccessor.class, credentials,
"credentials must be of type Saml2ResponseAssertionAccessor");
saml2Response(((Saml2ResponseAssertionAccessor) credentials).getResponseValue());
this.assertion = (Saml2ResponseAssertionAccessor) credentials;
return (B) this;
}
/**
* Use this registration id
* @param relyingPartyRegistrationId the
* {@link RelyingPartyRegistration#getRegistrationId} to use
* @return the {@link Builder} for further configurations
*/
public B relyingPartyRegistrationId(String relyingPartyRegistrationId) {
this.relyingPartyRegistrationId = relyingPartyRegistrationId;
return (B) this;
}
@Override
public Saml2AssertionAuthentication build() {
return new Saml2AssertionAuthentication(this);
}
}
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.saml2.provider.service.authentication;
import java.io.Serial;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
@ -69,6 +71,12 @@ public class Saml2Authentication extends AbstractAuthenticationToken {
setAuthenticated(true);
}
Saml2Authentication(Builder<?> builder) {
super(builder);
this.principal = builder.principal;
this.saml2Response = builder.saml2Response;
}
@Override
public Object getPrincipal() {
return this.principal;
@ -87,4 +95,29 @@ public class Saml2Authentication extends AbstractAuthenticationToken {
return getSaml2Response();
}
abstract static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private Object principal;
String saml2Response;
Builder(Saml2Authentication token) {
super(token);
this.principal = token.principal;
this.saml2Response = token.saml2Response;
}
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
void saml2Response(String saml2Response) {
this.saml2Response = saml2Response;
}
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.saml2.provider.service.authentication;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.AuthorityUtils;
import static org.assertj.core.api.Assertions.assertThat;
class Saml2AssertionAuthenticationTests {
@Test
void toBuilderWhenApplyThenCopies() {
Saml2ResponseAssertion.Builder prototype = Saml2ResponseAssertion.withResponseValue("response");
Saml2AssertionAuthentication factorOne = new Saml2AssertionAuthentication("alice",
prototype.nameId("alice").build(), AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice");
Saml2AssertionAuthentication factorTwo = new Saml2AssertionAuthentication("bob",
prototype.nameId("bob").build(), AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
Saml2AssertionAuthentication result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.credentials(factorTwo.getCredentials())
.relyingPartyRegistrationId(factorTwo.getRelyingPartyRegistrationId())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
assertThat(result.getRelyingPartyRegistrationId()).isSameAs(factorTwo.getRelyingPartyRegistrationId());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -248,6 +248,12 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
// return immediately as subclass has indicated that it hasn't completed
return;
}
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
if (current != null && current.isAuthenticated()) {
authenticationResult = authenticationResult.toBuilder()
.authorities((a) -> a.addAll(current.getAuthorities()))
.build();
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {

View File

@ -184,6 +184,12 @@ public class AuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
return;
}
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
if (current != null && current.isAuthenticated()) {
authenticationResult = authenticationResult.toBuilder()
.authorities((a) -> a.addAll(current.getAuthorities()))
.build();
}
HttpSession session = request.getSession(false);
if (session != null) {
request.changeSessionId();

View File

@ -204,6 +204,12 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
principal, credentials);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
if (current != null && current.isAuthenticated()) {
authenticationResult = authenticationResult.toBuilder()
.authorities((a) -> a.addAll(current.getAuthorities()))
.build();
}
successfulAuthentication(request, response, authenticationResult);
}
catch (AuthenticationException ex) {

View File

@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/**
* {@link org.springframework.security.core.Authentication} implementation for
@ -46,7 +47,7 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
* @param aCredentials The pre-authenticated credentials
*/
public PreAuthenticatedAuthenticationToken(Object aPrincipal, @Nullable Object aCredentials) {
super(null);
super((Collection<? extends GrantedAuthority>) null);
this.principal = aPrincipal;
this.credentials = aCredentials;
}
@ -66,6 +67,12 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
setAuthenticated(true);
}
protected PreAuthenticatedAuthenticationToken(Builder<?> builder) {
super(builder);
this.principal = builder.principal;
this.credentials = builder.credentials;
}
/**
* Get the credentials
*/
@ -82,4 +89,46 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
return this.principal;
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link PreAuthenticatedAuthenticationToken} instances
*
* @since 7.0
*/
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private Object principal;
private @Nullable Object credentials;
protected Builder(PreAuthenticatedAuthenticationToken token) {
super(token);
this.principal = token.principal;
this.credentials = token.credentials;
}
@Override
public B principal(@Nullable Object principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
return (B) this;
}
@Override
public B credentials(@Nullable Object credentials) {
this.credentials = credentials;
return (B) this;
}
@Override
public PreAuthenticatedAuthenticationToken build() {
return new PreAuthenticatedAuthenticationToken(this);
}
}
}

View File

@ -186,6 +186,10 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
if (authenticationIsRequired(username)) {
Authentication authResult = this.authenticationManager.authenticate(authRequest);
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
if (current != null && current.isAuthenticated()) {
authResult = authResult.toBuilder().authorities((a) -> a.addAll(current.getAuthorities())).build();
}
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);
this.securityContextHolderStrategy.setContext(context);

View File

@ -122,12 +122,26 @@ public class AuthenticationWebFilter implements WebFilter {
.flatMap((authenticationManager) -> authenticationManager.authenticate(token))
.switchIfEmpty(Mono
.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
.flatMap(this::applyCurrentAuthenication)
.flatMap(
(authentication) -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)))
.doOnError(AuthenticationException.class,
(ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage()), ex));
}
private Mono<Authentication> applyCurrentAuthenication(Authentication result) {
return ReactiveSecurityContextHolder.getContext().map((context) -> {
Authentication current = context.getAuthentication();
if (current == null) {
return result;
}
if (!current.isAuthenticated()) {
return result;
}
return result.toBuilder().authorities((a) -> a.addAll(current.getAuthorities())).build();
}).switchIfEmpty(Mono.just(result));
}
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
ServerWebExchange exchange = webFilterExchange.getExchange();
SecurityContextImpl securityContext = new SecurityContextImpl();

View File

@ -144,6 +144,7 @@ public class AuthenticationFilterTests {
this.authenticationConverter);
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl());
given(strategy.getContext()).willReturn(new SecurityContextImpl());
filter.setSecurityContextHolderStrategy(strategy);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
MockHttpServletResponse response = new MockHttpServletResponse();

View File

@ -18,6 +18,7 @@ package org.springframework.security.web.authentication.preauth;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
@ -73,4 +74,21 @@ public class PreAuthenticatedAuthenticationTokenTests {
.isTrue();
}
@Test
public void toBuilderWhenApplyThenCopies() {
PreAuthenticatedAuthenticationToken factorOne = new PreAuthenticatedAuthenticationToken("alice", "pass",
AuthorityUtils.createAuthorityList("FACTOR_ONE"));
PreAuthenticatedAuthenticationToken factorTwo = new PreAuthenticatedAuthenticationToken("bob", "ssap",
AuthorityUtils.createAuthorityList("FACTOR_TWO"));
PreAuthenticatedAuthenticationToken result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.credentials(factorTwo.getCredentials())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}

View File

@ -21,6 +21,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collection;
import java.util.Collections;
import jakarta.servlet.Filter;
@ -46,6 +47,7 @@ import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
@ -810,7 +812,7 @@ public class HttpSessionSecurityContextRepositoryTests {
private static class SomeTransientAuthentication extends AbstractAuthenticationToken {
SomeTransientAuthentication() {
super(null);
super((Collection<? extends GrantedAuthority>) null);
}
@Override
@ -840,7 +842,7 @@ public class HttpSessionSecurityContextRepositoryTests {
private static class SomeOtherTransientAuthentication extends AbstractAuthenticationToken {
SomeOtherTransientAuthentication() {
super(null);
super((Collection<? extends GrantedAuthority>) null);
}
@Override

View File

@ -48,6 +48,11 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
super.setAuthenticated(true);
}
private WebAuthnAuthentication(Builder<?> builder) {
super(builder);
this.principal = builder.principal;
}
@Override
public void setAuthenticated(boolean authenticated) {
Assert.isTrue(!authenticated, "Cannot set this token to trusted");
@ -69,4 +74,43 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
return this.principal.getName();
}
@Override
public Builder<?> toBuilder() {
return new Builder<>(this);
}
/**
* A builder of {@link WebAuthnAuthentication} instances
*
* @since 7.0
*/
public static final class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
private PublicKeyCredentialUserEntity principal;
private Builder(WebAuthnAuthentication token) {
super(token);
this.principal = token.principal;
}
/**
* Use this principal. It must be of type {@link PublicKeyCredentialUserEntity}
* @param principal the principal to use
* @return the {@link Builder} for further configurations
*/
@Override
public B principal(@Nullable Object principal) {
Assert.isInstanceOf(PublicKeyCredentialUserEntity.class, principal,
"principal must be of type PublicKeyCredentialUserEntity");
this.principal = (PublicKeyCredentialUserEntity) principal;
return (B) this;
}
@Override
public WebAuthnAuthentication build() {
return new WebAuthnAuthentication(this);
}
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.security.web.webauthn.authentication;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
@ -55,4 +56,21 @@ class WebAuthnAuthenticationTests {
assertThat(authentication.isAuthenticated()).isFalse();
}
@Test
void toBuilderWhenApplyThenCopies() {
PublicKeyCredentialUserEntity alice = TestPublicKeyCredentialUserEntities.userEntity().build();
WebAuthnAuthentication factorOne = new WebAuthnAuthentication(alice,
AuthorityUtils.createAuthorityList("FACTOR_ONE"));
PublicKeyCredentialUserEntity bob = TestPublicKeyCredentialUserEntities.userEntity().build();
WebAuthnAuthentication factorTwo = new WebAuthnAuthentication(bob,
AuthorityUtils.createAuthorityList("FACTOR_TWO"));
WebAuthnAuthentication result = factorOne.toBuilder()
.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
.principal(factorTwo.getPrincipal())
.build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}