From b09afb34cc721d3c74f690ce5f0faf3357e9c42d Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:47:34 -0600 Subject: [PATCH] Document Authentication.Builder The commit documents the new Authentication Builder interface and its usage in the security filter chain. Closes gh-17861 Closes gh-17862 --- .../servlet/authentication/architecture.adoc | 9 +++- .../authentication/passwords/basic.adoc | 2 + .../servlet/oauth2/resource-server/index.adoc | 2 + docs/modules/ROOT/pages/whats-new.adoc | 1 + .../CopyAuthoritiesTests.java | 41 +++++++++++++++++++ .../CopyAuthoritiesTests.kt | 39 ++++++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt diff --git a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc index 7d900f9476..89abe49407 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc @@ -140,6 +140,11 @@ In many cases, this is cleared after the user is authenticated, to ensure that i * `authorities`: The <> 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 <> is loaded and its +authorities are added to the returned <>. * The <> is set on the <>. 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. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc index 77de88f55e..4e24d44e57 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc @@ -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. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc index e30e55dca1..092a520073 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc @@ -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. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 1aa5803d1e..c09c26b019 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -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 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java new file mode 100644 index 0000000000..ca5de102fa --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java @@ -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(); + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt new file mode 100644 index 0000000000..af25a3a346 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt @@ -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() + } +}