Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bd793ffe6 | |||
| a2c0ac112b | |||
| ea6e7ab78f | |||
| 01ff3b086a | |||
| e8cb0ef541 | |||
| 33e6f4bd3f | |||
| 524ae92f6b | |||
| 47146f375b | |||
| e7080e8c7c | |||
| c348a7aa46 | |||
| f227934749 | |||
| e645aef3be | |||
| b238632fdc | |||
| 3f05f4d30c | |||
| cdd4b36d37 | |||
| 7672f76fde | |||
| 3db4999da4 | |||
| 3813627a15 | |||
| 9616e6b640 | |||
| a708d2f61b | |||
| ae827b6e1b | |||
| 65bf54d842 | |||
| 7f75fd611e | |||
| a013bfaaec | |||
| e726c05e76 | |||
| a7039fb3e6 | |||
| 88ea668f47 | |||
| 2c1c50ddca | |||
| 03a5de1955 | |||
| 91167adaa8 | |||
| 06cbea383e | |||
| e250236279 | |||
| 5b4fc73878 | |||
| ef76ba040d | |||
| 763a8d4691 | |||
| d69af716c8 | |||
| 1906075b0c | |||
| e8e0da1ec6 | |||
| 2f81d2d99e | |||
| 6cf4a5eed9 | |||
| 26937bf06c | |||
| 7e37aa2b75 | |||
| 8e8e1a80a9 | |||
| 3110c9074f | |||
| 07bfe371b4 | |||
| c29775a79e | |||
| bc96812461 | |||
| 7d9c2ce9d7 | |||
| e12edf43f2 | |||
| ca6dccf8d7 | |||
| a499e56b9b | |||
| 8c3f6ea0d4 | |||
| 40682415ba | |||
| 9893048ec9 | |||
| e17d85e460 | |||
| 4f97217f68 | |||
| fdaa883fb7 | |||
| f12036db05 | |||
| fbd9880a33 | |||
| 5e38c2aa88 | |||
| 7b5c502a97 | |||
| 57434fc597 | |||
| 20a7f96062 | |||
| 706b059ea8 | |||
| 7c49e0b457 | |||
| 04b270a0a3 | |||
| ea3b112bea | |||
| 17776e4738 | |||
| 1261c229a3 | |||
| 9ce2d76508 | |||
| fb84e24893 | |||
| 1575610d49 | |||
| 3a14745d92 | |||
| c29af014f4 | |||
| 4501ae7d1c | |||
| 48112d3d74 | |||
| b8735abb63 | |||
| 7c3c8bbdcb | |||
| 731848d5d3 | |||
| 68a02ff176 | |||
| ee97c83042 | |||
| ba12f5e6d0 | |||
| f37a706d62 | |||
| b48967eebc | |||
| 522c48b3b5 | |||
| 6898de8003 | |||
| 1dae9aa459 | |||
| 73ee893d98 | |||
| bec25edeb0 | |||
| 4d43edfb20 | |||
| 9f9699f8a5 | |||
| 311235f39e | |||
| fec988c82d | |||
| 17b434c1c1 | |||
| 0bb65411be | |||
| d29c984881 | |||
| 151bcf3b0b | |||
| 1116241ee3 | |||
| d87dc9ae57 | |||
| 2eb948d9b5 | |||
| f2aef5168c | |||
| ac556a45f9 | |||
| c8731a8dc0 | |||
| a4a6e9124c | |||
| b21159f453 | |||
| 6f7c8cb352 | |||
| 5973a66bb1 | |||
| 3e3eeda560 | |||
| e2486a2590 | |||
| 3c55f057b1 | |||
| 6d2a414022 | |||
| 58df50c3a3 | |||
| 79156b2387 | |||
| 3abb69d5a9 | |||
| 6c2b2a7611 | |||
| 0fab34f359 | |||
| c0da8b390b | |||
| 08e5b375ac | |||
| f9c32afb6f | |||
| 3d61276a1a |
@@ -17,7 +17,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
default-publish-milestones-central: true
|
||||
@@ -38,14 +38,14 @@ jobs:
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
perform-release:
|
||||
name: Perform Release
|
||||
needs: [ deploy-artifacts, deploy-schema ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
|
||||
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
|
||||
@@ -61,6 +61,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@729fed56d42122f88583aff1be35c0800b7d77e9 # v1.0.14
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-3
@@ -40,11 +40,11 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationConsentAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceVerificationAuthenticationConverter;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -279,8 +279,7 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
|
||||
if (StringUtils.hasText(this.consentPage)) {
|
||||
deviceVerificationEndpointFilter.setConsentPage(this.consentPage);
|
||||
}
|
||||
builder.addFilterBefore(postProcess(deviceVerificationEndpointFilter),
|
||||
AbstractPreAuthenticatedProcessingFilter.class);
|
||||
builder.addFilterAfter(postProcess(deviceVerificationEndpointFilter), AuthorizationFilter.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-1
@@ -255,7 +255,9 @@ class ServerHttpSecurityConfiguration {
|
||||
if (this.passwordEncoder != null) {
|
||||
manager.setPasswordEncoder(this.passwordEncoder);
|
||||
}
|
||||
manager.setUserDetailsPasswordService(this.userDetailsPasswordService);
|
||||
if (this.userDetailsPasswordService != null) {
|
||||
manager.setUserDetailsPasswordService(this.userDetailsPasswordService);
|
||||
}
|
||||
manager.setCompromisedPasswordChecker(this.compromisedPasswordChecker);
|
||||
return this.postProcessor.postProcess(manager);
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ base64 =
|
||||
## Whether a string should be base64 encoded
|
||||
attribute base64 {xsd:boolean}
|
||||
request-matcher =
|
||||
## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions.
|
||||
attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"}
|
||||
## Defines the strategy use for matching incoming requests. Currently the options are 'path' (for PathPatternRequestMatcher), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions.
|
||||
attribute request-matcher {"path" | "regex" | "ciRegex"}
|
||||
port =
|
||||
## Specifies an IP port number. Used to configure an embedded LDAP server, for example.
|
||||
attribute port { xsd:nonNegativeInteger }
|
||||
|
||||
+16
-20
@@ -27,15 +27,14 @@
|
||||
<xs:attributeGroup name="request-matcher">
|
||||
<xs:attribute name="request-matcher" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
|
||||
(for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
|
||||
and 'ciRegex' for case-insensitive regular expressions.
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'path'
|
||||
(for PathPatternRequestMatcher), 'regex' for regular expressions and 'ciRegex' for
|
||||
case-insensitive regular expressions.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:enumeration value="mvc"/>
|
||||
<xs:enumeration value="ant"/>
|
||||
<xs:enumeration value="path"/>
|
||||
<xs:enumeration value="regex"/>
|
||||
<xs:enumeration value="ciRegex"/>
|
||||
</xs:restriction>
|
||||
@@ -1306,15 +1305,14 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="request-matcher">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
|
||||
(for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
|
||||
and 'ciRegex' for case-insensitive regular expressions.
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'path'
|
||||
(for PathPatternRequestMatcher), 'regex' for regular expressions and 'ciRegex' for
|
||||
case-insensitive regular expressions.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:enumeration value="mvc"/>
|
||||
<xs:enumeration value="ant"/>
|
||||
<xs:enumeration value="path"/>
|
||||
<xs:enumeration value="regex"/>
|
||||
<xs:enumeration value="ciRegex"/>
|
||||
</xs:restriction>
|
||||
@@ -2474,15 +2472,14 @@
|
||||
<xs:attributeGroup name="filter-chain-map.attlist">
|
||||
<xs:attribute name="request-matcher">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
|
||||
(for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
|
||||
and 'ciRegex' for case-insensitive regular expressions.
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'path'
|
||||
(for PathPatternRequestMatcher), 'regex' for regular expressions and 'ciRegex' for
|
||||
case-insensitive regular expressions.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:enumeration value="mvc"/>
|
||||
<xs:enumeration value="ant"/>
|
||||
<xs:enumeration value="path"/>
|
||||
<xs:enumeration value="regex"/>
|
||||
<xs:enumeration value="ciRegex"/>
|
||||
</xs:restriction>
|
||||
@@ -2580,15 +2577,14 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="request-matcher">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
|
||||
(for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
|
||||
and 'ciRegex' for case-insensitive regular expressions.
|
||||
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'path'
|
||||
(for PathPatternRequestMatcher), 'regex' for regular expressions and 'ciRegex' for
|
||||
case-insensitive regular expressions.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:enumeration value="mvc"/>
|
||||
<xs:enumeration value="ant"/>
|
||||
<xs:enumeration value="path"/>
|
||||
<xs:enumeration value="regex"/>
|
||||
<xs:enumeration value="ciRegex"/>
|
||||
</xs:restriction>
|
||||
|
||||
+2
-2
@@ -359,7 +359,7 @@ public class OAuth2DeviceCodeGrantTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenBadRequest() throws Exception {
|
||||
public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenUnauthorized() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
// @formatter:off
|
||||
@@ -392,7 +392,7 @@ public class OAuth2DeviceCodeGrantTests {
|
||||
// @formatter:off
|
||||
this.mvc.perform(post(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
|
||||
.params(parameters))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isUnauthorized());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
+21
-1
@@ -107,7 +107,7 @@ public class ServerHttpSecurityConfigurationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadConfigWhenReactiveUserDetailsServiceConfiguredThenServerHttpSecurityExists() {
|
||||
public void loadConfigWhenReactiveUserAuthenticationServiceConfiguredThenServerHttpSecurityExists() {
|
||||
this.spring
|
||||
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
|
||||
WebFluxSecurityConfiguration.class)
|
||||
@@ -116,6 +116,16 @@ public class ServerHttpSecurityConfigurationTests {
|
||||
assertThat(serverHttpSecurity).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadConfigWhenOnlyReactiveUserDetailsServiceConfiguredThenServerHttpSecurityExists() {
|
||||
this.spring
|
||||
.register(ServerHttpSecurityConfiguration.class, ReactiveUserDetailsServiceOnlyTestConfiguration.class,
|
||||
WebFluxSecurityConfiguration.class)
|
||||
.autowire();
|
||||
ServerHttpSecurity serverHttpSecurity = this.spring.getContext().getBean(ServerHttpSecurity.class);
|
||||
assertThat(serverHttpSecurity).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadConfigWhenProxyingEnabledAndSubclassThenServerHttpSecurityExists() {
|
||||
this.spring
|
||||
@@ -581,4 +591,14 @@ public class ServerHttpSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ReactiveUserDetailsServiceOnlyTestConfiguration {
|
||||
|
||||
@Bean
|
||||
static ReactiveUserDetailsService userDetailsService() {
|
||||
return (username) -> Mono.just(PasswordEncodedUser.user());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http request-matcher="ant" use-authorization-manager="false">
|
||||
<http request-matcher="path" use-authorization-manager="false">
|
||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<http request-matcher="ant">
|
||||
<http request-matcher="path">
|
||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
http://www.springframework.org/schema/mvc
|
||||
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<http auto-config="true" request-matcher="mvc" use-authorization-manager="false">
|
||||
<http auto-config="true" request-matcher="path" use-authorization-manager="false">
|
||||
<intercept-url pattern="/path" access="denyAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
http://www.springframework.org/schema/mvc
|
||||
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<http auto-config="true" request-matcher="mvc">
|
||||
<http auto-config="true" request-matcher="path">
|
||||
<intercept-url pattern="/path" access="denyAll"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
http://www.springframework.org/schema/mvc
|
||||
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<http auto-config="true" request-matcher="mvc" use-authorization-manager="false">
|
||||
<http auto-config="true" request-matcher="path" use-authorization-manager="false">
|
||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
http://www.springframework.org/schema/mvc
|
||||
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<http auto-config="true" request-matcher="mvc">
|
||||
<http auto-config="true" request-matcher="path">
|
||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||
<http-basic/>
|
||||
</http>
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ public abstract class SecurityExpressionRoot<T extends @Nullable Object> impleme
|
||||
|
||||
@Override
|
||||
public final boolean hasAuthority(String authority) {
|
||||
return isGranted(this.authorizationManagerFactory.hasAnyAuthority(authority));
|
||||
return isGranted(this.authorizationManagerFactory.hasAuthority(authority));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-1
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.authentication.dao;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -43,6 +44,7 @@ import org.springframework.util.function.SingletonSupplier;
|
||||
*
|
||||
* @author Ben Alex
|
||||
* @author Rob Winch
|
||||
* @author Andrey Litvitski
|
||||
*/
|
||||
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
|
||||
|
||||
@@ -131,7 +133,8 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
|
||||
throw new CompromisedPasswordException("The provided password is compromised, please change your password");
|
||||
}
|
||||
String existingEncodedPassword = user.getPassword();
|
||||
boolean upgradeEncoding = existingEncodedPassword != null && this.userDetailsPasswordService != null
|
||||
boolean upgradeEncoding = existingEncodedPassword != null
|
||||
&& !Objects.equals(this.userDetailsPasswordService, UserDetailsPasswordService.NOOP)
|
||||
&& this.passwordEncoder.get().upgradeEncoding(existingEncodedPassword);
|
||||
if (upgradeEncoding) {
|
||||
String newPassword = this.passwordEncoder.get().encode(presentedPassword);
|
||||
|
||||
+5
-1
@@ -69,7 +69,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;
|
||||
}
|
||||
}
|
||||
|
||||
+148
@@ -21,6 +21,9 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
import org.springframework.security.authorization.SingleResultAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
||||
@@ -28,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
@@ -174,4 +178,148 @@ public class SecurityExpressionRootTests {
|
||||
assertThat(this.root.isAuthenticated()).isTrue();
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void hasAuthorityDelegatesToAuthorizationManagerFactoryHasAuthority() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.hasAuthority("CUSTOM_AUTHORITY")).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.hasAuthority("CUSTOM_AUTHORITY")).isFalse();
|
||||
verify(factory).hasAuthority("CUSTOM_AUTHORITY");
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void hasAnyAuthorityDelegatesToAuthorizationManagerFactoryHasAnyAuthority() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.hasAnyAuthority("CUSTOM_AUTHORITY")).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.hasAnyAuthority("CUSTOM_AUTHORITY")).isFalse();
|
||||
verify(factory).hasAnyAuthority("CUSTOM_AUTHORITY");
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void hasAllAuthoritiesDelegatesToAuthorizationManagerFactoryHasAllAuthorities() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.hasAllAuthorities("A", "B")).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.hasAllAuthorities("A", "B")).isFalse();
|
||||
verify(factory).hasAllAuthorities("A", "B");
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void hasRoleDelegatesToAuthorizationManagerFactoryHasRole() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.hasRole("CUSTOM_ROLE")).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.hasRole("CUSTOM_ROLE")).isFalse();
|
||||
verify(factory).hasRole("CUSTOM_ROLE");
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void hasAnyRoleDelegatesToAuthorizationManagerFactoryHasAnyRole() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.hasAnyRole("A", "B")).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.hasAnyRole("A", "B")).isFalse();
|
||||
verify(factory).hasAnyRole("A", "B");
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void hasAllRolesDelegatesToAuthorizationManagerFactoryHasAllRoles() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.hasAllRoles("A", "B")).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.hasAllRoles("A", "B")).isFalse();
|
||||
verify(factory).hasAllRoles("A", "B");
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void permitAllDelegatesToAuthorizationManagerFactoryPermitAll() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.permitAll()).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.permitAll()).isFalse();
|
||||
verify(factory).permitAll();
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void denyAllDelegatesToAuthorizationManagerFactoryDenyAll() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.denyAll()).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.denyAll()).isFalse();
|
||||
verify(factory).denyAll();
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void isAnonymousDelegatesToAuthorizationManagerFactoryAnonymous() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.anonymous()).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.isAnonymous()).isFalse();
|
||||
verify(factory).anonymous();
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void isAuthenticatedDelegatesToAuthorizationManagerFactoryAuthenticated() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.authenticated()).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.isAuthenticated()).isFalse();
|
||||
verify(factory).authenticated();
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void isRememberMeDelegatesToAuthorizationManagerFactoryRememberMe() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.rememberMe()).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.isRememberMe()).isFalse();
|
||||
verify(factory).rememberMe();
|
||||
}
|
||||
|
||||
// gh-18486
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void isFullyAuthenticatedDelegatesToAuthorizationManagerFactoryFullyAuthenticated() {
|
||||
AuthorizationManagerFactory<Object> factory = mock(AuthorizationManagerFactory.class);
|
||||
AuthorizationManager<Object> manager = SingleResultAuthorizationManager.denyAll();
|
||||
given(factory.fullyAuthenticated()).willReturn(manager);
|
||||
this.root.setAuthorizationManagerFactory(factory);
|
||||
assertThat(this.root.isFullyAuthenticated()).isFalse();
|
||||
verify(factory).fullyAuthenticated();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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 {
|
||||
|
||||
@@ -83,4 +87,20 @@ class AuthoritiesAuthorizationManagerTests {
|
||||
assertThat(manager.authorize(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,5 +1,6 @@
|
||||
plugins {
|
||||
id 'security-nullability'
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.spring-module'
|
||||
@@ -10,6 +11,8 @@ dependencies {
|
||||
optional 'org.bouncycastle:bcpkix-jdk18on'
|
||||
optional libs.com.password4j.password4j
|
||||
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
= Kerberos Migrations
|
||||
|
||||
For users leveraging Spring Security's Kerberos support, the Maven and Gradle Coordinates have been changed since the support was moved from an external module into Spring Security.
|
||||
See the xref:servlet/authentication/kerberos/introduction.adoc[Keberos documentation] for the new Maven and Gradle coordinates.
|
||||
@@ -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]
|
||||
----
|
||||
|
||||
@@ -320,7 +320,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.
|
||||
|
||||
@@ -3,3 +3,38 @@
|
||||
|
||||
Spring Security Kerberos {spring-security-version} is built and tested with JDK 17,
|
||||
Spring Security {spring-security-version} and Spring Framework {spring-core-version}.
|
||||
|
||||
The dependency coordinates changed with Spring Security 7:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
.pom.xml
|
||||
[source,xml,subs="verbatim,attributes"]
|
||||
----
|
||||
<dependencies>
|
||||
<!-- ... other dependency elements ... -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-kerberos-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-kerberos-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
.build.gradle
|
||||
[source,groovy]
|
||||
[subs="verbatim,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
implementation "org.springframework.security:spring-security-kerberos-core"
|
||||
implementation "org.springframework.security:spring-security-kerberos-web"
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,7 +10,7 @@ After the credential is registered, it can be used to authenticate by xref:servl
|
||||
[[passkeys-dependencies]]
|
||||
== Required Dependencies
|
||||
|
||||
To get started, add the `webauthn4j-core` dependency to your project.
|
||||
To get started, add the `spring-security-webauthn` dependency to your project.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -26,12 +26,7 @@ Maven::
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.webauthn4j</groupId>
|
||||
<artifactId>webauthn4j-core</artifactId>
|
||||
<version>{webauthn4j-core-version}</version>
|
||||
<artifactId>spring-security-webauthn</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
@@ -40,8 +35,7 @@ Gradle::
|
||||
[source,groovy,role="secondary",subs="verbatim,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
implementation "org.springframework.security:spring-security-web"
|
||||
implementation "com.webauthn4j:webauthn4j-core:{webauthn4j-core-version}"
|
||||
implementation "org.springframework.security:spring-security-webauthn"
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -97,6 +97,7 @@ This design allows any number of remember-me implementation strategies.
|
||||
We have seen earlier that Spring Security provides two implementations.
|
||||
We look at each of these in turn.
|
||||
|
||||
[[token-based-remember-me-services]]
|
||||
=== TokenBasedRememberMeServices
|
||||
This implementation supports the simpler approach described in <<remember-me-hash-token>>.
|
||||
`TokenBasedRememberMeServices` generates a `RememberMeAuthenticationToken`, which is processed by `RememberMeAuthenticationProvider`.
|
||||
@@ -110,105 +111,11 @@ If no `algorithmName` is present, the default matching algorithm will be used, w
|
||||
You can specify different algorithms for signature encoding and for signature matching, this allows users to safely upgrade to a different encoding algorithm while still able to verify old ones if there is no `algorithmName` present.
|
||||
To do that you can specify your customized `TokenBasedRememberMeServices` as a Bean and use it in the configuration.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.rememberMe((remember) -> remember
|
||||
.rememberMeServices(rememberMeServices)
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
|
||||
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
|
||||
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm);
|
||||
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
|
||||
return rememberMe;
|
||||
}
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
<remember-me services-ref="rememberMeServices"/>
|
||||
</http>
|
||||
|
||||
<bean id="rememberMeServices" class=
|
||||
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
|
||||
<property name="userDetailsService" ref="myUserDetailsService"/>
|
||||
<property name="key" value="springRocks"/>
|
||||
<property name="matchingAlgorithm" value="MD5"/>
|
||||
<property name="encodingAlgorithm" value="SHA256"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
include-code::./CustomAlgorithmRememberMeServicesConfiguration[tag=snippet,indent=0]
|
||||
|
||||
The following beans are required in an application context to enable remember-me services:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
RememberMeAuthenticationFilter rememberMeFilter() {
|
||||
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter();
|
||||
rememberMeFilter.setRememberMeServices(rememberMeServices());
|
||||
rememberMeFilter.setAuthenticationManager(theAuthenticationManager);
|
||||
return rememberMeFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
TokenBasedRememberMeServices rememberMeServices() {
|
||||
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices();
|
||||
rememberMeServices.setUserDetailsService(myUserDetailsService);
|
||||
rememberMeServices.setKey("springRocks");
|
||||
return rememberMeServices;
|
||||
}
|
||||
|
||||
@Bean
|
||||
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
|
||||
RememberMeAuthenticationProvider rememberMeAuthenticationProvider = new RememberMeAuthenticationProvider();
|
||||
rememberMeAuthenticationProvider.setKey("springRocks");
|
||||
return rememberMeAuthenticationProvider;
|
||||
}
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<bean id="rememberMeFilter" class=
|
||||
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
|
||||
<property name="rememberMeServices" ref="rememberMeServices"/>
|
||||
<property name="authenticationManager" ref="theAuthenticationManager" />
|
||||
</bean>
|
||||
|
||||
<bean id="rememberMeServices" class=
|
||||
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
|
||||
<property name="userDetailsService" ref="myUserDetailsService"/>
|
||||
<property name="key" value="springRocks"/>
|
||||
</bean>
|
||||
|
||||
<bean id="rememberMeAuthenticationProvider" class=
|
||||
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
|
||||
<property name="key" value="springRocks"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
include-code::./DefaultAlgorithmRememberMeServicesConfiguration[tag=snippet,indent=0]
|
||||
|
||||
Remember to add your `RememberMeServices` implementation to your `UsernamePasswordAuthenticationFilter.setRememberMeServices()` property, include the `RememberMeAuthenticationProvider` in your `AuthenticationManager.setProviders()` list, and add `RememberMeAuthenticationFilter` into your `FilterChainProxy` (typically immediately after your `UsernamePasswordAuthenticationFilter`).
|
||||
|
||||
|
||||
@@ -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]
|
||||
----
|
||||
|
||||
@@ -751,8 +751,8 @@ Java::
|
||||
public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) {
|
||||
RestOperations rest = builder
|
||||
.basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret())
|
||||
.setConnectTimeout(Duration.ofSeconds(60))
|
||||
.setReadTimeout(Duration.ofSeconds(60))
|
||||
.connectTimeout(Duration.ofSeconds(60))
|
||||
.readTimeout(Duration.ofSeconds(60))
|
||||
.build();
|
||||
|
||||
return SpringOpaqueTokenIntrospector(introspectionUri, rest);
|
||||
@@ -767,8 +767,8 @@ Kotlin::
|
||||
fun introspector(builder: RestTemplateBuilder, properties: OAuth2ResourceServerProperties): OpaqueTokenIntrospector? {
|
||||
val rest: RestOperations = builder
|
||||
.basicAuthentication(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret)
|
||||
.setConnectTimeout(Duration.ofSeconds(60))
|
||||
.setReadTimeout(Duration.ofSeconds(60))
|
||||
.connectTimeout(Duration.ofSeconds(60))
|
||||
.readTimeout(Duration.ofSeconds(60))
|
||||
.build()
|
||||
return SpringOpaqueTokenIntrospector(introspectionUri, rest)
|
||||
}
|
||||
|
||||
+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"
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2026-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.docs.servlet.authentication.tokenbasedremembermeservices;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.RememberMeTokenAlgorithm;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
/**
|
||||
* Demonstrates custom algorithm for remember me configuration.
|
||||
*
|
||||
* @author Ngoc Nhan
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class CustomAlgorithmRememberMeServicesConfiguration {
|
||||
|
||||
// tag::snippet[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.rememberMe((remember) -> remember
|
||||
.rememberMeServices(rememberMeServices)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
|
||||
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
|
||||
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices("myKey", userDetailsService,
|
||||
encodingAlgorithm);
|
||||
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
|
||||
return rememberMe;
|
||||
}
|
||||
// end::snippet[]
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2026-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.docs.servlet.authentication.tokenbasedremembermeservices;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.RememberMeAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
/**
|
||||
* Demonstrates default algorithm for remember me configuration.
|
||||
*
|
||||
* @author Ngoc Nhan
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class DefaultAlgorithmRememberMeServicesConfiguration {
|
||||
|
||||
// tag::snippet[]
|
||||
@Bean
|
||||
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
|
||||
return new TokenBasedRememberMeServices("myKey", userDetailsService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RememberMeAuthenticationFilter rememberMeFilter(AuthenticationManager authenticationManager,
|
||||
TokenBasedRememberMeServices rememberMeServices) {
|
||||
return new RememberMeAuthenticationFilter(authenticationManager, rememberMeServices);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
|
||||
return new RememberMeAuthenticationProvider("myKey");
|
||||
}
|
||||
// end::snippet[]
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2026-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.kt.docs.servlet.authentication.tokenbasedremembermeservices
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.RememberMeServices
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.RememberMeTokenAlgorithm
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
|
||||
/**
|
||||
* Demonstrates custom algorithm for remember me configuration.
|
||||
*
|
||||
* @author Ngoc Nhan
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CustomAlgorithmRememberMeServicesConfiguration {
|
||||
|
||||
// tag::snippet[]
|
||||
@Bean
|
||||
@Throws(Exception::class)
|
||||
fun securityFilterChain(http: HttpSecurity, rememberMeServices: RememberMeServices): SecurityFilterChain {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests{ it.anyRequest().authenticated() }
|
||||
.rememberMe { it.rememberMeServices(rememberMeServices) }
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun rememberMeServices(userDetailsService: UserDetailsService): RememberMeServices {
|
||||
val encodingAlgorithm = RememberMeTokenAlgorithm.SHA256
|
||||
val rememberMe = TokenBasedRememberMeServices("myKey", userDetailsService, encodingAlgorithm)
|
||||
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5)
|
||||
return rememberMe
|
||||
}
|
||||
// end::snippet[]
|
||||
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2026-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.kt.docs.servlet.authentication.tokenbasedremembermeservices
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.RememberMeAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.web.authentication.RememberMeServices
|
||||
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
|
||||
/**
|
||||
* Demonstrates default algorithm for remember me configuration.
|
||||
*
|
||||
* @author Ngoc Nhan
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class DefaultAlgorithmRememberMeServicesConfiguration {
|
||||
|
||||
// tag::snippet[]
|
||||
@Bean
|
||||
fun rememberMeServices(userDetailsService: UserDetailsService): RememberMeServices {
|
||||
return TokenBasedRememberMeServices("myKey", userDetailsService)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun rememberMeFilter(authenticationManager: AuthenticationManager, rememberMeServices: TokenBasedRememberMeServices): RememberMeAuthenticationFilter {
|
||||
return RememberMeAuthenticationFilter(authenticationManager, rememberMeServices)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun rememberMeAuthenticationProvider(): RememberMeAuthenticationProvider {
|
||||
return RememberMeAuthenticationProvider("myKey")
|
||||
}
|
||||
// end::snippet[]
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2026-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.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<b:bean id="userDetailsService"
|
||||
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"/>
|
||||
|
||||
<!-- tag::snippet[] -->
|
||||
<http>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<remember-me services-ref="rememberMeServices"/>
|
||||
</http>
|
||||
|
||||
<b:bean id="rememberMeServices"
|
||||
class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
|
||||
<b:constructor-arg value="myKey"/>
|
||||
<b:constructor-arg ref="userDetailsService"/>
|
||||
<b:constructor-arg value="SHA256"
|
||||
type="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices$RememberMeTokenAlgorithm"/>
|
||||
<b:property name="matchingAlgorithm" value="MD5"/>
|
||||
</b:bean>
|
||||
<!-- end::snippet[] -->
|
||||
|
||||
</b:beans>
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2026-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.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<b:bean id="userDetailsService"
|
||||
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"/>
|
||||
|
||||
<!-- tag::snippet[] -->
|
||||
<b:bean id="rememberMeServices"
|
||||
class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
|
||||
<b:constructor-arg value="myKey"/>
|
||||
<b:constructor-arg ref="userDetailsService"/>
|
||||
</b:bean>
|
||||
|
||||
<b:bean id="rememberMeFilter"
|
||||
class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
|
||||
<b:constructor-arg ref="authenticationManager"/>
|
||||
<b:constructor-arg ref="rememberMeServices"/>
|
||||
</b:bean>
|
||||
|
||||
<b:bean id="rememberMeAuthenticationProvider"
|
||||
class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
|
||||
<b:constructor-arg value="myKey"/>
|
||||
</b:bean>
|
||||
<!-- end::snippet[] -->
|
||||
|
||||
<authentication-manager alias="authenticationManager">
|
||||
<authentication-provider ref="rememberMeAuthenticationProvider"/>
|
||||
</authentication-manager>
|
||||
|
||||
</b:beans>
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
springBootVersion=4.0.0-SNAPSHOT
|
||||
version=7.0.3
|
||||
version=7.0.4
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -4,7 +4,7 @@ io-rsocket = "1.1.5"
|
||||
io-spring-javaformat = "0.0.47"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.2.0"
|
||||
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"
|
||||
@@ -12,11 +12,11 @@ org-jetbrains-kotlin = "2.2.21"
|
||||
org-jetbrains-kotlinx = "1.10.2"
|
||||
org-mockito = "5.17.0"
|
||||
org-opensaml5 = "5.1.6"
|
||||
org-springframework = "7.0.4"
|
||||
org-springframework = "7.0.6"
|
||||
com-password4j = "1.8.4"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.29"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.32"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.20.2"
|
||||
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"
|
||||
@@ -30,13 +30,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.9"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.3"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.4"
|
||||
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.14"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.15"
|
||||
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:3.0.0"
|
||||
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.2.0"
|
||||
@@ -51,7 +51,7 @@ net-sourceforge-htmlunit = "net.sourceforge.htmlunit:htmlunit:2.70.0"
|
||||
org-htmlunit-htmlunit = "org.htmlunit:htmlunit:4.11.1"
|
||||
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents.client5:httpclient5:5.5.2"
|
||||
org-apache-kerby-simplekdc='org.apache.kerby:kerb-simplekdc:2.1.1'
|
||||
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" }
|
||||
@@ -70,7 +70,7 @@ 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:2.2.21"
|
||||
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
|
||||
org-junit-junit-bom = "org.junit:junit-bom:6.0.2"
|
||||
org-junit-junit-bom = "org.junit:junit-bom:6.0.3"
|
||||
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
|
||||
org-opensaml-opensaml5-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml5" }
|
||||
org-opensaml-opensaml5-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml5" }
|
||||
@@ -81,7 +81,7 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.3"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.4"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.2"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
@@ -102,7 +102,7 @@ org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanne
|
||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.6'
|
||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.0.RELEASE'
|
||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.31.1.RELEASE'
|
||||
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
|
||||
|
||||
[plugins]
|
||||
|
||||
+1
-3
@@ -132,9 +132,7 @@ public final class OAuth2DeviceVerificationAuthenticationProvider implements Aut
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Did not authenticate device verification request since principal not authenticated");
|
||||
}
|
||||
// Return the device verification request as-is where isAuthenticated() is
|
||||
// false
|
||||
return deviceVerificationAuthentication;
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository
|
||||
|
||||
-9
@@ -161,15 +161,6 @@ public final class OAuth2DeviceVerificationEndpointFilter extends OncePerRequest
|
||||
}
|
||||
|
||||
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
|
||||
if (!authenticationResult.isAuthenticated()) {
|
||||
// If the Principal (Resource Owner) is not authenticated then pass
|
||||
// through the chain
|
||||
// with the expectation that the authentication process will commence via
|
||||
// AuthenticationEntryPoint
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (authenticationResult instanceof OAuth2DeviceAuthorizationConsentAuthenticationToken) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Device authorization consent is required");
|
||||
|
||||
+6
@@ -30,6 +30,7 @@ import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.OctetSequenceKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -112,6 +113,11 @@ public class JwtClientAssertionAuthenticationProviderTests {
|
||||
.setContext(new TestAuthorizationServerContext(this.authorizationServerSettings, null));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
|
||||
+6
@@ -25,6 +25,7 @@ import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -111,6 +112,11 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
|
||||
.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
|
||||
+6
@@ -22,6 +22,7 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -95,6 +96,11 @@ public class OAuth2AuthorizationConsentAuthenticationProviderTests {
|
||||
.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
|
||||
+19
-7
@@ -25,6 +25,7 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -96,6 +97,11 @@ public class OAuth2DeviceVerificationAuthenticationProviderTests {
|
||||
mockAuthorizationServerContext();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryIsNullThenThrowIllegalArgumentException() {
|
||||
// @formatter:off
|
||||
@@ -221,7 +227,7 @@ public class OAuth2DeviceVerificationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPrincipalNotAuthenticatedThenReturnUnauthenticated() {
|
||||
public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
// @formatter:off
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||
@@ -231,15 +237,21 @@ public class OAuth2DeviceVerificationAuthenticationProviderTests {
|
||||
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
|
||||
.build();
|
||||
// @formatter:on
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("user", null);
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("anonymous", null);
|
||||
principal.setAuthenticated(false);
|
||||
Authentication authentication = new OAuth2DeviceVerificationAuthenticationToken(principal, USER_CODE,
|
||||
Collections.emptyMap());
|
||||
given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization);
|
||||
given(this.authorizationService.findByToken(eq(USER_CODE),
|
||||
eq(OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE)))
|
||||
.willReturn(authorization);
|
||||
|
||||
OAuth2DeviceVerificationAuthenticationToken authenticationResult = (OAuth2DeviceVerificationAuthenticationToken) this.authenticationProvider
|
||||
.authenticate(authentication);
|
||||
assertThat(authenticationResult).isEqualTo(authentication);
|
||||
assertThat(authenticationResult.isAuthenticated()).isFalse();
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.extracting(OAuth2Error::getErrorCode)
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
// @formatter:on
|
||||
|
||||
verify(this.authorizationService).findByToken(USER_CODE,
|
||||
OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE);
|
||||
|
||||
-15
@@ -166,21 +166,6 @@ public class OAuth2DeviceVerificationEndpointFilterTests {
|
||||
verifyNoInteractions(this.authenticationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUnauthenticatedThenPassThrough() throws Exception {
|
||||
TestingAuthenticationToken unauthenticatedResult = new TestingAuthenticationToken("user", null);
|
||||
given(this.authenticationManager.authenticate(any(Authentication.class))).willReturn(unauthenticatedResult);
|
||||
|
||||
MockHttpServletRequest request = createRequest();
|
||||
request.addParameter(OAuth2ParameterNames.USER_CODE, USER_CODE);
|
||||
updateQueryString(request);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
verify(this.authenticationManager).authenticate(any(Authentication.class));
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenDeviceAuthorizationConsentRequestThenSuccess() throws Exception {
|
||||
Authentication authenticationResult = createDeviceVerificationAuthentication();
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ public final class CacheSaml2AuthenticationRequestRepository
|
||||
@Override
|
||||
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
Assert.notNull(authenticationRequest, "authenticationRequest must not be null");
|
||||
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
|
||||
Assert.notNull(relayState, "relayState must not be null");
|
||||
this.cache.put(relayState, authenticationRequest);
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.web.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.SmartHttpMessageConverter;
|
||||
|
||||
/**
|
||||
* {@link GenericHttpMessageConverter} implementation that delegates to a
|
||||
* {@link SmartHttpMessageConverter}.
|
||||
*
|
||||
* @param <T> the converted object type
|
||||
* @author Sebastien Deleuze
|
||||
* @since 7.0
|
||||
*/
|
||||
final class GenericHttpMessageConverterAdapter<T> implements GenericHttpMessageConverter<T> {
|
||||
|
||||
private final SmartHttpMessageConverter<T> smartConverter;
|
||||
|
||||
GenericHttpMessageConverterAdapter(SmartHttpMessageConverter<T> smartConverter) {
|
||||
this.smartConverter = smartConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
|
||||
return this.smartConverter.canRead(ResolvableType.forType(type), mediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
return this.smartConverter.read(ResolvableType.forType(type), inputMessage, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
return this.smartConverter.canWrite(ResolvableType.forType(type), clazz, mediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
this.smartConverter.write(t, ResolvableType.forType(type), contentType, outputMessage, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
return this.smartConverter.canRead(ResolvableType.forClass(clazz), mediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
return this.smartConverter.canWrite(clazz, mediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return this.smartConverter.getSupportedMediaTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
return this.smartConverter.read(clazz, inputMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
this.smartConverter.write(t, contentType, outputMessage);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -49,7 +49,7 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public final class HttpMessageConverterAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter();
|
||||
private HttpMessageConverter<Object> converter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.web.authentication;
|
||||
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.GsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for {@link HttpMessageConverter}'s.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 7.0.4
|
||||
*/
|
||||
final class HttpMessageConverters {
|
||||
|
||||
private static final boolean jacksonPresent;
|
||||
|
||||
private static final boolean jackson2Present;
|
||||
|
||||
private static final boolean gsonPresent;
|
||||
|
||||
private static final boolean jsonbPresent;
|
||||
|
||||
private static final String JSON_MAPPER = "tools.jackson.databind.json.JsonMapper";
|
||||
|
||||
private static final String OBJECT_MAPPER = "com.fasterxml.jackson.databind.ObjectMapper";
|
||||
|
||||
private static final String JSON_GENERATOR = "com.fasterxml.jackson.core.JsonGenerator";
|
||||
|
||||
private static final String GSON = "com.google.gson.Gson";
|
||||
|
||||
private static final String JSONB = "jakarta.json.bind.Jsonb";
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
|
||||
jacksonPresent = ClassUtils.isPresent(JSON_MAPPER, classLoader);
|
||||
jackson2Present = ClassUtils.isPresent(OBJECT_MAPPER, classLoader)
|
||||
&& ClassUtils.isPresent(JSON_GENERATOR, classLoader);
|
||||
gsonPresent = ClassUtils.isPresent(GSON, classLoader);
|
||||
jsonbPresent = ClassUtils.isPresent(JSONB, classLoader);
|
||||
}
|
||||
|
||||
private HttpMessageConverters() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link GenericHttpMessageConverterAdapter} to use for JSON.
|
||||
* @return the {@link GenericHttpMessageConverterAdapter} to use.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
|
||||
if (jacksonPresent) {
|
||||
return new GenericHttpMessageConverterAdapter<>(new JacksonJsonHttpMessageConverter());
|
||||
}
|
||||
if (jackson2Present) {
|
||||
return new MappingJackson2HttpMessageConverter();
|
||||
}
|
||||
if (gsonPresent) {
|
||||
return new GsonHttpMessageConverter();
|
||||
}
|
||||
if (jsonbPresent) {
|
||||
return new JsonbHttpMessageConverter();
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Cannot find JSON Converter on the classpath. Add one following classes to the classpath "
|
||||
+ String.join(", ", JSON_MAPPER, OBJECT_MAPPER, JSON_MAPPER, GSON, JSONB));
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -93,6 +93,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
|
||||
|
||||
+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();
|
||||
|
||||
+5
-6
@@ -55,11 +55,8 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
|
||||
throws JacksonException {
|
||||
List<AuthenticationExtensionsClientOutput<?>> outputs = new ArrayList<>();
|
||||
for (String key = parser.nextName(); key != null; key = parser.nextName()) {
|
||||
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);
|
||||
}
|
||||
@@ -67,7 +64,9 @@ class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping unknown extension with id " + key);
|
||||
}
|
||||
parser.nextValue();
|
||||
if (next.isStructStart()) {
|
||||
parser.skipChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-6
@@ -62,11 +62,8 @@ class AuthenticationExtensionsClientOutputsJackson2Deserializer
|
||||
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);
|
||||
}
|
||||
@@ -74,7 +71,9 @@ class AuthenticationExtensionsClientOutputsJackson2Deserializer
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping unknown extension with id " + key);
|
||||
}
|
||||
parser.nextValue();
|
||||
if (next.isStructStart()) {
|
||||
parser.skipChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.web.webauthn.jackson;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
|
||||
/**
|
||||
* Jackson mixin for {@link ImmutablePublicKeyCredentialUserEntity}
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
* @since 7.0.4
|
||||
*/
|
||||
abstract class ImmutablePublicKeyCredentialUserEntityMixin {
|
||||
|
||||
ImmutablePublicKeyCredentialUserEntityMixin(@JsonProperty("name") String name, @JsonProperty("id") Bytes id,
|
||||
@JsonProperty("displayName") String displayName) {
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -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.web.webauthn.jackson;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication;
|
||||
|
||||
/**
|
||||
* Jackson mixin for {@link WebAuthnAuthentication}
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
* @since 7.0.4
|
||||
*/
|
||||
@JsonIgnoreProperties({ "authenticated" })
|
||||
abstract class WebAuthnAuthenticationMixin {
|
||||
|
||||
WebAuthnAuthenticationMixin(@JsonProperty("principal") PublicKeyCredentialUserEntity principal,
|
||||
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
|
||||
}
|
||||
|
||||
}
|
||||
+8
@@ -33,12 +33,14 @@ import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
|
||||
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
|
||||
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
|
||||
import org.springframework.security.web.webauthn.api.ResidentKeyRequirement;
|
||||
import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication;
|
||||
import org.springframework.security.web.webauthn.management.RelyingPartyPublicKey;
|
||||
|
||||
/**
|
||||
@@ -47,6 +49,7 @@ import org.springframework.security.web.webauthn.management.RelyingPartyPublicKe
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rob Winch
|
||||
* @author Toshiaki Maki
|
||||
* @since 7.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@@ -61,6 +64,8 @@ public class WebauthnJacksonModule extends SecurityJacksonModule {
|
||||
|
||||
@Override
|
||||
public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) {
|
||||
builder.allowIfSubType(WebAuthnAuthentication.class)
|
||||
.allowIfSubType(ImmutablePublicKeyCredentialUserEntity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,6 +97,9 @@ public class WebauthnJacksonModule extends SecurityJacksonModule {
|
||||
context.setMixIn(RelyingPartyPublicKey.class, RelyingPartyPublicKeyMixin.class);
|
||||
context.setMixIn(ResidentKeyRequirement.class, ResidentKeyRequirementMixin.class);
|
||||
context.setMixIn(UserVerificationRequirement.class, UserVerificationRequirementMixin.class);
|
||||
context.setMixIn(WebAuthnAuthentication.class, WebAuthnAuthenticationMixin.class);
|
||||
context.setMixIn(ImmutablePublicKeyCredentialUserEntity.class,
|
||||
ImmutablePublicKeyCredentialUserEntityMixin.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+41
@@ -122,6 +122,47 @@ class Jackson2Tests {
|
||||
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 = """
|
||||
|
||||
+41
@@ -121,6 +121,47 @@ class JacksonTests {
|
||||
assertThat(outputs).usingRecursiveComparison().isEqualTo(credProps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAuthenticationExtensionsClientOutputsWhenAppId() {
|
||||
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() {
|
||||
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 = """
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.web.webauthn.jackson;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import tools.jackson.databind.JacksonModule;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
|
||||
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.jackson.SecurityJacksonModules;
|
||||
import org.springframework.security.web.webauthn.api.Bytes;
|
||||
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebAuthnAuthenticationMixin} and
|
||||
* {@link ImmutablePublicKeyCredentialUserEntityMixin} with polymorphic type handling.
|
||||
*
|
||||
* <p>
|
||||
* This test class is separate from {@link JacksonTests} because it requires a
|
||||
* {@link JsonMapper} configured with {@link SecurityJacksonModules} to enable polymorphic
|
||||
* type information ({@code @class}). {@link JacksonTests} uses a {@link JsonMapper}
|
||||
* configured only with {@link WebauthnJacksonModule}, and its existing custom serializers
|
||||
* are not compatible with the automatic inclusion of type information enabled by
|
||||
* {@link SecurityJacksonModules}.
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
* @since 7.1
|
||||
*/
|
||||
class WebAuthnAuthenticationMixinTests {
|
||||
|
||||
private JsonMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
ClassLoader classLoader = getClass().getClassLoader();
|
||||
WebauthnJacksonModule webauthnJacksonModule = new WebauthnJacksonModule();
|
||||
BasicPolymorphicTypeValidator.Builder typeValidatorBuilder = BasicPolymorphicTypeValidator.builder();
|
||||
webauthnJacksonModule.configurePolymorphicTypeValidator(typeValidatorBuilder);
|
||||
List<JacksonModule> modules = SecurityJacksonModules.getModules(classLoader, typeValidatorBuilder);
|
||||
modules.add(webauthnJacksonModule);
|
||||
this.mapper = JsonMapper.builder().addModules(modules).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeWebAuthnAuthentication() throws Exception {
|
||||
ImmutablePublicKeyCredentialUserEntity principal = (ImmutablePublicKeyCredentialUserEntity) ImmutablePublicKeyCredentialUserEntity
|
||||
.builder()
|
||||
.name("user@example.localhost")
|
||||
.id(Bytes.fromBase64("oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w"))
|
||||
.displayName("User")
|
||||
.build();
|
||||
WebAuthnAuthentication authentication = new WebAuthnAuthentication(principal,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
|
||||
String json = this.mapper.writeValueAsString(authentication);
|
||||
|
||||
String expected = """
|
||||
{
|
||||
"@class": "org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication",
|
||||
"principal": {
|
||||
"@class": "org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity",
|
||||
"name": "user@example.localhost",
|
||||
"id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
|
||||
"displayName": "User"
|
||||
},
|
||||
"authorities": ["java.util.Collections$UnmodifiableRandomAccessList", [
|
||||
{
|
||||
"@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
|
||||
"authority": "ROLE_USER"
|
||||
}
|
||||
]]
|
||||
}
|
||||
""";
|
||||
JSONAssert.assertEquals(expected, json, false);
|
||||
assertThat(json).doesNotContain("\"authenticated\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readWebAuthnAuthentication() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"@class": "org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication",
|
||||
"principal": {
|
||||
"@class": "org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity",
|
||||
"name": "user@example.localhost",
|
||||
"id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
|
||||
"displayName": "User"
|
||||
},
|
||||
"authorities": ["java.util.Collections$UnmodifiableRandomAccessList", [
|
||||
{
|
||||
"@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
|
||||
"authority": "ROLE_USER"
|
||||
}
|
||||
]]
|
||||
}
|
||||
""";
|
||||
ImmutablePublicKeyCredentialUserEntity expectedPrincipal = (ImmutablePublicKeyCredentialUserEntity) ImmutablePublicKeyCredentialUserEntity
|
||||
.builder()
|
||||
.name("user@example.localhost")
|
||||
.id(Bytes.fromBase64("oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w"))
|
||||
.displayName("User")
|
||||
.build();
|
||||
WebAuthnAuthentication expected = new WebAuthnAuthentication(expectedPrincipal,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
|
||||
WebAuthnAuthentication authentication = this.mapper.readValue(json, WebAuthnAuthentication.class);
|
||||
|
||||
assertThat(authentication).usingRecursiveComparison().isEqualTo(expected);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user