Merge branch '6.5.x'

This commit is contained in:
Josh Cummings 2025-05-06 16:45:14 -06:00
commit 9c357984d7
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
7 changed files with 495 additions and 66 deletions

View File

@ -0,0 +1,7 @@
= ACL
== Favor `AclPermissionEvaluator`
`AclEntryVoter`, `AclEntryAfterInvocationProvider`, and `AclPermissionEvaluator` provide the same service, plugged into different Spring Security APIs. Now that `AccessDecisionVoter` and `AfterInvocationProvider` are both deprecated, the corresponding ACL plugins are obsolete.
As such, begin using `AclPermissionEvaluator` before updating to Spring Security 7.

View File

@ -1,68 +1,2 @@
= Authentication Changes
== Opaque Token Credentials Will Be Encoded For You
In order to comply more closely with the Introspection RFC, Spring Security's opaque token support will encode the client id and secret before creating the authorization header.
This change means you will no longer have to encode the client id and secret yourself.
If your client id or secret contain URL-unsafe characters, then you can prepare yourself for this change by doing the following:
=== Replace Usage of `introspectionClientCredentials`
Since Spring Security can now do the encoding for you, replace xref:servlet/oauth2/resource-server/opaque-token.adoc#oauth2resourceserver-opaque-introspectionuri-dsl[using `introspectionClientCredentials`] with publishing the following `@Bean`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpaqueTokenIntrospector introspector() {
return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
.clientId(unencodedClientId).clientSecret(unencodedClientSecret).build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun introspector(): OpaqueTokenIntrospector {
return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
.clientId(unencodedClientId).clientSecret(unencodedClientSecret).build()
}
----
======
The above will be the default in 7.0.
If this setting gives you trouble or you cannot apply it for now, you can use the `RestOperations` constructor instead:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpaqueTokenIntrospector introspector() {
RestTemplate rest = new RestTemplate();
rest.addInterceptor(new BasicAuthenticationInterceptor(encodedClientId, encodedClientSecret));
return new SpringOpaqueTokenIntrospector(introspectionUri, rest);
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun introspector(): OpaqueTokenIntrospector {
val rest = RestTemplate()
rest.addInterceptor(BasicAuthenticationInterceptor(encodedClientId, encodedClientSecret))
return SpringOpaqueTokenIntrospector(introspectionUri, rest)
}
----
======

View File

@ -22,3 +22,82 @@ public void doSomething(Long id) {
You must compile with `-parameters` to ensure that the parameter names are available at runtime.
For more information about this, please visit the https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#core-container[Upgrading to Spring Framework 6.1 page].
=== Favor `AnnotationTemplateExpressionDefaults` over `PrePostTemplateDefaults`
In Spring Security 7, `AnnotationTemplateExpressionDefaults` will be included by default.
If you are customizing `PrePostTemplateDefaults` or simply want to see how your application responds to `AnnotationTemplateExpressionDefaults`, you can publish an `AnnotationTemplateExpressionDefaults` bean instead of a `PrePostTemplateDefaults` method:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
return new AnnotationTemplateExpressionDefaults();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
companion object {
@Bean
fun templateExpressionDefaults() = AnnotationTemplateExpressionDefaults()
}
----
Xml::
+
[source,xml,role="secondary"]
----
<b:bean id="templateExpressionDefaults" class="org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults"/>
----
======
==== I Am Publishing an AuthorizationAdvisor Bean
If you are publishing an `AuthorizationAdvisor` bean, like `AuthorizationManagerBeforeMethodInterceptor`, `AuthorizationManagerAfterMethodInterceptor`, `PreFilterAuthorizationMethodInterceptor`, or `PostFilterAuthorizationMethodInterceptor`, you can do the same by calling `setTemplateDefaults` with an `AnnotationTemplateExpressionDefaults` instance instead:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
@Role(BeanDescription.ROLE_INFRASTRUCTURE)
static Advisor preFilter() {
PreFilterAuthorizationMethodInterceptor interceptor = new PreFilterAuthorizationMethodInterceptor();
interceptor.setTemplateDefaults(new AnnotationTemplateExpressionDefaults());
return interceptor;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
companion object {
@Bean
@Role(BeanDescription.ROLE_INFRASTRUCTURE)
fun preFilter(): Advisor {
val interceptor = PreFilterAuthorizationMethodInterceptor()
interceptor.setTemplateDefaults(AnnotationTemplateExpressionDefaults)
return interceptor
}
}
----
======
=== Publish `AuthorizationAdvisor` instances instead of adding them in a `Customizer<AuthorizationAdvisorProxyFactory>`
While the ability to customize the `AuthorizationAdvisorProxyFactory` instance will remain in Spring Security 7, the ability to add advisors will be removed in favor of picking up published `AuthorizationAdvisor` beans.
If you are not calling `AuthorizationAdvisorProxyFactory#setAdvisors` or `AuthorizationAdvisorProxyFactory#addAdvisor`, you need do nothing.
If you are, publish the `AuthorizationAdvisor` bean instead and Spring Security will pick it up and apply it automatically.

View File

@ -123,3 +123,60 @@ In versions prior to 6.2, if you had a xref:servlet/configuration/java.adoc#jc-c
However, starting from version 6.2, this method is deprecated and will be removed in 7.0 because it will no longer be possible to chain configurations using `.and()` once `.and()` is removed (see https://github.com/spring-projects/spring-security/issues/13067).
Instead, it is recommended to use the new `.with(...)` method.
For more information about how to use `.with(...)` please refer to the xref:servlet/configuration/java.adoc#jc-custom-dsls[Custom DSLs section].
== Use `dispatcherTypeMatchers` instead of `shouldFilterAllDispatcherTypes`
If you are permitting the ERROR dispatch, you may be using `shouldFilterAllDispatcherTypes(false)` in the `auhorizeHttpRequests` DSL:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
http
.authorizeHttpRequests((authorize) -> authorize
.shouldFilterAllDispatcherTypes(false)
// ...
)
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
http {
authorizeHttpRequests {
shouldFilterAllDispatcherTypes = false
// ...
}
}
----
======
In preparation for 7, change this to use `dispatcherTypeMatchers`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
http
.authorizHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
// ...
)
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
http {
authorizeHttpRequests {
authorize(new DispatcherTypeRequestMatcher(DispatcherType.ERROR), permitAll())
}
}
----
======

View File

@ -170,3 +170,70 @@ fun jwtDecoder(): JwtDecoder {
<2> - specify the list of validators you need, excluding `JwtTypeValidator`
For additional guidance, please see the xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-validation[JwtDecoder Validators] section in the reference.
== Opaque Token Credentials Will Be Encoded For You
In order to comply more closely with the Introspection RFC, Spring Security's opaque token support will encode the client id and secret before creating the authorization header.
This change means you will no longer have to encode the client id and secret yourself.
If your client id or secret contain URL-unsafe characters, then you can prepare yourself for this change by doing the following:
=== Replace Usage of `introspectionClientCredentials`
Since Spring Security can now do the encoding for you, replace xref:servlet/oauth2/resource-server/opaque-token.adoc#oauth2resourceserver-opaque-introspectionuri-dsl[using `introspectionClientCredentials`] with publishing the following `@Bean`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpaqueTokenIntrospector introspector() {
return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
.clientId(unencodedClientId).clientSecret(unencodedClientSecret).build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun introspector(): OpaqueTokenIntrospector {
return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
.clientId(unencodedClientId).clientSecret(unencodedClientSecret).build()
}
----
======
The above will be the default in 7.0.
If this setting gives you trouble or you cannot apply it for now, you can use the `RestOperations` constructor instead:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpaqueTokenIntrospector introspector() {
RestTemplate rest = new RestTemplate();
rest.addInterceptor(new BasicAuthenticationInterceptor(encodedClientId, encodedClientSecret));
return new SpringOpaqueTokenIntrospector(introspectionUri, rest);
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun introspector(): OpaqueTokenIntrospector {
val rest = RestTemplate()
rest.addInterceptor(BasicAuthenticationInterceptor(encodedClientId, encodedClientSecret))
return SpringOpaqueTokenIntrospector(introspectionUri, rest)
}
----
======

View File

@ -1,5 +1,17 @@
= Saml 2.0 Migrations
== Use OpenSAML 5 By Default
OpenSAML 4.x is no longer supported by the OpenSAML team.
As such, Spring Security will default to using its `OpenSaml5` components in all cases.
If you want to see how well your application will respond to this, do the following:
1. Update your OpenSAML dependencies to 5.x
2. If you are constructing an `OpenSaml4XXX` Spring Security component, change it to `OpenSaml5`.
If you cannot opt-in, then add the `opensaml-saml-api` and `opensaml-saml-impl` 4.x dependencies and exclude the 5.x dependencies from `spring-security-saml2-service-provider`.
== Continue Filter Chain When No Relying Party Found
In Spring Security 6, `Saml2WebSsoAuthenticationFilter` throws an exception when the request URI matches, but no relying party registration is found.
@ -163,3 +175,230 @@ val responseValidator = ResponseValidator.withDefaults { responseToken: Response
provider.setResponseValidator(responseValidator)
----
======
== `RelyingPartyRegistration` Improvements
`RelyingPartyRegistration` links metadata from a relying party to metadata from an asserting party.
To prepare for some improvements to the API, please take the following steps:
1. If you are mutating a registration by using `RelyingPartyRegistration#withRelyingPartyRegistration`, instead call `RelyingPartyRegistration#mutate`
2. If you are providing or retrieving `AssertingPartyDetails`, use `getAssertingPartyMetadata` or `withAssertingPartyMetadata` instead.
== `OpenSaml5AuthenticationProvider` Improvements
Spring Security 7 will remove a handful of static factories from `OpenSaml5AuthenticationProvider` in favor of inner classes.
These inner classes simplify customization of the response validator, the assertion validator, and the response authentication converter.
=== Response Validation
Instead of doing:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
saml2.setResponseValidator((responseToken) -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
.andThen((result) -> result
.concat(myCustomValidator.convert(responseToken))
));
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
saml2.setResponseValidator { responseToken -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
.andThen { result -> result
.concat(myCustomValidator.convert(responseToken))
}
}
return saml2
}
----
======
use `OpenSaml5AuthenticationProvider.ResponseValidator`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator));
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator))
return saml2
}
----
======
=== Assertion Validation
Instead of doing:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
.createDefaultAssertionValidatorWithParameters(assertionToken -> {
Map<String, Object> params = new HashMap<>();
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
// ... other validation parameters
return new ValidationContext(params);
})
);
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
.createDefaultAssertionValidatorWithParameters { ->
val params = HashMap<String, Object>()
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis())
// ... other validation parameters
return ValidationContext(params)
}
)
return saml2
}
----
======
use `OpenSaml5AuthenticationProvider.AssertionValidator`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
Duration tenMinutes = Duration.ofMinutes(10);
authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build());
return saml2;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
val saml2 = OpenSaml5AuthenticationProvider()
val tenMinutes = Duration.ofMinutes(10)
authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build())
return saml2
}
----
======
== Response Authentication Converter
Instead of doing:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
Converter<ResponseToken, Saml2Authentication> authenticationConverter() {
return (responseToken) -> {
Saml2Authentication authentication = OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter()
.convert(responseToken);
// ... work with OpenSAML's Assertion object to extract the principal
return new Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities());
};
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun authenticationConverter(): Converter<ResponseToken, Saml2Authentication> {
return { responseToken ->
val authentication =
OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter().convert(responseToken)
// ... work with OpenSAML's Assertion object to extract the principal
return Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities())
}
}
----
======
use `OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
ResponseAuthenticationConverter authenticationConverter() {
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
authenticationConverter.setPrincipalNameConverter((assertion) -> {
// ... work with OpenSAML's Assertion object to extract the principal
});
return authenticationConverter;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
val authenticationConverter = ResponseAuthenticationConverter()
authenticationConverter.setPrincipalNameConverter { assertion ->
// ... work with OpenSAML's Assertion object to extract the principal
}
return authenticationConverter
}
----
======

View File

@ -521,3 +521,49 @@ Xml::
=====
If you have several circumstances where HTTP is needed, consider using `OrRequestMatcher` to combine them into a single `RequestMatcher` instance.
=====
== Use `setCookieCustomizer` instead of individual setters
In favor of a simpler API, `CookieCsrfTokenRepository#setCookieCustomizer` allows you to change any aspect of the cookie, replacing `setCookieHttpOnly`, `setCookieMaxAge`, `setSecure`, and `setCookieDomain`.
Change this:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
CookeCsrfTokenRepository csrf = CookeCsrfTokenRepository.withHttpOnlyFalse();
csrf.setCookieMaxAge(86400)
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
val csrf = CookeCsrfTokenRepository.withHttpOnlyFalse()
csrf.setCookieMaxAge(86400)
----
======
to this:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
CookeCsrfTokenRepository csrf = CookeCsrfTokenRepository.withHttpOnlyFalse();
csrf.setCookieCustomizer((c) -> c.maxAge(86400));
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
val csrf = CookeCsrfTokenRepository.withHttpOnlyFalse()
csrf.setCookieCustomizer { -> it.maxAge(86400) }
----
======