Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acc96f10be | |||
| 2ed013858a | |||
| c74f6bdac4 | |||
| 82e8817c95 | |||
| e80ef2df6c | |||
| 8065e2b308 | |||
| 5dd86473a3 | |||
| b2f7603e6d | |||
| a55c5c1d0d | |||
| c5fefd4dec | |||
| 8a7ae7a7f9 | |||
| a608352689 | |||
| ce2b9f5c90 | |||
| 0c20b14542 | |||
| c3dbbb2d01 | |||
| 5082da37ba | |||
| d187ddb5c6 | |||
| 9a5319d5a2 | |||
| 53d4ec44b4 | |||
| 1fb00879f6 | |||
| b6908a801a | |||
| eeaa682a6a | |||
| 697d0c9af4 | |||
| 472c9f8275 | |||
| 8a8caaed3b | |||
| 01f299f7ab | |||
| ef00312991 | |||
| 0af0751cfd | |||
| 16e2bdc9bc | |||
| c2447ec257 | |||
| 39dbd24dcb | |||
| 33ebd5405a | |||
| 40a16d451c | |||
| 76a465a544 | |||
| 2e327437dd | |||
| 5f1b9862cc | |||
| 52eadd7b06 | |||
| 6a66a5b74f | |||
| 2d2c2d31fa | |||
| 67d7cd4aea | |||
| fd9ec72bb9 | |||
| 6f8cc920cd | |||
| 80845d0c9a | |||
| ba575e8564 | |||
| 614123e6f9 | |||
| 8bd6991976 | |||
| c73becfa86 | |||
| 79801134b6 | |||
| 44033cd8b9 | |||
| e18ec48134 | |||
| afcce0c277 | |||
| 7162046144 | |||
| c439cfef0f | |||
| ce9f1821b1 | |||
| c036213892 | |||
| 69d2c6dff6 | |||
| 153ec5fbde | |||
| 3161b28701 | |||
| b58018dc0f | |||
| 6c9efc2f09 | |||
| b1505016c6 | |||
| 55be9eabd4 | |||
| 1a163dca37 | |||
| 1b0c4d68da | |||
| b38b495630 | |||
| c8a0601e6a | |||
| f226490be3 | |||
| 2455feb379 | |||
| a972338e1d | |||
| f84c4ea583 | |||
| 2c9dc08e43 | |||
| 5a7f12f1a9 | |||
| c0fe212d02 | |||
| a7105d833d | |||
| 8d6ede27d1 | |||
| 2e53745ddb | |||
| 747b806d37 | |||
| 3b355a0278 | |||
| 6849f4244b | |||
| 82e3061120 | |||
| a6d362fa18 | |||
| fdbfc58d54 | |||
| f905fad988 | |||
| dcfee957b5 | |||
| 37984c2e11 | |||
| e5c167a678 | |||
| d17a533e86 | |||
| b3a43d6154 | |||
| 94749d8942 | |||
| 712595b3a3 | |||
| a87fc4ea8a | |||
| 5495c2eca6 | |||
| 85d7e9ab55 | |||
| e7bff4240a | |||
| 51d96d7417 | |||
| f45be51987 | |||
| 9794952153 | |||
| d1daa7deef | |||
| 4b30acd1e0 | |||
| 4e6b8e4d29 | |||
| e550c48180 | |||
| 6d5f87e631 | |||
| 0ce90da113 | |||
| 794f4dbed9 | |||
| 2202632cad | |||
| a3c06d98ca | |||
| 940efe76fc | |||
| 8fe0303bad | |||
| 8f42c86a57 | |||
| a35a833839 | |||
| a759ede749 | |||
| a4a4c813d4 | |||
| 5dae6da15a | |||
| f3ef54b11b | |||
| 3a9b7a8a29 | |||
| 7db696b176 | |||
| 375326c639 | |||
| 6955c7aaea | |||
| 386e0a7b46 | |||
| 0ab9ad774d | |||
| e1c4177cd8 | |||
| e0a5712474 | |||
| 7f34ef7951 | |||
| c2e97d7661 | |||
| 55e7517414 | |||
| e74ac27b8f | |||
| 7e9f421a77 | |||
| aedffa7919 | |||
| ddd5d2e9cd | |||
| 2471df4d36 | |||
| b20d05724f | |||
| 27cd9fa86c | |||
| 94a66f7a26 | |||
| 9c48546883 | |||
| 7f106f0419 | |||
| 5f80468de3 | |||
| 2f762fefe1 | |||
| bb6045ebea | |||
| 2fdd541ea5 | |||
| 45c37c4454 | |||
| cfb9f1ed32 | |||
| 89246998c4 | |||
| e8c93fdc98 | |||
| 65cce7e305 | |||
| 008296cce2 | |||
| 238bc9733a | |||
| 836b350d2f | |||
| d80fade8b1 | |||
| 0c70f358d5 | |||
| 07454589f4 | |||
| 748c723bba | |||
| 2bd2aa9a75 | |||
| 370bf0ff74 | |||
| 7d38686e09 | |||
| 8b9beb0e1f | |||
| dd5bfc0b31 |
@@ -1,13 +1,52 @@
|
||||
name: Merge Dependabot PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
on: pull_request_target
|
||||
|
||||
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
merge-dependabot-pr:
|
||||
permissions: write-all
|
||||
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@1e8b0587a1f4f01697f9753fa3339c3e0d30f396
|
||||
with:
|
||||
mergeArguments: '--auto --rebase'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set Milestone to Dependabot Pull Request
|
||||
id: set-milestone
|
||||
run: |
|
||||
if test -f pom.xml
|
||||
then
|
||||
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
|
||||
else
|
||||
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
|
||||
fi
|
||||
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
|
||||
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
|
||||
|
||||
if [ -z $MILESTONE ]
|
||||
then
|
||||
gh run cancel ${{ github.run_id }}
|
||||
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
|
||||
else
|
||||
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
|
||||
echo mergeEnabled=true >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Merge Dependabot pull request
|
||||
if: steps.set-milestone.outputs.mergeEnabled
|
||||
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
|
||||
@@ -117,6 +117,7 @@ tasks.register('cloneRepository', IncludeRepoTask) {
|
||||
}
|
||||
|
||||
s101 {
|
||||
repository = 'https://structure101.com/binaries/latest'
|
||||
configurationDirectory = project.file("etc/s101")
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -17,7 +17,6 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -34,18 +33,11 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheFactory;
|
||||
@@ -71,6 +63,10 @@ public class S101Configurer {
|
||||
|
||||
private final Path licenseDirectory;
|
||||
|
||||
private final String repository;
|
||||
|
||||
private final String version;
|
||||
|
||||
private final Project project;
|
||||
private final Logger logger;
|
||||
|
||||
@@ -90,6 +86,9 @@ public class S101Configurer {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
this.licenseDirectory = new File(System.getProperty("user.home") + "/.Structure101/java").toPath();
|
||||
S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class);
|
||||
this.repository = extension.getRepository().get();
|
||||
this.version = extension.getVersion().get();
|
||||
}
|
||||
|
||||
public void license(String licenseId) {
|
||||
@@ -129,25 +128,7 @@ public class S101Configurer {
|
||||
|
||||
public void configure(File installationDirectory, File configurationDirectory) {
|
||||
deleteDirectory(configurationDirectory);
|
||||
String version = computeVersionFromInstallation(installationDirectory);
|
||||
configureProject(version, configurationDirectory);
|
||||
}
|
||||
|
||||
private String computeVersionFromInstallation(File installationDirectory) {
|
||||
File buildJar = new File(installationDirectory, "structure101-java-build.jar");
|
||||
try (JarInputStream input = new JarInputStream(new FileInputStream(buildJar))) {
|
||||
JarEntry entry;
|
||||
while ((entry = input.getNextJarEntry()) != null) {
|
||||
if (entry.getName().contains("structure101-build.properties")) {
|
||||
Properties properties = new Properties();
|
||||
properties.load(input);
|
||||
return properties.getProperty("s101-build");
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
throw new IllegalStateException("Unable to determine Structure101 version");
|
||||
configureProject(this.version, configurationDirectory);
|
||||
}
|
||||
|
||||
private boolean deleteDirectory(File directoryToBeDeleted) {
|
||||
@@ -161,24 +142,8 @@ public class S101Configurer {
|
||||
}
|
||||
|
||||
private String installBuildTool(File installationDirectory, File configurationDirectory) {
|
||||
String source = "https://structure101.com/binaries/v6";
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(source);
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher == null) {
|
||||
return null;
|
||||
}
|
||||
copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2));
|
||||
return matcher.group(2);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
copyZipToFilesystem(this.repository, installationDirectory, "structure101-build-java-all-" + this.version);
|
||||
return this.version;
|
||||
}
|
||||
|
||||
private void copyZipToFilesystem(String source, File destination, String name) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -17,7 +17,12 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
@@ -25,6 +30,11 @@ import org.gradle.api.tasks.InputDirectory;
|
||||
|
||||
public class S101PluginExtension {
|
||||
private final Property<String> licenseId;
|
||||
|
||||
private final Property<String> repository;
|
||||
|
||||
private final Property<String> version;
|
||||
|
||||
private final Property<File> installationDirectory;
|
||||
private final Property<File> configurationDirectory;
|
||||
private final Property<String> label;
|
||||
@@ -65,6 +75,24 @@ public class S101PluginExtension {
|
||||
this.label.set(label);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void setRepository(String repository) {
|
||||
this.repository.set(repository);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version.set(version);
|
||||
}
|
||||
|
||||
public S101PluginExtension(Project project) {
|
||||
this.licenseId = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.licenseId")) {
|
||||
@@ -78,5 +106,31 @@ public class S101PluginExtension {
|
||||
if (project.hasProperty("s101.label")) {
|
||||
setLabel((String) project.findProperty("s101.label"));
|
||||
}
|
||||
this.repository = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.repository")) {
|
||||
setRepository((String) project.findProperty("s101.repository"));
|
||||
} else {
|
||||
setRepository("https://structure101.com/binaries/v6");
|
||||
}
|
||||
this.version = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.version")) {
|
||||
setVersion((String) project.findProperty("s101.version"));
|
||||
} else {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getRepository().get());
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher != null) {
|
||||
setVersion(matcher.group(2));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ import org.springframework.security.authentication.AccountStatusUserDetailsCheck
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.SpringSecurityMessageSource;
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2011-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.
|
||||
* 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.cas.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* In order for the {@link CasAuthenticationProvider} to provide the correct service url
|
||||
* to authenticate the ticket, the returned value of {@link Authentication#getDetails()}
|
||||
* should implement this interface when tickets can be sent to any URL rather than only
|
||||
* {@link ServiceProperties#getService()}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource
|
||||
*/
|
||||
public interface ServiceAuthenticationDetails extends Serializable {
|
||||
|
||||
/**
|
||||
* Gets the absolute service url (i.e. https://example.com/service/).
|
||||
* @return the service url. Cannot be <code>null</code>.
|
||||
*/
|
||||
String getServiceUrl();
|
||||
|
||||
}
|
||||
+11
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011-2023 the original author or authors.
|
||||
* Copyright 2011-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,22 +16,24 @@
|
||||
|
||||
package org.springframework.security.cas.web.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* In order for the {@link CasAuthenticationProvider} to provide the correct service url
|
||||
* to authenticate the ticket, the returned value of {@link Authentication#getDetails()}
|
||||
* should implement this interface when tickets can be sent to any URL rather than only
|
||||
* {@link ServiceProperties#getService()}.
|
||||
* In order for the
|
||||
* {@link org.springframework.security.cas.authentication.CasAuthenticationProvider} to
|
||||
* provide the correct service url to authenticate the ticket, the returned value of
|
||||
* {@link Authentication#getDetails()} should implement this interface when tickets can be
|
||||
* sent to any URL rather than only {@link ServiceProperties#getService()}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @see ServiceAuthenticationDetailsSource
|
||||
* @deprecated Please use
|
||||
* org.springframework.security.cas.authentication.ServiceAuthenticationDetails
|
||||
*/
|
||||
public interface ServiceAuthenticationDetails extends Serializable {
|
||||
@Deprecated
|
||||
public interface ServiceAuthenticationDetails
|
||||
extends org.springframework.security.cas.authentication.ServiceAuthenticationDetails {
|
||||
|
||||
/**
|
||||
* Gets the absolute service url (i.e. https://example.com/service/).
|
||||
|
||||
+35
-3
@@ -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.
|
||||
@@ -22,11 +22,14 @@ import java.util.List;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.Aware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.core.NativeDetector;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -55,14 +58,13 @@ final class AutowireBeanFactoryObjectPostProcessor
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T postProcess(T object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
T result = null;
|
||||
try {
|
||||
result = (T) this.autowireBeanFactory.initializeBean(object, object.toString());
|
||||
result = initializeBeanIfNeeded(object);
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
Class<?> type = object.getClass();
|
||||
@@ -78,6 +80,36 @@ final class AutowireBeanFactoryObjectPostProcessor
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AutowireCapableBeanFactory#initializeBean(Object, String)} only if
|
||||
* needed, i.e when the application is not a native image or the object is not a CGLIB
|
||||
* proxy.
|
||||
* @param object the object to initialize
|
||||
* @param <T> the type of the object
|
||||
* @return the initialized bean or an existing bean if the object is a CGLIB proxy and
|
||||
* the application is a native image
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-projects/spring-security/issues/14825">Issue
|
||||
* gh-14825</a>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T initializeBeanIfNeeded(T object) {
|
||||
if (!NativeDetector.inNativeImage() || !AopUtils.isCglibProxy(object)) {
|
||||
return (T) this.autowireBeanFactory.initializeBean(object, object.toString());
|
||||
}
|
||||
ObjectProvider<?> provider = this.autowireBeanFactory.getBeanProvider(object.getClass());
|
||||
Object bean = provider.getIfUnique();
|
||||
if (bean == null) {
|
||||
String msg = """
|
||||
Failed to resolve an unique bean (single or primary) of type [%s] from the BeanFactory.
|
||||
Because the object is a CGLIB Proxy, a raw bean cannot be initialized during runtime in a native image.
|
||||
"""
|
||||
.formatted(object.getClass());
|
||||
throw new IllegalStateException(msg);
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
for (SmartInitializingSingleton singleton : this.smartSingletons) {
|
||||
|
||||
+1
-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.
|
||||
@@ -335,7 +335,6 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result = postProcess(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
+31
-8
@@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -37,10 +38,12 @@ import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegist
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
@@ -61,7 +64,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||
|
||||
private RestOperations restOperations = new RestTemplate();
|
||||
|
||||
private String logoutEndpointName = "/logout";
|
||||
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
|
||||
|
||||
private String sessionCookieName = "JSESSIONID";
|
||||
|
||||
@@ -112,12 +115,32 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||
}
|
||||
|
||||
String computeLogoutEndpoint(HttpServletRequest request) {
|
||||
String url = request.getRequestURL().toString();
|
||||
return UriComponentsBuilder.fromHttpUrl(url)
|
||||
.host("localhost")
|
||||
.replacePath(this.logoutEndpointName)
|
||||
.build()
|
||||
.toUriString();
|
||||
// @formatter:off
|
||||
UriComponents uriComponents = UriComponentsBuilder
|
||||
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replacePath(request.getContextPath())
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
.build();
|
||||
|
||||
Map<String, String> uriVariables = new HashMap<>();
|
||||
String scheme = uriComponents.getScheme();
|
||||
uriVariables.put("baseScheme", (scheme != null) ? scheme : "");
|
||||
uriVariables.put("baseUrl", uriComponents.toUriString());
|
||||
|
||||
String host = uriComponents.getHost();
|
||||
uriVariables.put("baseHost", (host != null) ? host : "");
|
||||
|
||||
String path = uriComponents.getPath();
|
||||
uriVariables.put("basePath", (path != null) ? path : "");
|
||||
|
||||
int port = uriComponents.getPort();
|
||||
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
||||
|
||||
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
||||
.buildAndExpand(uriVariables)
|
||||
.toUriString();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Collection<String> errors) {
|
||||
@@ -164,7 +187,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||
*/
|
||||
void setLogoutUri(String logoutUri) {
|
||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||
this.logoutEndpointName = logoutUri;
|
||||
this.logoutUri = logoutUri;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+43
-6
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
@@ -123,7 +124,7 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
private final AuthenticationManager authenticationManager = new ProviderManager(
|
||||
new OidcBackChannelLogoutAuthenticationProvider());
|
||||
|
||||
private LogoutHandler logoutHandler;
|
||||
private Function<B, LogoutHandler> logoutHandler = this::logoutHandler;
|
||||
|
||||
private AuthenticationConverter authenticationConverter(B http) {
|
||||
if (this.authenticationConverter == null) {
|
||||
@@ -139,18 +140,54 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
}
|
||||
|
||||
private LogoutHandler logoutHandler(B http) {
|
||||
if (this.logoutHandler == null) {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this endpoint when invoking a back-channel logout.
|
||||
*
|
||||
* <p>
|
||||
* The resulting {@link LogoutHandler} will {@code POST} the session cookie and
|
||||
* CSRF token to this endpoint to invalidate the corresponding end-user session.
|
||||
*
|
||||
* <p>
|
||||
* Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
|
||||
* {@code {basePort}}.
|
||||
*
|
||||
* <p>
|
||||
* By default, the URI is set to
|
||||
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme and
|
||||
* port of the original back-channel request is preserved, while the host and
|
||||
* endpoint are changed.
|
||||
*
|
||||
* <p>
|
||||
* If you are using Spring Security for the logout endpoint, the path part of this
|
||||
* URI should match the value configured there.
|
||||
*
|
||||
* <p>
|
||||
* Otherwise, this is handy in the event that your server configuration means that
|
||||
* the scheme, server name, or port in the {@code Host} header are different from
|
||||
* how you would address the same server internally.
|
||||
* @param logoutUri the URI to request logout on the back-channel
|
||||
* @return the {@link BackChannelLogoutConfigurer} for further customizations
|
||||
* @since 6.2.4
|
||||
*/
|
||||
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
||||
this.logoutHandler = (http) -> {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
return this.logoutHandler;
|
||||
logoutHandler.setLogoutUri(logoutUri);
|
||||
return logoutHandler;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
void configure(B http) {
|
||||
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
|
||||
authenticationManager());
|
||||
filter.setLogoutHandler(logoutHandler(http));
|
||||
filter.setLogoutHandler(this.logoutHandler.apply(http));
|
||||
http.addFilterBefore(filter, CsrfFilter.class);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -23,11 +23,11 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.
|
||||
@@ -42,7 +42,7 @@ public class JdbcUserServiceBeanDefinitionParser extends AbstractUserDetailsServ
|
||||
@Override
|
||||
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
|
||||
String dataSource = element.getAttribute(ATT_DATA_SOURCE);
|
||||
if (dataSource != null) {
|
||||
if (StringUtils.hasText(dataSource)) {
|
||||
builder.addPropertyReference("dataSource", dataSource);
|
||||
}
|
||||
else {
|
||||
|
||||
+32
-10
@@ -18,6 +18,7 @@ package org.springframework.security.config.web.server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -30,6 +31,7 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
@@ -42,6 +44,7 @@ import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
@@ -62,7 +65,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
|
||||
private WebClient web = WebClient.create();
|
||||
|
||||
private String logoutEndpointName = "/logout";
|
||||
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
|
||||
|
||||
private String sessionCookieName = "SESSION";
|
||||
|
||||
@@ -108,17 +111,36 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
||||
headers.add(credential.getKey(), credential.getValue());
|
||||
}
|
||||
String logout = computeLogoutEndpoint(exchange);
|
||||
String logout = computeLogoutEndpoint(exchange.getExchange().getRequest());
|
||||
return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
|
||||
}
|
||||
|
||||
String computeLogoutEndpoint(WebFilterExchange exchange) {
|
||||
String url = exchange.getExchange().getRequest().getURI().toString();
|
||||
return UriComponentsBuilder.fromHttpUrl(url)
|
||||
.host("localhost")
|
||||
.replacePath(this.logoutEndpointName)
|
||||
.build()
|
||||
.toUriString();
|
||||
String computeLogoutEndpoint(ServerHttpRequest request) {
|
||||
// @formatter:off
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
|
||||
.replacePath(request.getPath().contextPath().value())
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
.build();
|
||||
|
||||
Map<String, String> uriVariables = new HashMap<>();
|
||||
String scheme = uriComponents.getScheme();
|
||||
uriVariables.put("baseScheme", (scheme != null) ? scheme : "");
|
||||
uriVariables.put("baseUrl", uriComponents.toUriString());
|
||||
|
||||
String host = uriComponents.getHost();
|
||||
uriVariables.put("baseHost", (host != null) ? host : "");
|
||||
|
||||
String path = uriComponents.getPath();
|
||||
uriVariables.put("basePath", (path != null) ? path : "");
|
||||
|
||||
int port = uriComponents.getPort();
|
||||
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
||||
|
||||
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
||||
.buildAndExpand(uriVariables)
|
||||
.toUriString();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Collection<?> errors) {
|
||||
@@ -168,7 +190,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
*/
|
||||
void setLogoutUri(String logoutUri) {
|
||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||
this.logoutEndpointName = logoutUri;
|
||||
this.logoutUri = logoutUri;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+46
-6
@@ -58,6 +58,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
@@ -110,6 +111,7 @@ import org.springframework.security.oauth2.server.resource.web.access.server.Bea
|
||||
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
|
||||
import org.springframework.security.web.PortMapper;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||
@@ -5074,7 +5076,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager();
|
||||
|
||||
private ServerLogoutHandler logoutHandler;
|
||||
private Supplier<ServerLogoutHandler> logoutHandler = this::logoutHandler;
|
||||
|
||||
private ServerAuthenticationConverter authenticationConverter() {
|
||||
if (this.authenticationConverter == null) {
|
||||
@@ -5089,18 +5091,56 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
private ServerLogoutHandler logoutHandler() {
|
||||
if (this.logoutHandler == null) {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this endpoint when invoking a back-channel logout.
|
||||
*
|
||||
* <p>
|
||||
* The resulting {@link LogoutHandler} will {@code POST} the session cookie
|
||||
* and CSRF token to this endpoint to invalidate the corresponding end-user
|
||||
* session.
|
||||
*
|
||||
* <p>
|
||||
* Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
|
||||
* {@code {basePort}}.
|
||||
*
|
||||
* <p>
|
||||
* By default, the URI is set to
|
||||
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme
|
||||
* and port of the original back-channel request is preserved, while the host
|
||||
* and endpoint are changed.
|
||||
*
|
||||
* <p>
|
||||
* If you are using Spring Security for the logout endpoint, the path part of
|
||||
* this URI should match the value configured there.
|
||||
*
|
||||
* <p>
|
||||
* Otherwise, this is handy in the event that your server configuration means
|
||||
* that the scheme, server name, or port in the {@code Host} header are
|
||||
* different from how you would address the same server internally.
|
||||
* @param logoutUri the URI to request logout on the back-channel
|
||||
* @return the {@link OidcLogoutConfigurer.BackChannelLogoutConfigurer} for
|
||||
* further customizations
|
||||
* @since 6.2.4
|
||||
*/
|
||||
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
||||
this.logoutHandler = () -> {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
return this.logoutHandler;
|
||||
logoutHandler.setLogoutUri(logoutUri);
|
||||
return logoutHandler;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
void configure(ServerHttpSecurity http) {
|
||||
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
|
||||
authenticationManager());
|
||||
filter.setLogoutHandler(logoutHandler());
|
||||
filter.setLogoutHandler(this.logoutHandler.get());
|
||||
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
|
||||
}
|
||||
|
||||
|
||||
+61
-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.
|
||||
@@ -16,9 +16,13 @@
|
||||
|
||||
package org.springframework.security.config.annotation.configuration;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
@@ -31,13 +35,16 @@ import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.MessageSourceAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.NativeDetector;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.mockito.ArgumentMatchers.isNotNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -132,6 +139,59 @@ public class AutowireBeanFactoryObjectPostProcessorTests {
|
||||
assertThat(bean.doStuff()).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessWhenObjectIsCgLibProxyAndInNativeImageThenUseExistingBean() {
|
||||
try (var detector = Mockito.mockStatic(NativeDetector.class)) {
|
||||
given(NativeDetector.inNativeImage()).willReturn(true);
|
||||
|
||||
ProxyFactory proxyFactory = new ProxyFactory(new MyClass());
|
||||
proxyFactory.setProxyTargetClass(!Modifier.isFinal(MyClass.class.getModifiers()));
|
||||
MyClass myClass = (MyClass) proxyFactory.getProxy();
|
||||
|
||||
this.spring.register(Config.class, myClass.getClass()).autowire();
|
||||
this.spring.getContext().getBean(myClass.getClass()).setIdentifier("0000");
|
||||
|
||||
MyClass postProcessed = this.objectObjectPostProcessor.postProcess(myClass);
|
||||
assertThat(postProcessed.getIdentifier()).isEqualTo("0000");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessWhenObjectIsCgLibProxyAndInNativeImageAndBeanDoesNotExistsThenIllegalStateException() {
|
||||
try (var detector = Mockito.mockStatic(NativeDetector.class)) {
|
||||
given(NativeDetector.inNativeImage()).willReturn(true);
|
||||
|
||||
ProxyFactory proxyFactory = new ProxyFactory(new MyClass());
|
||||
proxyFactory.setProxyTargetClass(!Modifier.isFinal(MyClass.class.getModifiers()));
|
||||
MyClass myClass = (MyClass) proxyFactory.getProxy();
|
||||
|
||||
this.spring.register(Config.class).autowire();
|
||||
|
||||
assertThatException().isThrownBy(() -> this.objectObjectPostProcessor.postProcess(myClass))
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.withMessage(
|
||||
"""
|
||||
Failed to resolve an unique bean (single or primary) of type [class org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessorTests$MyClass$$SpringCGLIB$$0] from the BeanFactory.
|
||||
Because the object is a CGLIB Proxy, a raw bean cannot be initialized during runtime in a native image.
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
static class MyClass {
|
||||
|
||||
private String identifier = "1234";
|
||||
|
||||
String getIdentifier() {
|
||||
return this.identifier;
|
||||
}
|
||||
|
||||
void setIdentifier(String identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
|
||||
+25
-1
@@ -29,10 +29,34 @@ public class OidcBackChannelLogoutHandlerTests {
|
||||
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||
request.setRemoteHost("host.docker.internal");
|
||||
request.setServerName("host.docker.internal");
|
||||
request.setServerPort(8090);
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||
request.setServerName("host.docker.internal");
|
||||
request.setServerPort(8090);
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
|
||||
}
|
||||
|
||||
// gh-14609
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||
request.setScheme("https");
|
||||
request.setServerName("server-one.com");
|
||||
request.setServerPort(80);
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+42
-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.
|
||||
@@ -197,6 +197,25 @@ public class OidcLogoutConfigurerTests {
|
||||
this.mvc.perform(get("/token/logout").session(three)).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenRemoteLogoutUriThenUses() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, LogoutUriConfig.class).autowire();
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MockHttpSession one = login();
|
||||
String logoutToken = this.mvc.perform(get("/token/logout/all").session(one))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(content().string(containsString("partial_logout")))
|
||||
.andExpect(content().string(containsString("not all sessions were terminated")));
|
||||
this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
|
||||
@@ -312,6 +331,28 @@ public class OidcLogoutConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class LogoutUriConfig {
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel((backchannel) -> backchannel.logoutUri("http://localhost/wrong"))
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
|
||||
+37
-1
@@ -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.
|
||||
@@ -19,7 +19,10 @@ package org.springframework.security.config.authentication;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Element;
|
||||
import org.xml.sax.SAXParseException;
|
||||
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.CachingUserDetailsService;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
@@ -33,6 +36,7 @@ import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||
import org.springframework.security.util.FieldUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -160,6 +164,38 @@ public class JdbcUserServiceBeanDefinitionParserTests {
|
||||
assertThat(AuthorityUtils.authorityListToSet(rod.getAuthorities())).contains("PREFIX_ROLE_SUPERVISOR");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataSourceRef() {
|
||||
// @formatter:off
|
||||
String xml = "<authentication-manager>"
|
||||
+ " <authentication-provider>"
|
||||
+ " <jdbc-user-service data-source-ref=''/>"
|
||||
+ " </authentication-provider>"
|
||||
+ "</authentication-manager>";
|
||||
assertThatExceptionOfType(BeanDefinitionParsingException.class)
|
||||
.isThrownBy(() -> setContext(xml))
|
||||
.withFailMessage("Expected exception due to empty data-source-ref")
|
||||
.withMessageContaining("data-source-ref is required for jdbc-user-service");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingDataSourceRef() {
|
||||
// @formatter:off
|
||||
String xml = "<authentication-manager>"
|
||||
+ " <authentication-provider>"
|
||||
+ " <jdbc-user-service/>"
|
||||
+ " </authentication-provider>"
|
||||
+ "</authentication-manager>";
|
||||
assertThatExceptionOfType(XmlBeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> setContext(xml))
|
||||
.withFailMessage("Expected exception due to missing data-source-ref")
|
||||
.havingRootCause()
|
||||
.isInstanceOf(SAXParseException.class)
|
||||
.withMessageContaining("Attribute 'data-source-ref' must appear on element 'jdbc-user-service'");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private void setContext(String context) {
|
||||
this.appContext = new InMemoryXmlApplicationContext(context);
|
||||
}
|
||||
|
||||
+22
-6
@@ -17,12 +17,8 @@
|
||||
package org.springframework.security.config.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;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -38,9 +34,29 @@ public class OidcBackChannelServerLogoutHandlerTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("https://host.docker.internal:8090/back-channel/logout")
|
||||
.build();
|
||||
ServerWebExchange exchange = new MockServerWebExchange.Builder(request).build();
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(new WebFilterExchange(exchange, (ex) -> Mono.empty()));
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("https://localhost:8090/logout");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("http://host.docker.internal:8090/back-channel/logout")
|
||||
.build();
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
|
||||
}
|
||||
|
||||
// gh-14609
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://server-one.com/back-channel/logout").build();
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+49
-1
@@ -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.
|
||||
@@ -242,6 +242,32 @@ public class OidcLogoutSpecTests {
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", three).exchange().expectStatus().isUnauthorized();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenRemoteLogoutUriThenUses() {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, LogoutUriConfig.class).autowire();
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
String one = login();
|
||||
String logoutToken = this.test.get()
|
||||
.uri("/token/logout/all")
|
||||
.cookie("SESSION", one)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.returnResult(String.class)
|
||||
.getResponseBody()
|
||||
.blockFirst();
|
||||
this.test.post()
|
||||
.uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.body(BodyInserters.fromFormData("logout_token", logoutToken))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest()
|
||||
.expectBody(String.class)
|
||||
.value(containsString("partial_logout"))
|
||||
.value(containsString("not all sessions were terminated"));
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
|
||||
@@ -396,6 +422,28 @@ public class OidcLogoutSpecTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class LogoutUriConfig {
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel((backchannel) -> backchannel.logoutUri("http://localhost/wrong"))
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
|
||||
@@ -61,8 +61,7 @@ public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
|
||||
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
private boolean isFullyAuthenticated(Authentication authentication) {
|
||||
return (!this.authenticationTrustResolver.isAnonymous(authentication)
|
||||
&& !this.authenticationTrustResolver.isRememberMe(authentication));
|
||||
return this.authenticationTrustResolver.isFullyAuthenticated(authentication);
|
||||
}
|
||||
|
||||
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
|
||||
|
||||
+2
-4
@@ -21,8 +21,6 @@ import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
|
||||
/**
|
||||
* An {@link ObservationConvention} for translating authorizations into {@link KeyValues}.
|
||||
*
|
||||
@@ -85,10 +83,10 @@ public final class AuthorizationObservationConvention
|
||||
if (context.getObject() instanceof MethodInvocation) {
|
||||
return "method";
|
||||
}
|
||||
if (context.getObject() instanceof MethodInvocationResult) {
|
||||
String className = context.getObject().getClass().getSimpleName();
|
||||
if (className.contains("Method")) {
|
||||
return "method";
|
||||
}
|
||||
String className = context.getObject().getClass().getSimpleName();
|
||||
if (className.contains("Request")) {
|
||||
return "request";
|
||||
}
|
||||
|
||||
+2
-2
@@ -93,7 +93,7 @@ public final class AuthorizationManagerAfterMethodInterceptor
|
||||
PostAuthorizeAuthorizationManager authorizationManager) {
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(
|
||||
AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
|
||||
interceptor.setOrder(500);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class AuthorizationManagerAfterMethodInterceptor
|
||||
AuthorizationManager<MethodInvocationResult> authorizationManager) {
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(
|
||||
AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
|
||||
interceptor.setOrder(500);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -59,6 +59,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,6 +69,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,6 +79,7 @@ public class AuthenticatedVoterTests {
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(createAnonymous(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createRememberMe(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_GRANTED).isEqualTo(voter.vote(createFullyAuthenticated(), null, def));
|
||||
assertThat(AccessDecisionVoter.ACCESS_DENIED).isEqualTo(voter.vote(null, null, def));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -20,7 +20,7 @@ Java::
|
||||
@Bean
|
||||
SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception {
|
||||
DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler(
|
||||
new WebSessionServerLogoutHandler(), new SecurityContextServerLogoutHandler()
|
||||
new SecurityContextServerLogoutHandler(), new WebSessionServerLogoutHandler()
|
||||
);
|
||||
|
||||
http
|
||||
@@ -38,7 +38,7 @@ Kotlin::
|
||||
@Bean
|
||||
fun http(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
val customLogoutHandler = DelegatingServerLogoutHandler(
|
||||
WebSessionServerLogoutHandler(), SecurityContextServerLogoutHandler()
|
||||
SecurityContextServerLogoutHandler(), WebSessionServerLogoutHandler()
|
||||
)
|
||||
|
||||
return http {
|
||||
|
||||
@@ -38,7 +38,7 @@ public class MyApplication {
|
||||
|
||||
@Bean
|
||||
ObservationRegistryCustomizer<ObservationRegistry> addTextHandler() {
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -67,7 +67,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun addTextHandler(): ObservationRegistryCustomizer<ObservationRegistry> {
|
||||
return registry: ObservationRegistry -> registry.observationConfig()
|
||||
.observationHandler(ObservationTextHandler());
|
||||
.observationHandler(ObservationTextPublisher());
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
@@ -129,7 +129,7 @@ public class MyApplication {
|
||||
@Bean
|
||||
ObservationRegistry<ObservationRegistry> observationRegistry() {
|
||||
ObservationRegistry registry = ObservationRegistry.create();
|
||||
registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun observationRegistry(): ObservationRegistry<ObservationRegistry> {
|
||||
ObservationRegistry registry = ObservationRegistry.create()
|
||||
registry.observationConfig().observationHandler(ObservationTextHandler())
|
||||
registry.observationConfig().observationHandler(ObservationTextPublisher())
|
||||
return registry
|
||||
}
|
||||
|
||||
|
||||
@@ -876,7 +876,7 @@ class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
|
||||
delegate.handle(request, response, csrfToken)
|
||||
}
|
||||
|
||||
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String {
|
||||
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
|
||||
/*
|
||||
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
|
||||
* to resolve the CsrfToken. This applies when a single-page application includes
|
||||
@@ -1221,6 +1221,24 @@ public class CsrfTests {
|
||||
.andExpect(header().string(HttpHeaders.LOCATION, "/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
|
||||
this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenMissingCsrfTokenThenForbidden() throws Exception {
|
||||
this.mockMvc.perform(post("/login")
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
public void logoutWhenValidCsrfTokenThenSuccess() throws Exception {
|
||||
@@ -1264,6 +1282,24 @@ class CsrfTests {
|
||||
.andExpect(header().string(HttpHeaders.LOCATION, "/"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginWhenInvalidCsrfTokenThenForbidden() {
|
||||
mockMvc.perform(post("/login").with(csrf().useInvalidToken())
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginWhenMissingCsrfTokenThenForbidden() {
|
||||
mockMvc.perform(post("/login")
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.param("username", "user")
|
||||
.param("password", "password"))
|
||||
.andExpect(status().isForbidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
|
||||
@@ -38,7 +38,7 @@ public class MyApplication {
|
||||
|
||||
@Bean
|
||||
ObservationRegistryCustomizer<ObservationRegistry> addTextHandler() {
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
return (registry) -> registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -67,7 +67,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun addTextHandler(): ObservationRegistryCustomizer<ObservationRegistry> {
|
||||
return registry: ObservationRegistry -> registry.observationConfig()
|
||||
.observationHandler(ObservationTextHandler());
|
||||
.observationHandler(ObservationTextPublisher());
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
@@ -134,7 +134,7 @@ public class MyApplication {
|
||||
@Bean
|
||||
ObservationRegistry<ObservationRegistry> observationRegistry() {
|
||||
ObservationRegistry registry = ObservationRegistry.create();
|
||||
registry.observationConfig().observationHandler(new ObservationTextHandler());
|
||||
registry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class MyApplication {
|
||||
@Bean
|
||||
fun observationRegistry(): ObservationRegistry<ObservationRegistry> {
|
||||
ObservationRegistry registry = ObservationRegistry.create()
|
||||
registry.observationConfig().observationHandler(ObservationTextHandler())
|
||||
registry.observationConfig().observationHandler(ObservationTextPublisher())
|
||||
return registry
|
||||
}
|
||||
|
||||
|
||||
@@ -12,3 +12,5 @@
|
||||
^http://openoffice.org/.*
|
||||
^http://www.w3.org/2003/g/data-view
|
||||
^http://schemas.openid.net/event/backchannel-logout
|
||||
^http://host.docker.internal:8090/back-channel/logout
|
||||
^http://host.docker.internal:8090/logout
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<property name="relative-to" value="const(THIS_FILE)/../.." />
|
||||
<property name="action-set-mod" value="1" />
|
||||
<property name="detail-mode" value="true" />
|
||||
<property name="hide-deprecated" value="false" />
|
||||
<property name="hide-deprecated" value="true" />
|
||||
<property name="resolve-name-clashes" value="true" />
|
||||
<property name="project-excluded" />
|
||||
<property name="show-needs-to-compile" value="false" />
|
||||
@@ -29,11 +29,8 @@
|
||||
<classpathentry kind="lib" path="oauth2/oauth2-core/build/classes/java/main" module="spring-security-oauth2-core" />
|
||||
<classpathentry kind="lib" path="oauth2/oauth2-jose/build/classes/java/main" module="spring-security-oauth2-jose" />
|
||||
<classpathentry kind="lib" path="oauth2/oauth2-resource-server/build/classes/java/main" module="spring-security-oauth2-resource-server" />
|
||||
<classpathentry kind="lib" path="remoting/build/classes/java/main" module="spring-security-remoting" />
|
||||
<classpathentry kind="lib" path="rsocket/build/classes/java/main" module="spring-security-rsocket" />
|
||||
<classpathentry kind="lib" path="saml2/saml2-service-provider/build/classes/java/main" module="spring-security-saml2-service-provider" />
|
||||
<classpathentry kind="lib" path="saml2/saml2-service-provider/build/classes/java/opensaml3Main" module="spring-security-saml2-service-provider" />
|
||||
<classpathentry kind="lib" path="saml2/saml2-service-provider/build/classes/java/opensaml4Main" module="spring-security-saml2-service-provider" />
|
||||
<classpathentry kind="lib" path="taglibs/build/classes/java/main" module="spring-security-taglibs" />
|
||||
<classpathentry kind="lib" path="test/build/classes/java/main" module="spring-security-test" />
|
||||
<classpathentry kind="lib" path="web/build/classes/java/main" module="spring-security-web" />
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=3.1.1
|
||||
version=6.2.2
|
||||
version=6.2.4
|
||||
samplesBranch=6.2.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+13
-13
@@ -6,14 +6,14 @@ io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.1.1"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-apache-maven-resolver = "1.9.18"
|
||||
org-aspectj = "1.9.21.1"
|
||||
org-aspectj = "1.9.22"
|
||||
org-bouncycastle = "1.70"
|
||||
org-eclipse-jetty = "11.0.20"
|
||||
org-jetbrains-kotlin = "1.9.22"
|
||||
org-jetbrains-kotlin = "1.9.23"
|
||||
org-jetbrains-kotlinx = "1.7.3"
|
||||
org-mockito = "5.5.0"
|
||||
org-opensaml = "4.3.0"
|
||||
org-springframework = "6.1.4"
|
||||
org-opensaml = "4.3.1"
|
||||
org-springframework = "6.1.6"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14"
|
||||
@@ -26,15 +26,15 @@ 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.3"
|
||||
io-mockk = "io.mockk:mockk:1.13.9"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.3"
|
||||
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.12.5"
|
||||
io-mockk = "io.mockk:mockk:1.13.10"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.5"
|
||||
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.1"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.2"
|
||||
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
@@ -43,7 +43,7 @@ jakarta-servlet-jsp-jakarta-servlet-jsp-api = "jakarta.servlet.jsp:jakarta.servl
|
||||
jakarta-servlet-jsp-jstl-jakarta-servlet-jsp-jstl-api = "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0"
|
||||
jakarta-websocket-jakarta-websocket-api = { module = "jakarta.websocket:jakarta.websocket-api", version.ref = "jakarta-websocket" }
|
||||
jakarta-websocket-jakarta-websocket-client-api = { module = "jakarta.websocket:jakarta.websocket-client-api", version.ref = "jakarta-websocket" }
|
||||
jakarta-xml-bind-jakarta-xml-bind-api = "jakarta.xml.bind:jakarta.xml.bind-api:4.0.1"
|
||||
jakarta-xml-bind-jakarta-xml-bind-api = "jakarta.xml.bind:jakarta.xml.bind-api:4.0.2"
|
||||
ldapsdk = "ldapsdk:ldapsdk:4.1"
|
||||
net-sourceforge-htmlunit = "net.sourceforge.htmlunit:htmlunit:2.70.0"
|
||||
org-apache-directory-server-apacheds-core = { module = "org.apache.directory.server:apacheds-core", version.ref = "org-apache-directory-server" }
|
||||
@@ -70,7 +70,7 @@ org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.3.2.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.2"
|
||||
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.22"
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23"
|
||||
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:5.10.2"
|
||||
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
|
||||
@@ -83,9 +83,9 @@ org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:3
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
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.12"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.3"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.2"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.13"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2023.1.5"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.3"
|
||||
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"
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+10
-8
@@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -179,16 +180,17 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
// RFC-7662 page 7 directs users to RFC-7519 for defining the values of these
|
||||
// issuer fields.
|
||||
@@ -208,11 +210,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
// may be awkward to debug, we do not want to manipulate this value. Previous
|
||||
// versions of Spring Security
|
||||
// would *only* allow valid URLs, which is not what we wish to achieve here.
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
@@ -222,7 +224,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(converted, authorities);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-8
@@ -22,6 +22,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -136,16 +137,17 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
Map<String, Object> converted = new LinkedHashMap<>(claims);
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
// RFC-7662 page 7 directs users to RFC-7519 for defining the values of these
|
||||
// issuer fields.
|
||||
@@ -165,11 +167,11 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
// may be awkward to debug, we do not want to manipulate this value. Previous
|
||||
// versions of Spring Security
|
||||
// would *only* allow valid URLs, which is not what we wish to achieve here.
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
@@ -179,7 +181,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(converted, authorities);
|
||||
}
|
||||
|
||||
private OAuth2IntrospectionException onError(Throwable ex) {
|
||||
|
||||
+9
-142
@@ -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,25 +16,9 @@
|
||||
|
||||
package org.springframework.security.saml2.provider.service.metadata;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of {@link Saml2MetadataResponseResolver} that identifies which
|
||||
@@ -42,140 +26,23 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.1
|
||||
* @deprecated Please use
|
||||
* {@link org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver}
|
||||
*/
|
||||
public final class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver {
|
||||
|
||||
private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml";
|
||||
|
||||
private RequestMatcher matcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata"));
|
||||
|
||||
private String filename = DEFAULT_METADATA_FILENAME;
|
||||
|
||||
private final RelyingPartyRegistrationRepository registrations;
|
||||
|
||||
private final Saml2MetadataResolver metadata;
|
||||
@Deprecated
|
||||
public final class RequestMatcherMetadataResponseResolver extends
|
||||
org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver {
|
||||
|
||||
/**
|
||||
* Construct a {@link RequestMatcherMetadataResponseResolver}
|
||||
* Construct a
|
||||
* {@link org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver}
|
||||
* @param registrations the source for relying party metadata
|
||||
* @param metadata the strategy for converting {@link RelyingPartyRegistration}s into
|
||||
* metadata
|
||||
*/
|
||||
public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations,
|
||||
Saml2MetadataResolver metadata) {
|
||||
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
|
||||
Assert.notNull(metadata, "saml2MetadataResolver cannot be null");
|
||||
this.registrations = registrations;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and serialize a relying party's SAML 2.0 metadata based on the given
|
||||
* {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify
|
||||
* the metadata request, including looking for any indicated {@code registrationId}.
|
||||
*
|
||||
* <p>
|
||||
* If a {@code registrationId} is found in the request, it will attempt to use that,
|
||||
* erroring if no {@link RelyingPartyRegistration} is found.
|
||||
*
|
||||
* <p>
|
||||
* If no {@code registrationId} is found in the request, it will attempt to show all
|
||||
* {@link RelyingPartyRegistration}s in an {@code <md:EntitiesDescriptor>}. To
|
||||
* exercise this functionality, the provided
|
||||
* {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}.
|
||||
* @param request the HTTP request
|
||||
* @return a {@link Saml2MetadataResponse} instance
|
||||
* @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent
|
||||
* {@code registrationId}
|
||||
*/
|
||||
@Override
|
||||
public Saml2MetadataResponse resolve(HttpServletRequest request) {
|
||||
RequestMatcher.MatchResult result = this.matcher.matcher(request);
|
||||
if (!result.isMatch()) {
|
||||
return null;
|
||||
}
|
||||
String registrationId = result.getVariables().get("registrationId");
|
||||
Saml2MetadataResponse response = responseByRegistrationId(request, registrationId);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
if (this.registrations instanceof Iterable<?>) {
|
||||
Iterable<RelyingPartyRegistration> registrations = (Iterable<RelyingPartyRegistration>) this.registrations;
|
||||
return responseByIterable(request, registrations);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) {
|
||||
if (registrationId == null) {
|
||||
return null;
|
||||
}
|
||||
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
|
||||
if (registration == null) {
|
||||
throw new Saml2Exception("registration not found");
|
||||
}
|
||||
return responseByIterable(request, Collections.singleton(registration));
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByIterable(HttpServletRequest request,
|
||||
Iterable<RelyingPartyRegistration> registrations) {
|
||||
Map<String, RelyingPartyRegistration> results = new LinkedHashMap<>();
|
||||
for (RelyingPartyRegistration registration : registrations) {
|
||||
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
|
||||
String entityId = uriResolver.resolve(registration.getEntityId());
|
||||
results.computeIfAbsent(entityId, (e) -> {
|
||||
String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation());
|
||||
String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation());
|
||||
String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation());
|
||||
return registration.mutate()
|
||||
.entityId(entityId)
|
||||
.assertionConsumerServiceLocation(ssoLocation)
|
||||
.singleLogoutServiceLocation(sloLocation)
|
||||
.singleLogoutServiceResponseLocation(sloResponseLocation)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
String metadata = this.metadata.resolve(results.values());
|
||||
String value = (results.size() == 1) ? results.values().iterator().next().getRegistrationId()
|
||||
: UUID.randomUUID().toString();
|
||||
String fileName = this.filename.replace("{registrationId}", value);
|
||||
try {
|
||||
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
|
||||
return new Saml2MetadataResponse(metadata, encodedFileName);
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RequestMatcher} to identity which requests to generate metadata
|
||||
* for. By default, matches {@code /saml2/metadata},
|
||||
* {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata},
|
||||
* and {@code /saml2/service-provider-metadata/{registrationId}}
|
||||
* @param requestMatcher the {@link RequestMatcher} to use
|
||||
*/
|
||||
public void setRequestMatcher(RequestMatcher requestMatcher) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be empty");
|
||||
this.matcher = requestMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the metadata filename template. If it contains the {@code {registrationId}}
|
||||
* placeholder, it will be resolved as a random UUID if there are multiple
|
||||
* {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the
|
||||
* {@link RelyingPartyRegistration}'s id.
|
||||
*
|
||||
* <p>
|
||||
* The default value is {@code saml-{registrationId}-metadata.xml}
|
||||
* @param metadataFilename metadata filename, must contain a {registrationId}
|
||||
*/
|
||||
public void setMetadataFilename(String metadataFilename) {
|
||||
Assert.hasText(metadataFilename, "metadataFilename cannot be empty");
|
||||
this.filename = metadataFilename;
|
||||
super(registrations, metadata);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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.saml2.provider.service.web.metadata;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of {@link Saml2MetadataResponseResolver} that identifies which
|
||||
* {@link RelyingPartyRegistration}s to use with a {@link RequestMatcher}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.1
|
||||
*/
|
||||
public class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver {
|
||||
|
||||
private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml";
|
||||
|
||||
private RequestMatcher matcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata/{registrationId}"),
|
||||
new AntPathRequestMatcher("/saml2/metadata"));
|
||||
|
||||
private String filename = DEFAULT_METADATA_FILENAME;
|
||||
|
||||
private final RelyingPartyRegistrationRepository registrations;
|
||||
|
||||
private final Saml2MetadataResolver metadata;
|
||||
|
||||
/**
|
||||
* Construct a
|
||||
* {@link org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver}
|
||||
* @param registrations the source for relying party metadata
|
||||
* @param metadata the strategy for converting {@link RelyingPartyRegistration}s into
|
||||
* metadata
|
||||
*/
|
||||
public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations,
|
||||
Saml2MetadataResolver metadata) {
|
||||
Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
|
||||
Assert.notNull(metadata, "saml2MetadataResolver cannot be null");
|
||||
this.registrations = registrations;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and serialize a relying party's SAML 2.0 metadata based on the given
|
||||
* {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify
|
||||
* the metadata request, including looking for any indicated {@code registrationId}.
|
||||
*
|
||||
* <p>
|
||||
* If a {@code registrationId} is found in the request, it will attempt to use that,
|
||||
* erroring if no {@link RelyingPartyRegistration} is found.
|
||||
*
|
||||
* <p>
|
||||
* If no {@code registrationId} is found in the request, it will attempt to show all
|
||||
* {@link RelyingPartyRegistration}s in an {@code <md:EntitiesDescriptor>}. To
|
||||
* exercise this functionality, the provided
|
||||
* {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}.
|
||||
* @param request the HTTP request
|
||||
* @return a {@link Saml2MetadataResponse} instance
|
||||
* @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent
|
||||
* {@code registrationId}
|
||||
*/
|
||||
@Override
|
||||
public Saml2MetadataResponse resolve(HttpServletRequest request) {
|
||||
RequestMatcher.MatchResult result = this.matcher.matcher(request);
|
||||
if (!result.isMatch()) {
|
||||
return null;
|
||||
}
|
||||
String registrationId = result.getVariables().get("registrationId");
|
||||
Saml2MetadataResponse response = responseByRegistrationId(request, registrationId);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
if (this.registrations instanceof Iterable<?>) {
|
||||
Iterable<RelyingPartyRegistration> registrations = (Iterable<RelyingPartyRegistration>) this.registrations;
|
||||
return responseByIterable(request, registrations);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) {
|
||||
if (registrationId == null) {
|
||||
return null;
|
||||
}
|
||||
RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId);
|
||||
if (registration == null) {
|
||||
throw new Saml2Exception("registration not found");
|
||||
}
|
||||
return responseByIterable(request, Collections.singleton(registration));
|
||||
}
|
||||
|
||||
private Saml2MetadataResponse responseByIterable(HttpServletRequest request,
|
||||
Iterable<RelyingPartyRegistration> registrations) {
|
||||
Map<String, RelyingPartyRegistration> results = new LinkedHashMap<>();
|
||||
for (RelyingPartyRegistration registration : registrations) {
|
||||
RelyingPartyRegistrationPlaceholderResolvers.UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers
|
||||
.uriResolver(request, registration);
|
||||
String entityId = uriResolver.resolve(registration.getEntityId());
|
||||
results.computeIfAbsent(entityId, (e) -> {
|
||||
String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation());
|
||||
String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation());
|
||||
String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation());
|
||||
return registration.mutate()
|
||||
.entityId(entityId)
|
||||
.assertionConsumerServiceLocation(ssoLocation)
|
||||
.singleLogoutServiceLocation(sloLocation)
|
||||
.singleLogoutServiceResponseLocation(sloResponseLocation)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
String metadata = this.metadata.resolve(results.values());
|
||||
String value = (results.size() == 1) ? results.values().iterator().next().getRegistrationId()
|
||||
: UUID.randomUUID().toString();
|
||||
String fileName = this.filename.replace("{registrationId}", value);
|
||||
try {
|
||||
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
|
||||
return new Saml2MetadataResponse(metadata, encodedFileName);
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
throw new Saml2Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RequestMatcher} to identity which requests to generate metadata
|
||||
* for. By default, matches {@code /saml2/metadata},
|
||||
* {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata},
|
||||
* and {@code /saml2/service-provider-metadata/{registrationId}}
|
||||
* @param requestMatcher the {@link RequestMatcher} to use
|
||||
*/
|
||||
public void setRequestMatcher(RequestMatcher requestMatcher) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be empty");
|
||||
this.matcher = requestMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the metadata filename template. If it contains the {@code {registrationId}}
|
||||
* placeholder, it will be resolved as a random UUID if there are multiple
|
||||
* {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the
|
||||
* {@link RelyingPartyRegistration}'s id.
|
||||
*
|
||||
* <p>
|
||||
* The default value is {@code saml-{registrationId}-metadata.xml}
|
||||
* @param metadataFilename metadata filename, must contain a {registrationId}
|
||||
*/
|
||||
public void setMetadataFilename(String metadataFilename) {
|
||||
Assert.hasText(metadataFilename, "metadataFilename cannot be empty");
|
||||
this.filename = metadataFilename;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "com.gradle.enterprise" version "3.12.6"
|
||||
id "io.spring.ge.conventions" version "0.0.15"
|
||||
id "io.spring.ge.conventions" version "0.0.16"
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-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.
|
||||
@@ -130,9 +130,13 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
|
||||
s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
|
||||
|
||||
private static final Pattern HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[[^\\p{IsControl}]||\\t]]*");
|
||||
|
||||
private static final Predicate<String> HEADER_VALUE_PREDICATE = (s) -> HEADER_VALUE_PATTERN.matcher(s).matches();
|
||||
|
||||
private Predicate<String> allowedHeaderNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedHeaderValues = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
private Predicate<String> allowedHeaderValues = HEADER_VALUE_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedParameterNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -125,7 +125,7 @@ public class AuthenticationWebFilter implements WebFilter {
|
||||
.flatMap(
|
||||
(authentication) -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)))
|
||||
.doOnError(AuthenticationException.class,
|
||||
(ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage())));
|
||||
(ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage()), ex));
|
||||
}
|
||||
|
||||
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-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.
|
||||
@@ -781,6 +781,13 @@ public class StrictHttpFirewallTests {
|
||||
assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Something"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestGetHeaderWhenHorizontalTabInHeaderValueThenNoException() {
|
||||
this.request.addHeader("Something", "tab\tvalue");
|
||||
HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
|
||||
assertThat(request.getHeader("Something")).isEqualTo("tab\tvalue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestGetHeaderWhenUndefinedCharacterInHeaderValueThenException() {
|
||||
this.request.addHeader("Something", "bad\uFFFEvalue");
|
||||
|
||||
Reference in New Issue
Block a user