Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6adc3b3d99 | |||
| 363159ef53 | |||
| 2a6f6ecdb4 | |||
| 3ec2a2c309 | |||
| e5b54ed730 | |||
| 73e2e751a1 | |||
| 64b698fc48 | |||
| 2537a033f5 | |||
| d4e151c8a4 | |||
| 5587fe1015 | |||
| a0cdf099c0 | |||
| 79c8eb3af4 | |||
| 996e1be952 | |||
| 3d9df377ef | |||
| 5c0f1730d6 | |||
| e41771491b | |||
| f622d8e2e2 | |||
| 92cab2b678 | |||
| f47a7123e8 | |||
| 0913903110 | |||
| b5187b5da7 | |||
| eb0babc5a6 | |||
| 254709ce0b | |||
| 5f6b950290 | |||
| a7f8a8a42c | |||
| b2c17ec406 | |||
| e9c1b7b0ae | |||
| 329130f244 | |||
| 8875094361 | |||
| 2b322da9ac | |||
| 50235efb75 | |||
| 4b7499b19b | |||
| f899641f88 | |||
| 44454c21a4 | |||
| 3df22058bb | |||
| ec5cee416e | |||
| 397163b0b2 | |||
| b7c14608e1 | |||
| ff83e3758f | |||
| 122b47e6f9 | |||
| c3f766f3b3 | |||
| bce8035bb6 | |||
| 0532659245 | |||
| 22c7b8760a | |||
| f231ea277d | |||
| 6aabd768a8 | |||
| 91cd2cec2b | |||
| e013d96758 | |||
| 0aed8df549 | |||
| d6228e0882 | |||
| cdd626644e | |||
| 5a798e93f1 | |||
| 693cee9f3e | |||
| 4330bda97e | |||
| 8743186423 | |||
| ebb44d704f | |||
| 2dd1c58450 | |||
| aa88404d69 | |||
| 1e4aff2bdb | |||
| 3fc7b6e921 | |||
| dcb8c563e8 | |||
| 3defed4c3d | |||
| 1cc66faaef | |||
| 0da7284ddd | |||
| 99f233f98c | |||
| 7288fecc24 | |||
| db9f5935ae | |||
| 5a1d261ce0 | |||
| e34621ec2c | |||
| 1695d03b72 | |||
| dd5edeb255 | |||
| d067aca43d | |||
| 28c8432676 | |||
| 130e7d1b3f | |||
| 737c50c323 | |||
| 1c6726d20a | |||
| 797550fee7 | |||
| e7ea4091a0 | |||
| 659801163e | |||
| d0f95c90c5 | |||
| 235dfc4dfd | |||
| f820ef19a4 | |||
| a30088fc0a | |||
| a9419bb69c | |||
| 7f56306dcc | |||
| ddcaeb5c20 | |||
| 2c7b7a5e42 | |||
| 092a63d1e0 | |||
| 6956ed693c | |||
| ac9bdf5cbf | |||
| da1869c271 | |||
| 58cbc47376 | |||
| 896dd93313 | |||
| 0acf6cca6e | |||
| 47ad405063 | |||
| c7b739eb3f | |||
| df59516b18 | |||
| 9744cc44d2 | |||
| bd72741879 | |||
| 97f8c4b80e | |||
| 08d8f56cd6 | |||
| 32e860523c | |||
| 57f7b42345 | |||
| cda45d6f2c | |||
| 6c70b8740e | |||
| 0364d1e21f | |||
| 6818480d7a | |||
| dc27ae59ad | |||
| 80737c1a39 | |||
| 09fac2265c | |||
| 58232b1172 | |||
| ade8a5e5a4 | |||
| f51b35cbdc | |||
| 9ee0a52316 | |||
| 395310deb6 | |||
| 7b5914ddf8 | |||
| 2ff8dfe239 | |||
| 0e8fd1cd6c | |||
| d1cd51ebd2 | |||
| 3d7bcd2fad | |||
| 324415a4a0 | |||
| 1f001f8a6d | |||
| ca40d90d2c | |||
| dcef7ef454 | |||
| 3fdfa98172 | |||
| c6009ae007 | |||
| 1373bee220 | |||
| 90f5141880 | |||
| a57ba8a8d0 | |||
| 8cb2d17eb1 | |||
| ab6667eb5c | |||
| 8357b149fd | |||
| 013c46098e | |||
| 5bfd1f59d1 | |||
| 4b10c2daac | |||
| 720b43924a | |||
| 491f494991 | |||
| 0e4d29763f | |||
| 63e24a3026 | |||
| f115f90151 | |||
| 7251c7ffb7 | |||
| 514600721c | |||
| 2e4ea1eec5 | |||
| 86de2dbf6f | |||
| 2563b854ea | |||
| 9027dceb71 | |||
| 984939b5f7 | |||
| 3582673cac | |||
| d61a85f74b | |||
| 8b3d33ed64 | |||
| defe27d53e | |||
| c5b86370ff |
+16
-55
@@ -29,31 +29,6 @@ updates:
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.1.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '03:00'
|
||||
timezone: Etc/UTC
|
||||
labels:
|
||||
- 'type: dependency-upgrade'
|
||||
registries:
|
||||
- spring-milestones
|
||||
ignore:
|
||||
- dependency-name: com.nimbusds:nimbus-jose-jwt
|
||||
- dependency-name: org.python:jython
|
||||
- dependency-name: org.apache.directory.server:*
|
||||
- dependency-name: org.junit:junit-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: org.mockito:mockito-bom
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: '*'
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- package-ecosystem: gradle
|
||||
target-branch: 6.2.x
|
||||
directory: /
|
||||
@@ -118,32 +93,6 @@ updates:
|
||||
- 'in: build'
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 6.1.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
labels:
|
||||
- 'type: task'
|
||||
- 'in: build'
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: 6.2.x
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
labels:
|
||||
- 'type: task'
|
||||
- 'in: build'
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: main
|
||||
directory: /
|
||||
@@ -154,8 +103,6 @@ updates:
|
||||
- 'in: build'
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
- package-ecosystem: github-actions
|
||||
target-branch: docs-build
|
||||
directory: /
|
||||
@@ -166,11 +113,25 @@ updates:
|
||||
- 'in: build'
|
||||
ignore:
|
||||
- dependency-name: sjohnr/*
|
||||
- dependency-name: spring-io/*
|
||||
- dependency-name: spring-security-release-tools/*
|
||||
|
||||
- package-ecosystem: npm
|
||||
target-branch: docs-build
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: npm
|
||||
target-branch: main
|
||||
directory: /docs
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: npm
|
||||
target-branch: 6.2.x
|
||||
directory: /docs
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: npm
|
||||
target-branch: 5.8.x
|
||||
directory: /docs
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/gradle-build-action@v3
|
||||
- name: Upgrade Wrappers
|
||||
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
|
||||
env:
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue --scan
|
||||
run: ./gradlew clean build --continue
|
||||
generate-docs:
|
||||
name: Generate Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Update Antora UI Spring
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-antora-ui-spring:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update
|
||||
strategy:
|
||||
matrix:
|
||||
branch: [ '5.8.x', '6.1.x', '6.2.x', 'main' ]
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@17ed79ea5fbd65813c69ef1062a024d4a37ff0ca
|
||||
name: Update on Supported Branches
|
||||
with:
|
||||
docs-branch: ${{ matrix.branch }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
antora-file-path: 'docs/antora-playbook.yml'
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@17ed79ea5fbd65813c69ef1062a024d4a37ff0ca
|
||||
name: Update on docs-build
|
||||
with:
|
||||
docs-branch: 'docs-build'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
+67
-62
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -22,6 +22,7 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -42,11 +43,13 @@ import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
|
||||
/**
|
||||
@@ -203,43 +206,22 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
if (servletContext == null) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
boolean isProgrammaticApiAvailable = isProgrammaticApiAvailable(servletContext);
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
for (String pattern : patterns) {
|
||||
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
|
||||
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
|
||||
if (isProgrammaticApiAvailable) {
|
||||
matchers.add(resolve(ant, mvc, servletContext));
|
||||
}
|
||||
else {
|
||||
this.logger
|
||||
.warn("The ServletRegistration API was not available at startup time. This may be due to a misconfiguration; "
|
||||
+ "if you are using AbstractSecurityWebApplicationInitializer, please double-check the recommendations outlined in "
|
||||
+ "https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#abstractsecuritywebapplicationinitializer-with-spring-mvc");
|
||||
matchers.add(new DeferredRequestMatcher((request) -> resolve(ant, mvc, request.getServletContext()),
|
||||
mvc, ant));
|
||||
}
|
||||
matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant));
|
||||
}
|
||||
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
|
||||
}
|
||||
|
||||
private static boolean isProgrammaticApiAvailable(ServletContext servletContext) {
|
||||
try {
|
||||
servletContext.getServletRegistrations();
|
||||
return true;
|
||||
}
|
||||
catch (UnsupportedOperationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc, ServletContext servletContext) {
|
||||
Map<String, ? extends ServletRegistration> registrations = mappableServletRegistrations(servletContext);
|
||||
if (registrations.isEmpty()) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
if (!hasDispatcherServlet(registrations)) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
ServletRegistration dispatcherServlet = requireOneRootDispatcherServlet(registrations);
|
||||
if (dispatcherServlet != null) {
|
||||
@@ -474,34 +456,29 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
static class DeferredRequestMatcher implements RequestMatcher {
|
||||
|
||||
final Function<HttpServletRequest, RequestMatcher> requestMatcherFactory;
|
||||
final Function<ServletContext, RequestMatcher> requestMatcherFactory;
|
||||
|
||||
final AtomicReference<String> description = new AtomicReference<>();
|
||||
|
||||
volatile RequestMatcher requestMatcher;
|
||||
final Map<ServletContext, RequestMatcher> requestMatchers = new ConcurrentHashMap<>();
|
||||
|
||||
DeferredRequestMatcher(Function<HttpServletRequest, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (request) -> {
|
||||
if (this.requestMatcher == null) {
|
||||
synchronized (this) {
|
||||
if (this.requestMatcher == null) {
|
||||
this.requestMatcher = resolver.apply(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.requestMatcher;
|
||||
};
|
||||
DeferredRequestMatcher(Function<ServletContext, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (sc) -> this.requestMatchers.computeIfAbsent(sc, resolver);
|
||||
this.description.set("Deferred " + Arrays.toString(candidates));
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(ServletContext servletContext) {
|
||||
return this.requestMatcherFactory.apply(servletContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matches(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matcher(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -511,27 +488,70 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
}
|
||||
|
||||
static class MockMvcRequestMatcher implements RequestMatcher {
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
|
||||
DispatcherServletRequestMatcher(ServletContext servletContext) {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(name, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return DispatcherServlet.class.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final AntPathRequestMatcher ant;
|
||||
|
||||
private final MvcRequestMatcher mvc;
|
||||
|
||||
private final ServletContext servletContext;
|
||||
private final RequestMatcher dispatcherServlet;
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
ServletContext servletContext) {
|
||||
this(ant, mvc, new OrRequestMatcher(new MockMvcRequestMatcher(),
|
||||
new DispatcherServletRequestMatcher(servletContext)));
|
||||
}
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
RequestMatcher dispatcherServlet) {
|
||||
this.ant = ant;
|
||||
this.mvc = mvc;
|
||||
this.servletContext = servletContext;
|
||||
this.dispatcherServlet = dispatcherServlet;
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(HttpServletRequest request) {
|
||||
if (this.dispatcherServlet.matches(request)) {
|
||||
return this.mvc;
|
||||
}
|
||||
return this.ant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
if (this.dispatcherServlet.matches(request)) {
|
||||
return this.mvc.matches(request);
|
||||
}
|
||||
return this.ant.matches(request);
|
||||
@@ -539,27 +559,12 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
if (this.dispatcherServlet.matches(request)) {
|
||||
return this.mvc.matcher(request);
|
||||
}
|
||||
return this.ant.matcher(request);
|
||||
}
|
||||
|
||||
private boolean isDispatcherServlet(ServletRegistration registration) {
|
||||
Class<?> dispatcherServlet = ClassUtils
|
||||
.resolveClassName("org.springframework.web.servlet.DispatcherServlet", null);
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return dispatcherServlet.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DispatcherServletDelegating [" + "ant = " + this.ant + ", mvc = " + this.mvc + "]";
|
||||
|
||||
+4
@@ -29,6 +29,7 @@ import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.security.cas.authentication.CasAssertionAuthenticatio
|
||||
import org.springframework.security.cas.authentication.CasAuthenticationToken;
|
||||
import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.security.core.session.ReactiveSessionInformation;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
@@ -138,6 +140,8 @@ class SpringSecurityCoreVersionSerializableTests {
|
||||
generatorByClassName.put(OidcUserInfo.class, (r) -> OidcUserInfo.builder().email("email@example.com").build());
|
||||
generatorByClassName.put(SessionInformation.class,
|
||||
(r) -> new SessionInformation(user, r.alphanumeric(4), new Date(1704378933936L)));
|
||||
generatorByClassName.put(ReactiveSessionInformation.class,
|
||||
(r) -> new ReactiveSessionInformation(user, r.alphanumeric(4), Instant.ofEpochMilli(1704378933936L)));
|
||||
generatorByClassName.put(OAuth2LoginAuthenticationToken.class, (r) -> {
|
||||
var token = new OAuth2LoginAuthenticationToken(clientRegistration,
|
||||
TestOAuth2AuthorizationExchanges.success());
|
||||
|
||||
+72
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -26,27 +26,35 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.TestMockHttpServletMappings;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractRequestMatcherRegistry}.
|
||||
@@ -167,18 +175,65 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
// gh-14418
|
||||
@Test
|
||||
public void requestMatchersWhenNoDispatcherServletMockMvcThenMvcRequestMatcherType() throws Exception {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
try (SpringTestContext spring = new SpringTestContext(this)) {
|
||||
spring.register(MockMvcConfiguration.class)
|
||||
.postProcessor((context) -> context.setServletContext(servletContext))
|
||||
.autowire();
|
||||
this.matcherRegistry.setApplicationContext(spring.getContext());
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(spring.getContext()).build();
|
||||
MockHttpServletRequest request = mvc.perform(get("/")).andReturn().getRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -303,11 +358,13 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
return requestMatchers;
|
||||
}
|
||||
|
||||
private static List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
private List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
for (RequestMatcher requestMatcher : wrappedMatchers) {
|
||||
if (requestMatcher instanceof AbstractRequestMatcherRegistry.DeferredRequestMatcher) {
|
||||
requestMatchers.add(((DeferredRequestMatcher) requestMatcher).requestMatcher);
|
||||
if (requestMatcher instanceof DeferredRequestMatcher) {
|
||||
DeferredRequestMatcher deferred = (DeferredRequestMatcher) requestMatcher;
|
||||
WebApplicationContext web = (WebApplicationContext) getApplicationContext();
|
||||
requestMatchers.add(deferred.requestMatcher(web.getServletContext()));
|
||||
}
|
||||
else {
|
||||
requestMatchers.add(requestMatcher);
|
||||
@@ -318,4 +375,11 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class MockMvcConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -78,7 +78,7 @@ public class AuthorizeRequestsTests {
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.servletContext = spy(MockServletContext.mvc());
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(this.servletContext, "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -75,7 +75,7 @@ public class HttpSecuritySecurityMatchersTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -72,7 +72,7 @@ public class UrlAuthorizationConfigurerTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -30,7 +30,7 @@ urls:
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.11/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
|
||||
@@ -188,6 +188,12 @@ open fun reactiveSessionRegistry(): ReactiveSessionRegistry {
|
||||
When the maximum number of sessions is exceeded, by default, the least recently used session(s) will be expired.
|
||||
If you want to change that behavior, you can <<concurrent-sessions-control-custom-strategy,customize the strategy used when the maximum number of sessions is exceeded>>.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
The Concurrent Session Management is not aware if there is another session in some Identity Provider that you might use via xref:reactive/oauth2/login/index.adoc[OAuth 2 Login] for example.
|
||||
If you also need to invalidate the session against the Identity Provider you must <<concurrent-sessions-control-custom-strategy,include your own implementation of `ServerMaximumSessionsExceededHandler`>>.
|
||||
====
|
||||
|
||||
[[concurrent-sessions-control-custom-strategy]]
|
||||
== Handling Maximum Number of Sessions Exceeded
|
||||
|
||||
|
||||
@@ -10,6 +10,41 @@ However, despite using a username and password for authentication, it does not u
|
||||
There are many different scenarios for how an LDAP server can be configured, so Spring Security's LDAP provider is fully configurable.
|
||||
It uses separate strategy interfaces for authentication and role retrieval and provides default implementations, which can be configured to handle a wide range of situations.
|
||||
|
||||
[[servlet-authentication-ldap-required-dependencies]]
|
||||
== Required Dependencies
|
||||
|
||||
To get started, add the `spring-security-ldap` dependency to your project.
|
||||
When using Spring Boot, add the following dependencies:
|
||||
|
||||
.Spring Security LDAP Dependencies
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-ldap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-ldap</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary"]
|
||||
----
|
||||
depenendencies {
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-ldap"
|
||||
implementation "org.springframework.security:spring-security-ldap"
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[servlet-authentication-ldap-prerequisites]]
|
||||
== Prerequisites
|
||||
|
||||
@@ -81,14 +116,14 @@ dn: cn=user,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: user
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=user,ou=people,dc=springframework,dc=org
|
||||
|
||||
dn: cn=admin,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: admin
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
----
|
||||
|
||||
[[servlet-authentication-ldap-unboundid]]
|
||||
|
||||
@@ -50,9 +50,9 @@ If you have more than one in your application context, you need to specify which
|
||||
|
||||
[[remember-me-persistent-token]]
|
||||
== Persistent Token Approach
|
||||
This approach is based on the article titled http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice], with some minor modifications. (Essentially, the username is not included in the cookie, to prevent exposing a valid login name unnecessarily.
|
||||
There is a discussion on this in the comments section of this article.)
|
||||
To use the this approach with namespace configuration, supply a datasource reference:
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[Improved Persistent Login Cookie Best Practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
There is a discussion on this in the comments section of this article.].
|
||||
To use this approach with namespace configuration, you would supply a datasource reference:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
|
||||
@@ -241,8 +241,8 @@ Alternatively, you can modify every access constraint which requires the "user"
|
||||
This can get quite complicated if you have a lot of different roles in your application.
|
||||
|
||||
The use of a role-hierarchy allows you to configure which roles (or authorities) should include others.
|
||||
An extended version of Spring Security's `RoleVoter`, `RoleHierarchyVoter`, is configured with a `RoleHierarchy`, from which it obtains all the "reachable authorities" which the user is assigned.
|
||||
A typical configuration might look like this:
|
||||
This is supported for filter-based authorization in `HttpSecurity#authorizeHttpRequests` and for method-based authorization through `DefaultMethodSecurityExpressionHandler` for pre-post annotations, `SecuredAuthorizationManager` for `@Secured`, and `Jsr250AuthorizationManager` for JSR-250 annotations.
|
||||
You can configure the behavior for all of them at once in the following way:
|
||||
|
||||
.Hierarchical Roles Configuration
|
||||
[tabs]
|
||||
@@ -260,7 +260,7 @@ static RoleHierarchy roleHierarchy() {
|
||||
.build();
|
||||
}
|
||||
|
||||
// and, if using method security also add
|
||||
// and, if using pre-post method security also add
|
||||
@Bean
|
||||
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
|
||||
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
|
||||
@@ -292,13 +292,10 @@ Xml::
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
`RoleHierarchy` bean configuration is not yet ported over to `@EnableMethodSecurity`.
|
||||
As such this example is using `AccessDecisionVoter`.
|
||||
If you need `RoleHierarchy` support for method security, please continue using `@EnableGlobalMethodSecurity` until https://github.com/spring-projects/spring-security/issues/12783 is complete.
|
||||
|
||||
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
|
||||
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AuthorizationManager` adapted to call the above `RoleHierarchyVoter`.
|
||||
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against any filter- or method-based rules.
|
||||
|
||||
[TIP]
|
||||
The `>` symbol can be thought of as meaning "includes".
|
||||
|
||||
Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user.
|
||||
|
||||
@@ -185,15 +185,15 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/endpoint").hasAuthority("USER")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
public SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/endpoint").hasAuthority("USER")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
// ...
|
||||
|
||||
return http.build();
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -202,14 +202,15 @@ Kotlin::
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
||||
http {
|
||||
fun web(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/endpoint", hasAuthority("USER"))
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
}
|
||||
return http.build();
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -1074,7 +1074,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
Advisor postAuthorize() {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize();
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -1089,7 +1089,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun postAuthorize() : Advisor {
|
||||
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize()
|
||||
return AuthorizationManagerAfterMethodInterceptor.postAuthorize()
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -179,8 +179,8 @@ Java::
|
||||
[source=java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public HttpSessionEventListener sessionEventListener() {
|
||||
return new HttpSessionEventListener();
|
||||
public HttpSessionEventPublisher sessionEventPublisher() {
|
||||
return new HttpSessionEventPublisher();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -189,8 +189,8 @@ Kotlin::
|
||||
[source=kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun sessionEventListener(): HttpSessionEventListener {
|
||||
return HttpSessionEventListener()
|
||||
open fun sessionEventPublisher(): HttpSessionEventPublisher {
|
||||
return HttpSessionEventPublisher()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -339,7 +339,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
|
||||
JWTProcessor jwtProcessor(JWTClaimsSetAwareJWSKeySelector keySelector) {
|
||||
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
|
||||
new DefaultJWTProcessor();
|
||||
jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.4",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@antora/collector-extension": "1.0.0-alpha.4",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.10.0",
|
||||
"@springio/antora-extensions": "1.11.1",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.AntPathRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.RegexRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.core.annotation.MergedAnnotations.SearchStrategy.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.assertj.core.api.InstanceOfAssertFactories.*"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
springBootVersion=3.1.1
|
||||
version=6.3.0
|
||||
samplesBranch=main
|
||||
version=6.3.1
|
||||
samplesBranch=6.3.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
com-squareup-okhttp3 = "3.14.9"
|
||||
io-rsocket = "1.1.4"
|
||||
io-spring-javaformat = "0.0.41"
|
||||
io-spring-javaformat = "0.0.42"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.1.1"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
@@ -13,7 +13,7 @@ org-jetbrains-kotlin = "1.9.24"
|
||||
org-jetbrains-kotlinx = "1.8.1"
|
||||
org-mockito = "5.11.0"
|
||||
org-opensaml = "4.3.2"
|
||||
org-springframework = "6.1.7"
|
||||
org-springframework = "6.1.9"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.6"
|
||||
@@ -26,9 +26,9 @@ com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebser
|
||||
com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "com-squareup-okhttp3" }
|
||||
com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.6"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.7"
|
||||
io-mockk = "io.mockk:mockk:1.13.11"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.6"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.7"
|
||||
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" }
|
||||
@@ -53,7 +53,7 @@ org-apache-directory-server-apacheds-protocol-shared = { module = "org.apache.di
|
||||
org-apache-directory-server-apacheds-server-jndi = { module = "org.apache.directory.server:apacheds-server-jndi", version.ref = "org-apache-directory-server" }
|
||||
org-apache-directory-shared-shared-ldap = "org.apache.directory.shared:shared-ldap:0.9.15"
|
||||
org-apache-httpcomponents-httpclient = "org.apache.httpcomponents:httpclient:4.5.14"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.6"
|
||||
org-apache-maven-maven-resolver-provider = "org.apache.maven:maven-resolver-provider:3.9.7"
|
||||
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" }
|
||||
@@ -67,8 +67,8 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on",
|
||||
org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" }
|
||||
org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" }
|
||||
org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.4.8.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.2"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.4.9.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.3"
|
||||
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24"
|
||||
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
|
||||
@@ -84,8 +84,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.13"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.0"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.3"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.1"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.4"
|
||||
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"
|
||||
|
||||
@@ -95,11 +95,11 @@ net-sourceforge-saxon-saxon = "net.sourceforge.saxon:saxon:9.1.0.8"
|
||||
org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33"
|
||||
org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.3.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.3"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.4"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.13"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.15"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.19"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969"
|
||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.messaging.web.csrf;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Copied from
|
||||
@@ -43,26 +44,26 @@ final class XorCsrfTokenUtils {
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+69
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.messaging.web.csrf;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -141,6 +142,73 @@ public class XorCsrfChannelInterceptorTests {
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsShorterThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsLongerThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsShorterThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "a");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsLongerThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "abcde");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenActualBytesIsEmptyThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), "");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Message<String> message() {
|
||||
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
|
||||
}
|
||||
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -94,14 +94,10 @@ public final class WebSessionServerOAuth2AuthorizedClientRepository implements S
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, OAuth2AuthorizedClient> getAuthorizedClients(WebSession session) {
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = (session != null)
|
||||
? (Map<String, OAuth2AuthorizedClient>) session.getAttribute(this.sessionAttributeName) : null;
|
||||
if (authorizedClients == null) {
|
||||
authorizedClients = new HashMap<>();
|
||||
}
|
||||
return authorizedClients;
|
||||
Assert.notNull(session, "session cannot be null");
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = session.getAttribute(this.sessionAttributeName);
|
||||
return (authorizedClients != null) ? authorizedClients : new HashMap<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
@@ -24,10 +25,12 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -202,4 +205,28 @@ public class WebSessionServerOAuth2AuthorizedClientRepositoryTests {
|
||||
assertThat(loadedAuthorizedClient2).isSameAs(authorizedClient2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration1, this.principalName1,
|
||||
mock(OAuth2AccessToken.class));
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -183,7 +183,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
return claims;
|
||||
}
|
||||
|
||||
private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map<String, Object> claims) {
|
||||
private ArrayListFromStringClaimAccessor convertClaimsSet(Map<String, Object> claims) {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
@@ -277,4 +277,18 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
|
||||
}
|
||||
|
||||
// gh-15165
|
||||
private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor {
|
||||
|
||||
@Override
|
||||
default List<String> getScopes() {
|
||||
Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE);
|
||||
if (value instanceof ArrayListFromString list) {
|
||||
return list;
|
||||
}
|
||||
return OAuth2TokenIntrospectionClaimAccessor.super.getScopes();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -143,7 +143,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
.switchIfEmpty(Mono.error(() -> new BadOpaqueTokenException("Provided token isn't active")));
|
||||
}
|
||||
|
||||
private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map<String, Object> claims) {
|
||||
private ArrayListFromStringClaimAccessor convertClaimsSet(Map<String, Object> claims) {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
@@ -231,4 +231,18 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
|
||||
}
|
||||
|
||||
// gh-15165
|
||||
private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor {
|
||||
|
||||
@Override
|
||||
default List<String> getScopes() {
|
||||
Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE);
|
||||
if (value instanceof ArrayListFromString list) {
|
||||
return list;
|
||||
}
|
||||
return OAuth2TokenIntrospectionClaimAccessor.super.getScopes();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -39,6 +39,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||
@@ -245,6 +246,21 @@ public class SpringOpaqueTokenIntrospectorTests {
|
||||
assertThat(scope).containsExactly("read", "write", "dolphin");
|
||||
}
|
||||
|
||||
// gh-15165
|
||||
@Test
|
||||
public void introspectWhenActiveThenMapsAuthorities() {
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
OpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(INTROSPECTION_URL,
|
||||
restOperations);
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(STRING_OBJECT_MAP))).willReturn(ACTIVE);
|
||||
OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token");
|
||||
assertThat(principal.getAuthorities()).isNotEmpty();
|
||||
Collection<String> scope = principal.getAttribute("scope");
|
||||
assertThat(scope).containsExactly("read", "write", "dolphin");
|
||||
Collection<String> authorities = AuthorityUtils.authorityListToSet(principal.getAuthorities());
|
||||
assertThat(authorities).containsExactly("SCOPE_read", "SCOPE_write", "SCOPE_dolphin");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenIntrospectionUriIsNullThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -20,6 +20,7 @@ import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -37,6 +38,7 @@ import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||
@@ -197,6 +199,20 @@ public class SpringReactiveOpaqueTokenIntrospectorTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-15165
|
||||
@Test
|
||||
public void introspectWhenActiveThenMapsAuthorities() {
|
||||
WebClient webClient = mockResponse(ACTIVE_RESPONSE);
|
||||
SpringReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector(
|
||||
INTROSPECTION_URL, webClient);
|
||||
OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token").block();
|
||||
assertThat(principal.getAuthorities()).isNotEmpty();
|
||||
Collection<String> scope = principal.getAttribute("scope");
|
||||
assertThat(scope).containsExactly("read", "write", "dolphin");
|
||||
Collection<String> authorities = AuthorityUtils.authorityListToSet(principal.getAuthorities());
|
||||
assertThat(authorities).containsExactly("SCOPE_read", "SCOPE_write", "SCOPE_dolphin");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationConverterWhenConverterIsNullThenExceptionIsThrown() {
|
||||
WebClient web = mock(WebClient.class);
|
||||
|
||||
+8
@@ -47,6 +47,7 @@ import org.opensaml.core.xml.schema.XSDateTime;
|
||||
import org.opensaml.core.xml.schema.XSInteger;
|
||||
import org.opensaml.core.xml.schema.XSString;
|
||||
import org.opensaml.core.xml.schema.XSURI;
|
||||
import org.opensaml.saml.common.assertion.AssertionValidationException;
|
||||
import org.opensaml.saml.common.assertion.ValidationContext;
|
||||
import org.opensaml.saml.common.assertion.ValidationResult;
|
||||
import org.opensaml.saml.saml2.assertion.ConditionValidator;
|
||||
@@ -858,6 +859,13 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
static SAML20AssertionValidator createSignatureValidator(SignatureTrustEngine engine) {
|
||||
return new SAML20AssertionValidator(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), null, engine,
|
||||
validator) {
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ValidationResult validateBasicData(@Nonnull Assertion assertion,
|
||||
@Nonnull ValidationContext context) throws AssertionValidationException {
|
||||
return ValidationResult.VALID;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
protected ValidationResult validateConditions(Assertion assertion, ValidationContext context) {
|
||||
|
||||
+14
@@ -847,6 +847,20 @@ public class OpenSaml4AuthenticationProviderTests {
|
||||
provider.authenticate(token);
|
||||
}
|
||||
|
||||
// gh-15022
|
||||
@Test
|
||||
public void authenticateWhenClockSkewThenVerifiesSignature() {
|
||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
||||
provider.setAssertionValidator(OpenSaml4AuthenticationProvider.createDefaultAssertionValidatorWithParameters(
|
||||
(params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(10))));
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
assertion.setIssueInstant(Instant.now().plus(Duration.ofMinutes(9)));
|
||||
response.getAssertions().add(assertion);
|
||||
Saml2AuthenticationToken token = token(signed(response), verifying(registration()));
|
||||
provider.authenticate(token);
|
||||
}
|
||||
|
||||
private <T extends XMLObject> T build(QName qName) {
|
||||
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName);
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ pluginManagement {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.gradle.develocity" version "3.17.4"
|
||||
id "com.gradle.develocity" version "3.17.5"
|
||||
id "io.spring.ge.conventions" version "0.0.17"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -27,11 +27,13 @@ import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Standard implementation of {@code SecurityFilterChain}.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Jinwoo Bae
|
||||
* @since 3.1
|
||||
*/
|
||||
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
|
||||
@@ -48,10 +50,15 @@ public final class DefaultSecurityFilterChain implements SecurityFilterChain {
|
||||
|
||||
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
|
||||
if (filters.isEmpty()) {
|
||||
logger.info(LogMessage.format("Will not secure %s", requestMatcher));
|
||||
logger.debug(LogMessage.format("Will not secure %s", requestMatcher));
|
||||
}
|
||||
else {
|
||||
logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters));
|
||||
List<String> filterNames = new ArrayList<>();
|
||||
for (Filter filter : filters) {
|
||||
filterNames.add(filter.getClass().getSimpleName());
|
||||
}
|
||||
String names = StringUtils.collectionToDelimitedString(filterNames, ", ");
|
||||
logger.debug(LogMessage.format("Will secure %s with filters: %s", requestMatcher, names));
|
||||
}
|
||||
this.requestMatcher = requestMatcher;
|
||||
this.filters = new ArrayList<>(filters);
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's
|
||||
* <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's <a href=
|
||||
* "https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* Persistent Login Cookie Best Practice</a>.
|
||||
*
|
||||
* There is a slight modification to the described approach, in that the username is not
|
||||
|
||||
+9
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -84,20 +84,19 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null;
|
||||
return Utf8.decode(csrfBytes);
|
||||
}
|
||||
|
||||
private static String createXoredCsrfToken(SecureRandom secureRandom, String token) {
|
||||
@@ -114,12 +113,10 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
if (csrfBytes.length < randomBytes.length) {
|
||||
return null;
|
||||
}
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+8
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -77,17 +77,16 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null;
|
||||
@@ -107,12 +106,10 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
if (csrfBytes.length < randomBytes.length) {
|
||||
return null;
|
||||
}
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+14
-7
@@ -18,6 +18,7 @@ package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Scanner;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -47,7 +48,7 @@ public final class IpAddressMatcher implements RequestMatcher {
|
||||
* come.
|
||||
*/
|
||||
public IpAddressMatcher(String ipAddress) {
|
||||
assertStartsWithHexa(ipAddress);
|
||||
assertNotHostName(ipAddress);
|
||||
if (ipAddress.indexOf('/') > 0) {
|
||||
String[] addressAndMask = StringUtils.split(ipAddress, "/");
|
||||
ipAddress = addressAndMask[0];
|
||||
@@ -68,7 +69,7 @@ public final class IpAddressMatcher implements RequestMatcher {
|
||||
}
|
||||
|
||||
public boolean matches(String address) {
|
||||
assertStartsWithHexa(address);
|
||||
assertNotHostName(address);
|
||||
InetAddress remoteAddress = parseAddress(address);
|
||||
if (!this.requiredAddress.getClass().equals(remoteAddress.getClass())) {
|
||||
return false;
|
||||
@@ -91,11 +92,17 @@ public final class IpAddressMatcher implements RequestMatcher {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void assertStartsWithHexa(String ipAddress) {
|
||||
Assert.isTrue(
|
||||
ipAddress.charAt(0) == '[' || ipAddress.charAt(0) == ':'
|
||||
|| Character.digit(ipAddress.charAt(0), 16) != -1,
|
||||
"ipAddress must start with a [, :, or a hexadecimal digit");
|
||||
private void assertNotHostName(String ipAddress) {
|
||||
String error = "ipAddress " + ipAddress + " doesn't look like an IP Address. Is it a host name?";
|
||||
Assert.isTrue(ipAddress.charAt(0) == '[' || ipAddress.charAt(0) == ':'
|
||||
|| Character.digit(ipAddress.charAt(0), 16) != -1, error);
|
||||
if (!ipAddress.contains(":")) {
|
||||
Scanner parts = new Scanner(ipAddress);
|
||||
parts.useDelimiter("[./]");
|
||||
while (parts.hasNext()) {
|
||||
Assert.isTrue(parts.hasNextInt() && parts.nextInt() >> 8 == 0, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InetAddress parseAddress(String address) {
|
||||
|
||||
+49
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -44,6 +44,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*/
|
||||
public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -208,14 +211,58 @@ public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
assertThat(tokenValue).isEqualTo(this.token.getToken());
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenIsInvalidThenReturnsNull() {
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), "");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
return (invocation) -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
|
||||
+69
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -46,6 +46,9 @@ import static org.mockito.Mockito.verify;
|
||||
*/
|
||||
public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -188,16 +191,76 @@ public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenIsInvalidThenReturnsNull() {
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.body(this.token.getParameterName() + "=" + XOR_CSRF_TOKEN_VALUE))
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
CsrfToken token = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
Mono<String> csrfToken = this.handler.resolveCsrfTokenValue(this.exchange, token);
|
||||
assertThat(csrfToken.block()).isNull();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), ""))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
|
||||
+16
-1
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
@@ -108,7 +109,21 @@ public class IpAddressMatcherTests {
|
||||
@Test
|
||||
public void invalidAddressThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpAddressMatcher("invalid-ip"))
|
||||
.withMessage("ipAddress must start with a [, :, or a hexadecimal digit");
|
||||
.withMessage("ipAddress invalid-ip doesn't look like an IP Address. Is it a host name?");
|
||||
}
|
||||
|
||||
// gh-15172
|
||||
@Test
|
||||
public void hexadecimalDomainNameThenIllegalArgumentException() {
|
||||
assertThatException().isThrownBy(() -> new IpAddressMatcher("deadbeef.abc"))
|
||||
.withMessage("ipAddress deadbeef.abc doesn't look like an IP Address. Is it a host name?");
|
||||
}
|
||||
|
||||
// gh-15172
|
||||
@Test
|
||||
public void numericDomainNameThenIllegalArgumentException() {
|
||||
assertThatException().isThrownBy(() -> new IpAddressMatcher("123.156.7.18.org"))
|
||||
.withMessage("ipAddress 123.156.7.18.org doesn't look like an IP Address. Is it a host name?");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user