1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Add Authentication.Builder

This commit adds a new default method to Authentication
for the purposes of creating a Builder based on the current
authentication, allowing other authentications to be
applied to it as a composite.

It also adds Builders for each one of the authentication
result classes.

Issue gh-17861
This commit is contained in:
Josh Cummings
2025-08-22 16:21:26 -06:00
parent eeb4574bb3
commit a201a2b862
27 changed files with 1016 additions and 1 deletions
@@ -20,6 +20,8 @@ import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
@@ -185,4 +187,36 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
return sb.toString();
}
protected abstract static class AbstractAuthenticationBuilder<A extends Authentication, B extends AbstractAuthenticationBuilder<A, B>>
implements Builder<A, B> {
private final Collection<GrantedAuthority> authorities = new HashSet<>();
protected AbstractAuthenticationBuilder() {
}
@Override
public B authorities(Consumer<Collection<GrantedAuthority>> authorities) {
authorities.accept(this.authorities);
return (B) this;
}
@Override
public A build() {
return build(this.authorities);
}
@Override
public B apply(Authentication token) {
Assert.isTrue(token.isAuthenticated(), "cannot mutate an unauthenticated token");
Assert.notNull(token.getPrincipal(), "principal cannot be null");
this.authorities.addAll(token.getAuthorities());
return (B) this;
}
protected abstract A build(Collection<GrantedAuthority> authorities);
}
}
@@ -18,7 +18,11 @@ package org.springframework.security.authentication;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/**
* Represents a remembered <code>Authentication</code>.
@@ -88,6 +92,11 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
return this.principal;
}
@Override
public Builder toBuilder() {
return new Builder().apply(this);
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) {
@@ -106,4 +115,42 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
return result;
}
/**
* A builder preserving the concrete {@link Authentication} type
*
* @since 7.0
*/
public static final class Builder extends AbstractAuthenticationBuilder<RememberMeAuthenticationToken, Builder> {
private @Nullable Integer keyHash;
private @Nullable Object principal;
private Builder() {
}
public Builder apply(RememberMeAuthenticationToken token) {
return super.apply(token).keyHash(token.getKeyHash()).principal(token.getPrincipal());
}
public Builder principal(Object principal) {
this.principal = principal;
return this;
}
public Builder keyHash(int keyHash) {
this.keyHash = keyHash;
return this;
}
@Override
protected RememberMeAuthenticationToken build(Collection<GrantedAuthority> authorities) {
Assert.notNull(this.keyHash, "keyHash cannot be null");
Assert.notNull(this.principal, "principal cannot be null");
return new RememberMeAuthenticationToken(this.keyHash, this.principal, authorities);
}
}
}
@@ -19,8 +19,12 @@ package org.springframework.security.authentication;
import java.util.Collection;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
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
@@ -71,4 +75,48 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
return this.principal;
}
@Override
public Builder toBuilder() {
return new Builder().apply(this);
}
/**
* A builder preserving the concrete {@link Authentication} type
*
* @since 7.0
*/
public static final class Builder extends AbstractAuthenticationBuilder<TestingAuthenticationToken, Builder> {
private @Nullable Object principal;
private @Nullable Object credentials;
private Builder() {
}
public Builder apply(TestingAuthenticationToken authentication) {
return super.apply(authentication).principal(authentication.getPrincipal())
.credentials(authentication.getCredentials());
}
public Builder principal(Object principal) {
this.principal = principal;
return this;
}
public Builder credentials(Object credentials) {
this.credentials = credentials;
return this;
}
@Override
protected TestingAuthenticationToken build(Collection<GrantedAuthority> authorities) {
Assert.notNull(this.principal, "principal cannot be null");
Assert.notNull(this.credentials, "credentials cannot be null");
return new TestingAuthenticationToken(this.principal, this.credentials, authorities);
}
}
}
@@ -20,6 +20,7 @@ import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
@@ -124,4 +125,47 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
this.credentials = null;
}
@Override
public Builder<?, ?> toBuilder() {
return new Builder<>().apply(this);
}
/**
* A builder preserving the concrete {@link Authentication} type
*
* @since 7.0
*/
public static class Builder<A extends UsernamePasswordAuthenticationToken, B extends Builder<A, B>>
extends AbstractAuthenticationBuilder<A, B> {
private @Nullable Object principal;
private @Nullable Object credentials;
protected Builder() {
}
public B apply(UsernamePasswordAuthenticationToken authentication) {
return super.apply(authentication).principal(authentication.getPrincipal())
.credentials(authentication.getCredentials());
}
public B principal(Object principal) {
this.principal = principal;
return (B) this;
}
public B credentials(@Nullable Object credentials) {
this.credentials = credentials;
return (B) this;
}
@Override
protected A build(Collection<GrantedAuthority> authorities) {
Assert.notNull(this.principal, "principal cannot be null");
return (A) new UsernamePasswordAuthenticationToken(this.principal, this.credentials, authorities);
}
}
}
@@ -16,6 +16,7 @@
package org.springframework.security.authentication.jaas;
import java.util.Collection;
import java.util.List;
import javax.security.auth.login.LoginContext;
@@ -23,7 +24,9 @@ import javax.security.auth.login.LoginContext;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/**
* UsernamePasswordAuthenticationToken extension to carry the Jaas LoginContext that the
@@ -52,4 +55,47 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
return this.loginContext;
}
@Override
public Builder toBuilder() {
return new Builder().apply(this);
}
/**
* A builder preserving the concrete {@link Authentication} type
*
* @since 7.0
*/
public static final class Builder
extends UsernamePasswordAuthenticationToken.Builder<JaasAuthenticationToken, Builder> {
private @Nullable LoginContext loginContext;
private Builder() {
}
public Builder apply(JaasAuthenticationToken authentication) {
return super.apply(authentication).loginContext(authentication.getLoginContext());
}
/**
* Use this {@link LoginContext}
* @param loginContext the {@link LoginContext} to use
* @return the {@link Builder} for further configuration
*/
public Builder loginContext(LoginContext loginContext) {
this.loginContext = loginContext;
return this;
}
@Override
protected JaasAuthenticationToken build(Collection<GrantedAuthority> authorities) {
UsernamePasswordAuthenticationToken token = super.build(authorities);
Assert.notNull(this.loginContext, "loginContext cannot be null");
return new JaasAuthenticationToken(token.getPrincipal(), token.getCredentials(),
(List<GrantedAuthority>) token.getAuthorities(), this.loginContext);
}
}
}
@@ -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
@@ -53,4 +54,45 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
return null;
}
@Override
public Builder toBuilder() {
return new Builder().apply(this);
}
/**
* A builder for constructing a {@link OneTimeTokenAuthentication} instance
*/
public static final class Builder extends AbstractAuthenticationBuilder<OneTimeTokenAuthentication, Builder> {
private @Nullable Object principal;
private Builder() {
}
/**
* Apply this {@link OneTimeTokenAuthentication}
* @return the {@link Builder} for further configuration
*/
public Builder apply(OneTimeTokenAuthentication authentication) {
return super.apply(authentication).principal(authentication.principal);
}
/**
* Use this principal
* @return the {@link Builder} for further configuration
*/
public Builder principal(Object principal) {
this.principal = principal;
return this;
}
@Override
protected OneTimeTokenAuthentication build(Collection<GrantedAuthority> authorities) {
Assert.notNull(this.principal, "principal cannot be null");
return new OneTimeTokenAuthentication(this.principal, authorities);
}
}
}
@@ -16,14 +16,17 @@
package org.springframework.security.core;
import java.io.Serial;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert;
/**
* Represents the token for an authentication request or for an authenticated principal
@@ -54,6 +57,9 @@ import org.springframework.security.core.context.SecurityContextHolder;
*/
public interface Authentication extends Principal, Serializable {
@Serial
long serialVersionUID = -3884394378624019849L;
/**
* Set by an <code>AuthenticationManager</code> to indicate the authorities that the
* principal has been granted. Note that classes should not rely on this value as
@@ -64,7 +70,7 @@ public interface Authentication extends Principal, Serializable {
* instance.
* </p>
* @return the authorities granted to the principal, or an empty collection if the
* token has not been authenticated. Never null.
* token has not been authenticated. Never null.Saml2AssertAu
*/
Collection<? extends GrantedAuthority> getAuthorities();
@@ -136,4 +142,50 @@ public interface Authentication extends Principal, Serializable {
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
/**
* Return an {@link Builder} based on this instance
* @return an {@link Builder} for building a new {@link Authentication} based on this
* instance
* @since 7.0
*/
default Builder<?, ?> toBuilder() {
return new NoopAuthenticationBuilder<>(this);
}
/**
* A builder based on a given {@link Authentication} instance
*
* @param <A> the type of {@link Authentication}
* @author Josh Cummings
* @since 7.0
*/
interface Builder<A extends Authentication, B extends Builder<A, B>> {
/**
* Apply this {@link Authentication} to the builder.
* <p>
* By default, this method adds the authorities from {@code authentication} to
* this builder
* @return the {@link Builder} for further configuration
*/
default B apply(Authentication authentication) {
Assert.isTrue(authentication.isAuthenticated(), "cannot apply an unauthenticated token");
return authorities((a) -> a.addAll(authentication.getAuthorities()));
}
/**
* Apply these authorities to the builder.
* @param authorities the authorities to apply
* @return the {@link Builder} for further configuration
*/
B authorities(Consumer<Collection<GrantedAuthority>> authorities);
/**
* Build an {@link Authentication} instance
* @return the {@link Authentication} instance
*/
A build();
}
}
@@ -0,0 +1,53 @@
/*
* 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.util.Collection;
import java.util.function.Consumer;
import org.springframework.util.Assert;
/**
* An adapter implementation of {@link Authentication.Builder} that provides a no-op
* implementation for the principal, credentials, and authorities
*
* @param <A> the type of {@link Authentication}
* @author Josh Cummings
* @since 7.0
*/
class NoopAuthenticationBuilder<A extends Authentication>
implements Authentication.Builder<A, NoopAuthenticationBuilder<A>> {
private A original;
NoopAuthenticationBuilder(A authentication) {
Assert.isTrue(authentication.isAuthenticated(), "cannot mutate an unauthenticated token");
Assert.notNull(authentication.getPrincipal(), "principal cannot be null");
this.original = authentication;
}
@Override
public NoopAuthenticationBuilder<A> authorities(Consumer<Collection<GrantedAuthority>> authorities) {
return this;
}
@Override
public A build() {
return this.original;
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authentication;
import java.util.Collection;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AbstractAuthenticationToken.AbstractAuthenticationBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
class AbstractAuthenticationBuilderTests {
@Test
void applyWhenUnauthenticatedThenErrors() {
TestAbstractAuthenticationBuilder builder = new TestAbstractAuthenticationBuilder();
TestingAuthenticationToken unauthenticated = new TestingAuthenticationToken("user", "password");
assertThatIllegalArgumentException().isThrownBy(() -> builder.apply(unauthenticated));
}
@Test
void applyWhenAuthoritiesThenAdds() {
TestAbstractAuthenticationBuilder builder = new TestAbstractAuthenticationBuilder();
TestingAuthenticationToken factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
Authentication result = builder.apply(factorOne).apply(factorTwo).build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
private static final class TestAbstractAuthenticationBuilder
extends AbstractAuthenticationBuilder<Authentication, TestAbstractAuthenticationBuilder> {
@Override
protected Authentication build(Collection<GrantedAuthority> authorities) {
return new TestingAuthenticationToken("user", "password", authorities);
}
}
}
@@ -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,17 @@ 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().apply(factorTwo).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");
}
}
@@ -16,8 +16,11 @@
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;
@@ -85,4 +88,17 @@ 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"));
Authentication authentication = factorOne.toBuilder().apply(factorTwo).build();
Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
assertThat(authentication.getPrincipal()).isEqualTo("bob");
assertThat(authentication.getCredentials()).isEqualTo("ssap");
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}
@@ -0,0 +1,46 @@
/*
* 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().apply(factorTwo).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");
}
}
@@ -0,0 +1,41 @@
/*
* 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().apply(factorTwo).build();
Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
}