Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c54a55ae8 | |||
| 01ff3b086a | |||
| 33e6f4bd3f | |||
| cdd4b36d37 | |||
| 7672f76fde | |||
| 3db4999da4 | |||
| a708d2f61b | |||
| e726c05e76 | |||
| a7039fb3e6 | |||
| 88ea668f47 | |||
| 03a5de1955 | |||
| 06cbea383e | |||
| e250236279 | |||
| ef76ba040d | |||
| d69af716c8 | |||
| 7e37aa2b75 | |||
| 07bfe371b4 | |||
| e12edf43f2 | |||
| a499e56b9b | |||
| 8c3f6ea0d4 | |||
| e17d85e460 | |||
| f12036db05 | |||
| 7b5c502a97 | |||
| 706b059ea8 | |||
| ea3b112bea | |||
| 1261c229a3 | |||
| 4501ae7d1c | |||
| 48112d3d74 | |||
| b8735abb63 | |||
| 68a02ff176 | |||
| f37a706d62 | |||
| 522c48b3b5 | |||
| 1dae9aa459 | |||
| bec25edeb0 | |||
| 4d43edfb20 | |||
| 9f9699f8a5 | |||
| d29c984881 | |||
| 1116241ee3 | |||
| d87dc9ae57 | |||
| ac556a45f9 | |||
| c8731a8dc0 | |||
| 3e3eeda560 | |||
| 58df50c3a3 | |||
| 79156b2387 | |||
| 6c2b2a7611 | |||
| 0fab34f359 | |||
| 08e5b375ac | |||
| f9c32afb6f | |||
| 3d61276a1a |
@@ -0,0 +1,76 @@
|
||||
name: Defer Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
defer-issues:
|
||||
name: Defer Issues
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Compute Version
|
||||
id: compute-version
|
||||
uses: spring-io/spring-release-actions/compute-version@0.0.3
|
||||
- name: Get Today's Release Version
|
||||
id: todays-release
|
||||
uses: spring-io/spring-release-actions/get-todays-release-version@0.0.3
|
||||
with:
|
||||
snapshot-version: ${{ steps.compute-version.outputs.version }}
|
||||
milestone-repository: ${{ github.repository }}
|
||||
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compute Next Version
|
||||
id: next-version
|
||||
uses: spring-io/spring-release-actions/compute-next-version@0.0.3
|
||||
with:
|
||||
version: ${{ steps.todays-release.outputs.release-version }}
|
||||
- name: Schedule Next Milestone
|
||||
uses: spring-io/spring-release-actions/schedule-milestone@0.0.3
|
||||
with:
|
||||
version: ${{ steps.next-version.outputs.version }}
|
||||
version-date: ${{ steps.next-version.outputs.version-date }}
|
||||
repository: ${{ github.repository }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Move Open Issues to Next Milestone
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CURRENT_MILESTONE: ${{ steps.todays-release.outputs.release-version }}
|
||||
NEXT_MILESTONE: ${{ steps.next-version.outputs.version }}
|
||||
run: |
|
||||
current_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
|
||||
--jq ".[] | select(.title == \"$CURRENT_MILESTONE\") | .number")
|
||||
if [ -z "$current_milestone_number" ]; then
|
||||
echo "No milestone found for $CURRENT_MILESTONE"
|
||||
exit 0
|
||||
fi
|
||||
next_milestone_number=$(gh api repos/${{ github.repository }}/milestones \
|
||||
--jq ".[] | select(.title == \"$NEXT_MILESTONE\") | .number")
|
||||
if [ -z "$next_milestone_number" ]; then
|
||||
echo "No milestone found for $NEXT_MILESTONE"
|
||||
exit 1
|
||||
fi
|
||||
echo "Moving open issues from milestone '$CURRENT_MILESTONE' (#$current_milestone_number) to '$NEXT_MILESTONE' (#$next_milestone_number)"
|
||||
page=1
|
||||
while true; do
|
||||
issues=$(gh api "repos/${{ github.repository }}/issues?milestone=$current_milestone_number&state=open&per_page=100&page=$page" \
|
||||
--jq '.[].number')
|
||||
if [ -z "$issues" ]; then
|
||||
break
|
||||
fi
|
||||
for issue in $issues; do
|
||||
echo "Moving issue/PR #$issue to milestone $NEXT_MILESTONE"
|
||||
gh api repos/${{ github.repository }}/issues/$issue \
|
||||
--method PATCH \
|
||||
--field milestone=$next_milestone_number \
|
||||
--silent
|
||||
done
|
||||
page=$((page + 1))
|
||||
done
|
||||
echo "Done."
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
|
||||
- name: Upload Docs
|
||||
id: upload
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
|
||||
+21
@@ -68,6 +68,27 @@ The https://github.com/spring-projects/spring-security/tree/docs-build[playbook
|
||||
|
||||
Discover more commands with `./gradlew tasks`.
|
||||
|
||||
=== IDE setup (IntelliJ)
|
||||
|
||||
No special steps are needed to open Spring Security in IntelliJ.
|
||||
|
||||
=== IDE setup (Eclipse and VS Code)
|
||||
|
||||
To work in Eclipse or VS Code, first generate Eclipse metadata so you can import the project into Eclipse or VS Code:
|
||||
|
||||
[indent=0]
|
||||
----
|
||||
./gradlew cleanEclipse eclipse
|
||||
----
|
||||
|
||||
If you have not built the project yet, run `./gradlew publishToMavenLocal` first so dependencies are resolved.
|
||||
|
||||
*VS Code:* Open the repository root as a folder. The repository includes `.vscode/settings.json` which disables automatic Gradle import so that the generated Eclipse metadata (`.classpath`, `.project`) is used. Do not use the Gradle for Java extension to import the project.
|
||||
|
||||
*Eclipse:* File → Import → General → Existing Projects into Workspace, then select the repository root.
|
||||
|
||||
The build uses a custom Eclipse plugin to work around Gradle dependency cycles that confuse IDE metadata generation. You may see Eclipse warnings about `xml-apis` from some test dependencies; those are excluded in the build and can be ignored.
|
||||
|
||||
== Getting Support
|
||||
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
|
||||
https://spring.io/support[Commercial support] is available too.
|
||||
|
||||
+5
-1
@@ -67,7 +67,11 @@ public final class AuthoritiesAuthorizationManager implements AuthorizationManag
|
||||
|
||||
private boolean isAuthorized(Authentication authentication, Collection<String> authorities) {
|
||||
for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) {
|
||||
if (authorities.contains(grantedAuthority.getAuthority())) {
|
||||
String authority = grantedAuthority.getAuthority();
|
||||
if (authority == null) {
|
||||
continue;
|
||||
}
|
||||
if (authorities.contains(authority)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -17,7 +17,9 @@
|
||||
package org.springframework.security.authorization;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -30,11 +32,13 @@ import org.springframework.security.core.Authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthoritiesAuthorizationManager}.
|
||||
*
|
||||
* @author Evgeniy Cheban
|
||||
* @author Khyojae
|
||||
*/
|
||||
class AuthoritiesAuthorizationManagerTests {
|
||||
|
||||
@@ -84,4 +88,20 @@ class AuthoritiesAuthorizationManagerTests {
|
||||
assertThat(manager.check(authentication, Collections.singleton("ROLE_USER")).isGranted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
// gh-18543
|
||||
void authorizeWhenAuthorityIsNullThenDoesNotThrowNullPointerException() {
|
||||
AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager();
|
||||
|
||||
Authentication authentication = new TestingAuthenticationToken("user", "password",
|
||||
Collections.singletonList(() -> null));
|
||||
|
||||
Collection<String> authoritiesContainsThrowsNPE = Set.of("ROLE_USER");
|
||||
|
||||
// must be Collection that throws NPE when .contains(null) is invoked
|
||||
// to replicate the issue in gh-18543
|
||||
assertThatNullPointerException().isThrownBy(() -> authoritiesContainsThrowsNPE.contains(null));
|
||||
assertThat(manager.authorize(() -> authentication, authoritiesContainsThrowsNPE).isGranted()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
plugins {
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.spring-module'
|
||||
|
||||
dependencies {
|
||||
@@ -6,6 +10,8 @@ dependencies {
|
||||
optional 'org.springframework:spring-core'
|
||||
optional 'org.bouncycastle:bcpkix-jdk18on'
|
||||
|
||||
testFixturesImplementation "org.assertj:assertj-core"
|
||||
|
||||
testImplementation "org.assertj:assertj-core"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params"
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.crypto.assertions;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link CryptoAssertions} and {@link CryptoStringAssert}.
|
||||
*/
|
||||
class CryptoAssertionsTests {
|
||||
|
||||
@Test
|
||||
void doesNotDecryptToPassesWhenSupplierThrows() {
|
||||
CryptoAssertions.assertThat((() -> {
|
||||
throw new RuntimeException("decrypt failed");
|
||||
})).doesNotDecryptTo("any");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotDecryptToPassesWhenResultDiffersFromExpected() {
|
||||
CryptoAssertions.assertThat(() -> "other").doesNotDecryptTo("plaintext");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotDecryptToFailsWhenResultEqualsExpected() {
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> CryptoAssertions.assertThat(() -> "plaintext").doesNotDecryptTo("plaintext"))
|
||||
.withMessageContaining("Expected supplier not to return <plaintext> but it did");
|
||||
}
|
||||
|
||||
}
|
||||
+8
-4
@@ -22,8 +22,9 @@ import java.security.interfaces.RSAPublicKey;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.crypto.assertions.CryptoAssertions;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
@@ -86,13 +87,15 @@ public class RsaSecretEncryptorTests {
|
||||
@Test
|
||||
public void roundTripWithMixedAlgorithm() {
|
||||
RsaSecretEncryptor oaep = new RsaSecretEncryptor(RsaAlgorithm.OAEP);
|
||||
assertThatIllegalStateException().isThrownBy(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")));
|
||||
CryptoAssertions.assertThat(() -> oaep.decrypt(this.encryptor.encrypt("encryptor")))
|
||||
.doesNotDecryptTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithMixedSalt() {
|
||||
RsaSecretEncryptor other = new RsaSecretEncryptor(this.encryptor.getPublicKey(), RsaAlgorithm.DEFAULT, "salt");
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.encryptor.decrypt(other.encrypt("encryptor")));
|
||||
CryptoAssertions.assertThat(() -> this.encryptor.decrypt(other.encrypt("encryptor")))
|
||||
.doesNotDecryptTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,7 +109,8 @@ public class RsaSecretEncryptorTests {
|
||||
public void publicKeyCannotDecrypt() {
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(this.encryptor.getPublicKey());
|
||||
assertThat(encryptor.canDecrypt()).as("Encryptor schould not be able to decrypt").isFalse();
|
||||
assertThatIllegalStateException().isThrownBy(() -> encryptor.decrypt(encryptor.encrypt("encryptor")));
|
||||
CryptoAssertions.assertThat(() -> encryptor.decrypt(encryptor.encrypt("encryptor")))
|
||||
.doesNotDecryptTo("encryptor");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+44
@@ -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.crypto.assertions;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* AssertJ entry point for crypto-related assertions. Use {@link #assertThat(Supplier)} to
|
||||
* assert on a {@link Supplier}<{@link String}> (e.g. a decryption lambda).
|
||||
* <p>
|
||||
* Example: <pre>
|
||||
* assertThat(() -> encryptor.decrypt(ciphertext)).doesNotDecryptTo("plaintext");
|
||||
* </pre>
|
||||
*/
|
||||
public final class CryptoAssertions {
|
||||
|
||||
private CryptoAssertions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create assertions for the given supplier (e.g. a decryption expression).
|
||||
* @param actual the supplier to assert on
|
||||
* @return assertion object with methods like
|
||||
* {@link CryptoStringAssert#doesNotDecryptTo(String)}
|
||||
*/
|
||||
public static CryptoStringAssert assertThat(Supplier<String> actual) {
|
||||
return new CryptoStringAssert(actual);
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.crypto.assertions;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
|
||||
/**
|
||||
* AssertJ assertion for {@link Supplier}<{@link String}>, supporting
|
||||
* decryption-related checks such as {@link #doesNotDecryptTo(String)}.
|
||||
*/
|
||||
public final class CryptoStringAssert extends AbstractObjectAssert<CryptoStringAssert, Supplier<String>> {
|
||||
|
||||
CryptoStringAssert(Supplier<String> actual) {
|
||||
super(actual, CryptoStringAssert.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that either the supplier throws an exception when invoked, or the value it
|
||||
* returns is not equal to the given string. Use this to assert that a decryption
|
||||
* attempt does not yield a specific plaintext (e.g. wrong key or tampered
|
||||
* ciphertext).
|
||||
* @param expected the value that the supplier must not return
|
||||
* @return this assertion for chaining
|
||||
*/
|
||||
public CryptoStringAssert doesNotDecryptTo(String expected) {
|
||||
isNotNull();
|
||||
try {
|
||||
String result = this.actual.get();
|
||||
if (Objects.equals(result, expected)) {
|
||||
failWithMessage("Expected supplier not to return <%s> but it did", expected);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Exception thrown: supplier does not "decrypt to" the expected value
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,7 @@ urls:
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.26/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
|
||||
@@ -39,17 +39,18 @@ This endpoint is referred to as a https://openid.net/specs/openid-connect-discov
|
||||
|
||||
When this property and these dependencies are used, Resource Server automatically configures itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
|
||||
|
||||
. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property.
|
||||
. Configure the validation strategy to query `jwks_url` for valid public keys.
|
||||
. Configure the validation strategy to validate each JWT's `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be receiving requests in order for Resource Server to successfully start up.
|
||||
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup fails.
|
||||
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder[`SupplierReactiveJwtDecoder`].
|
||||
Consider wrapping any <<webflux-oauth2resourceserver-decoder-bean,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
|
||||
====
|
||||
|
||||
=== Runtime Expectations
|
||||
@@ -85,7 +86,7 @@ From here, consider jumping to:
|
||||
[[webflux-oauth2resourceserver-jwt-jwkseturi]]
|
||||
=== Specifying the Authorization Server JWK Set Uri Directly
|
||||
|
||||
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, you can supply `jwk-set-uri` as well:
|
||||
If the authorization server does not support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, you can supply `jwk-set-uri` as well:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
|
||||
@@ -319,7 +319,7 @@ If you have trouble working out where a session is being created, you can add so
|
||||
[[appendix-faq-forbidden-csrf]]
|
||||
=== I get a 403 Forbidden when performing a POST. What is wrong?
|
||||
|
||||
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to https://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
|
||||
If an HTTP 403 Forbidden error is returned for HTTP POST, but it works for HTTP GET, the issue is most likely related to xref:features/exploits/csrf.adoc#csrf[CSRF]. Either provide the CSRF Token or disable CSRF protection (the latter is not recommended).
|
||||
|
||||
[[appendix-faq-no-security-on-forward]]
|
||||
=== I am forwarding a request to another URL by using the RequestDispatcher, but my security constraints are not being applied.
|
||||
|
||||
@@ -259,7 +259,7 @@ Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directives.ALL));
|
||||
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL));
|
||||
http
|
||||
.logout((logout) -> logout.addLogoutHandler(clearSiteData))
|
||||
----
|
||||
@@ -268,7 +268,7 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directives.ALL))
|
||||
val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directive.ALL))
|
||||
http {
|
||||
logout {
|
||||
addLogoutHandler(clearSiteData)
|
||||
|
||||
@@ -63,7 +63,9 @@ To use this tag, you must also have an instance of `WebInvocationPrivilegeEvalua
|
||||
If you are using the namespace, one is automatically registered.
|
||||
This is an instance of `DefaultWebInvocationPrivilegeEvaluator`, which creates a dummy web request for the supplied URL and invokes the security interceptor to see whether the request would succeed or fail.
|
||||
This lets you delegate to the access-control setup you defined by using `intercept-url` declarations within the `<http>` namespace configuration and saves having to duplicate the information (such as the required roles) within your JSPs.
|
||||
You can also combine this approach with a `method` attribute (supplying the HTTP method, such as `POST`) for a more specific match.
|
||||
|
||||
If you have xref:servlet/authorization/authorize-http-requests.adoc#match-by-httpmethod[method-based authorization rules], you should combine this approach with the `method` attribute (supplying the HTTP method, such as `POST`) to activate the intended method-based rule.
|
||||
For example, if you have a rule `.requestMatchers(POST, "/admin").hasRole("ADMIN")`, then you should do `<sec:authorize method="POST" url="/admin">` to match.
|
||||
|
||||
You can store the Boolean result of evaluating the tag (whether it grants or denies access) in a page context scope variable by setting the `var` attribute to the variable name, avoiding the need for duplicating and re-evaluating the condition at other points in the page.
|
||||
|
||||
|
||||
@@ -40,17 +40,20 @@ And that's it!
|
||||
|
||||
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
It achieves this through a deterministic discovery process it launches at the first request containing a JWT:
|
||||
|
||||
1. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property
|
||||
2. Query the `jwks_url` endpoint for supported algorithms
|
||||
3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found
|
||||
4. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
|
||||
One benefit of deferring this process is that Resource Server startup is not coupled to the authorization server's availability.
|
||||
|
||||
[NOTE]
|
||||
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
|
||||
====
|
||||
This deferral is managed by javadoc:org.springframework.security.oauth2.jwt.SupplierJwtDecoder[`SupplierJwtDecoder`].
|
||||
Consider wrapping any <<oauth2resourceserver-jwt-decoder,`JwtDecoder` `@Bean`>> you declare in order to preserve this behavior.
|
||||
====
|
||||
|
||||
=== Runtime Expectations
|
||||
|
||||
@@ -66,7 +69,7 @@ So long as this scheme is indicated, Resource Server will attempt to process the
|
||||
|
||||
Given a well-formed JWT, Resource Server will:
|
||||
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup or on first request, depending on configuration, and matched against the JWT
|
||||
2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`.
|
||||
|
||||
@@ -111,7 +114,7 @@ Ultimately, the returned `JwtAuthenticationToken` will be set on the xref:servle
|
||||
[[oauth2resourceserver-jwt-jwkseturi]]
|
||||
== Specifying the Authorization Server JWK Set Uri Directly
|
||||
|
||||
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to start up independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
|
||||
If the authorization server doesn't support any configuration endpoints, or if Resource Server must be able to initialize independently from the authorization server, then the `jwk-set-uri` can be supplied as well:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.11",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||
"@antora/collector-extension": "1.0.2",
|
||||
"@antora/collector-extension": "1.0.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.7",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=3.3.3
|
||||
version=6.5.8-SNAPSHOT
|
||||
version=6.5.9
|
||||
samplesBranch=6.5.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -5,7 +5,7 @@ io-spring-javaformat = "0.0.47"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.2.0"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-apache-maven-resolver = "1.9.25"
|
||||
org-apache-maven-resolver = "1.9.27"
|
||||
org-aspectj = "1.9.25.1"
|
||||
org-bouncycastle = "1.80"
|
||||
org-eclipse-jetty = "11.0.26"
|
||||
@@ -14,11 +14,11 @@ org-jetbrains-kotlinx = "1.10.2"
|
||||
org-mockito = "5.17.0"
|
||||
org-opensaml = "4.3.2"
|
||||
org-opensaml5 = "5.1.2"
|
||||
org-springframework = "6.2.15"
|
||||
org-springframework = "6.2.17"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.29"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.5"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.6"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.4"
|
||||
@@ -31,13 +31,13 @@ commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.4"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.14"
|
||||
io-mockk = "io.mockk:mockk:1.14.7"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.14"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2024.0.16"
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" }
|
||||
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
|
||||
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.13"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.14"
|
||||
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
@@ -57,7 +57,7 @@ org-apache-directory-server-apacheds-protocol-shared = { module = "org.apache.di
|
||||
org-apache-directory-server-apacheds-server-jndi = { module = "org.apache.directory.server:apacheds-server-jndi", version.ref = "org-apache-directory-server" }
|
||||
org-apache-directory-shared-shared-ldap = "org.apache.directory.shared:shared-ldap:0.9.15"
|
||||
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents:httpclient:4.5.14"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.12"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.14"
|
||||
org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" }
|
||||
org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" }
|
||||
@@ -71,7 +71,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
|
||||
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
|
||||
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
|
||||
org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.42.Final"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.44.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
|
||||
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
|
||||
|
||||
+3
-1
@@ -35,6 +35,7 @@ import org.springframework.util.Assert;
|
||||
* that it was for the user trying to log in. Please see the reference for details.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @author Andrey Litvitski
|
||||
* @since 6.5
|
||||
*/
|
||||
public final class CacheSaml2AuthenticationRequestRepository
|
||||
@@ -52,7 +53,8 @@ public final class CacheSaml2AuthenticationRequestRepository
|
||||
@Override
|
||||
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
|
||||
Assert.notNull(authenticationRequest, "authenticationRequest must not be null");
|
||||
String relayState = authenticationRequest.getRelayState();
|
||||
Assert.notNull(relayState, "relayState must not be null");
|
||||
this.cache.put(relayState, authenticationRequest);
|
||||
}
|
||||
|
||||
+9
-7
@@ -42,9 +42,10 @@ class CacheSaml2AuthenticationRequestRepositoryTests {
|
||||
|
||||
@Test
|
||||
void loadAuthenticationRequestWhenCachedThenReturns() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, "test");
|
||||
Saml2PostAuthenticationRequest authenticationRequest = TestSaml2PostAuthenticationRequests.create();
|
||||
String relayState = authenticationRequest.getRelayState();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, relayState);
|
||||
this.repository.saveAuthenticationRequest(authenticationRequest, request, null);
|
||||
assertThat(this.repository.loadAuthenticationRequest(request)).isEqualTo(authenticationRequest);
|
||||
this.repository.removeAuthenticationRequest(request, null);
|
||||
@@ -77,15 +78,16 @@ class CacheSaml2AuthenticationRequestRepositoryTests {
|
||||
CacheSaml2AuthenticationRequestRepository repository = new CacheSaml2AuthenticationRequestRepository();
|
||||
Cache cache = spy(new ConcurrentMapCache("requests"));
|
||||
repository.setCache(cache);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, "test");
|
||||
Saml2PostAuthenticationRequest authenticationRequest = TestSaml2PostAuthenticationRequests.create();
|
||||
String relayState = authenticationRequest.getRelayState();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter(Saml2ParameterNames.RELAY_STATE, relayState);
|
||||
repository.saveAuthenticationRequest(authenticationRequest, request, null);
|
||||
verify(cache).put(eq("test"), any());
|
||||
verify(cache).put(eq(relayState), any());
|
||||
repository.loadAuthenticationRequest(request);
|
||||
verify(cache).get("test", AbstractSaml2AuthenticationRequest.class);
|
||||
verify(cache).get(relayState, AbstractSaml2AuthenticationRequest.class);
|
||||
repository.removeAuthenticationRequest(request, null);
|
||||
verify(cache).evict("test");
|
||||
verify(cache).evict(relayState);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -91,6 +91,7 @@ public class CookieRequestCache implements RequestCache {
|
||||
.setServerPort(port)
|
||||
.setMethod(request.getMethod())
|
||||
.setLocales(Collections.list(request.getLocales()))
|
||||
.setParameters(request.getParameterMap())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+29
-1
@@ -58,10 +58,38 @@ public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrap
|
||||
|
||||
@Override
|
||||
public void addHeader(String name, String value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.addHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIntHeader(String name, int value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.addIntHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHeader(String name, String value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.setHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIntHeader(String name, int value) {
|
||||
checkContentLengthHeader(name, value);
|
||||
super.setIntHeader(name, value);
|
||||
}
|
||||
|
||||
private void checkContentLengthHeader(String name, int value) {
|
||||
if ("Content-Length".equalsIgnoreCase(name)) {
|
||||
setContentLength(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkContentLengthHeader(String name, String value) {
|
||||
if ("Content-Length".equalsIgnoreCase(name)) {
|
||||
setContentLength(Long.parseLong(value));
|
||||
}
|
||||
super.addHeader(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-6
@@ -56,11 +56,8 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
|
||||
throws IOException, JacksonException {
|
||||
List<AuthenticationExtensionsClientOutput<?>> outputs = new ArrayList<>();
|
||||
for (String key = parser.nextFieldName(); key != null; key = parser.nextFieldName()) {
|
||||
JsonToken startObject = parser.nextValue();
|
||||
if (startObject != JsonToken.START_OBJECT) {
|
||||
break;
|
||||
}
|
||||
if (CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
|
||||
JsonToken next = parser.nextToken();
|
||||
if (next == JsonToken.START_OBJECT && CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
|
||||
CredentialPropertiesOutput output = parser.readValueAs(CredentialPropertiesOutput.class);
|
||||
outputs.add(output);
|
||||
}
|
||||
@@ -68,7 +65,9 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping unknown extension with id " + key);
|
||||
}
|
||||
parser.nextValue();
|
||||
if (next.isStructStart()) {
|
||||
parser.skipChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -108,6 +108,21 @@ public class CookieRequestCacheTests {
|
||||
assertThat(savedRequest.getRedirectUrl()).isEqualTo(redirectUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRequestWhenRequestContainsSavedRequestCookieThenSavedRequestContainsRequestParameters() {
|
||||
CookieRequestCache cookieRequestCache = new CookieRequestCache();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie("https://abc.com/destination")));
|
||||
request.setParameter("single", "first");
|
||||
request.addParameter("multi", "second");
|
||||
request.addParameter("multi", "third");
|
||||
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
|
||||
assertThat(savedRequest).isNotNull();
|
||||
assertThat(savedRequest.getParameterValues("single")).containsExactly("first");
|
||||
assertThat(savedRequest.getParameterValues("multi")).containsExactly("second", "third");
|
||||
assertThat(savedRequest.getParameterMap()).containsKeys("single", "multi");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchingRequestWhenRequestDoesNotContainSavedRequestCookieThenReturnsNull() {
|
||||
CookieRequestCache cookieRequestCache = new CookieRequestCache();
|
||||
|
||||
+27
@@ -1006,6 +1006,33 @@ public class OnCommittedResponseWrapperTests {
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
int expected = 1234;
|
||||
this.response.addIntHeader("Content-Length", String.valueOf(expected).length());
|
||||
this.response.getWriter().write(expected);
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
int expected = 1234;
|
||||
this.response.setHeader("Content-Length", String.valueOf(String.valueOf(expected).length()));
|
||||
this.response.getWriter().write(expected);
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setIntHeaderContentLengthPrintWriterWriteStringCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
int expected = 1234;
|
||||
this.response.setIntHeader("Content-Length", String.valueOf(expected).length());
|
||||
this.response.getWriter().write(expected);
|
||||
assertThat(this.committed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bufferSizePrintWriterWriteCommits() throws Exception {
|
||||
givenGetWriterThenReturn();
|
||||
|
||||
@@ -122,6 +122,47 @@ class JacksonTests {
|
||||
assertThat(outputs).usingRecursiveComparison().isEqualTo(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenAppId() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"appid": false,
|
||||
"credProps": {
|
||||
"rk": false
|
||||
}
|
||||
}
|
||||
""";
|
||||
CredentialPropertiesOutput credProps = new CredentialPropertiesOutput(false);
|
||||
|
||||
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
|
||||
AuthenticationExtensionsClientOutputs.class);
|
||||
assertThat(outputs.getOutputs()).usingRecursiveFieldByFieldElementComparator().contains(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenUnknownExtension() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"unknownObject1": {
|
||||
"key": "value"
|
||||
},
|
||||
"unknownArray": [
|
||||
{ "key": "value1" },
|
||||
{ "key": "value2" }
|
||||
],
|
||||
"credProps": {
|
||||
"rk": false
|
||||
},
|
||||
"unknownObject2": {}
|
||||
}
|
||||
""";
|
||||
CredentialPropertiesOutput credProps = new CredentialPropertiesOutput(false);
|
||||
|
||||
AuthenticationExtensionsClientOutputs outputs = this.mapper.readValue(json,
|
||||
AuthenticationExtensionsClientOutputs.class);
|
||||
assertThat(outputs.getOutputs()).usingRecursiveFieldByFieldElementComparator().contains(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenFieldAfter() throws Exception {
|
||||
String json = """
|
||||
|
||||
Reference in New Issue
Block a user