Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6354250a1 | |||
| a5d963387b | |||
| 99c4f58c34 | |||
| c1aa99fdd2 | |||
| eb01394427 | |||
| 0d3d6f75f8 | |||
| eb83c35ded | |||
| 3c0fef59b5 | |||
| da94fbe431 | |||
| 857ef6fe08 | |||
| b7df86197c | |||
| c84c438075 | |||
| 1e7db094d1 | |||
| 456604ab45 | |||
| 15b9a50060 | |||
| fcc1bd598d | |||
| af2668f7cb | |||
| 883765b2de | |||
| 58e7ba4a4b | |||
| 72554f7f36 | |||
| 65e83f8e7a | |||
| ab6e9d2d1f | |||
| a53ca7c3d0 | |||
| af40d7e35a | |||
| daf8cfe8d2 | |||
| 75b537f99a | |||
| d1b7f8a119 | |||
| 5353d499b4 | |||
| 1d75b907f9 | |||
| 7dbd69fee1 | |||
| 147081f771 | |||
| 709d9bc039 | |||
| d9bb16e913 | |||
| 11114919ec | |||
| adb303e152 | |||
| 46f0dc6dfc | |||
| 36ea1b11a7 | |||
| e793a962c5 | |||
| 46cd94b5f4 | |||
| acd2de4553 | |||
| 31f593cca2 | |||
| 64bdcecdcd | |||
| bc0fd60e1a | |||
| a0cfb2777c | |||
| e8206b42d2 | |||
| 0737957f94 | |||
| 77892d571f | |||
| 06026684e5 | |||
| c3d45ae529 | |||
| acf19c1f1f | |||
| b3c880cc24 | |||
| 33d96d574f | |||
| b64d5af9c4 | |||
| 002dbf355a | |||
| 7d5414b349 | |||
| 2de2e3803a | |||
| 43a2fbf5ad | |||
| d6b295ba2c | |||
| 211fa52649 | |||
| 7f410ce5b4 | |||
| a3f6825f9c | |||
| ea0ec9e662 | |||
| 2e9e5d0555 | |||
| 92161aae2e | |||
| 3ffda83ba1 | |||
| d44923f64e | |||
| 15faf0f621 | |||
| a9f8a23e50 | |||
| 643a3f1206 | |||
| 7aafe2ed5a | |||
| ce90b85945 | |||
| 87888d42fc | |||
| 85ca9e9a57 | |||
| d9e9e3cdeb | |||
| 3e43eda42b | |||
| e7d6dc22b2 | |||
| da06f6a9e6 | |||
| 6a0b683e60 | |||
| 2b6d586987 | |||
| e528923878 | |||
| 8d1b0d9764 | |||
| 2fe7faea87 | |||
| eb313ec901 | |||
| 7873ab8601 | |||
| 348f064df1 | |||
| 8b9fe13c88 | |||
| 1f7dcc0fa6 | |||
| 908b9b5a85 | |||
| 807c3dd3ab | |||
| cac03995a3 | |||
| 6ea7da5178 | |||
| 49f1b3554c | |||
| 519df3f7d5 | |||
| 16272f634c | |||
| 0b9887505e | |||
| dd8ee38194 | |||
| 87de6cea1b | |||
| 3d1e4b5f18 | |||
| ef7b11ac01 | |||
| 0f85da77be | |||
| 96a9cf0d2d | |||
| ebb75f5e90 | |||
| a6c3d123ed | |||
| 324de7af93 | |||
| e79ceaeb75 | |||
| f361ee72e8 | |||
| 21ac1022ef | |||
| 68d91916e2 | |||
| 753f8aecc9 | |||
| 84e4c9bc42 | |||
| 1dbaa08cd4 | |||
| 1d916c35d1 | |||
| e8ba039a61 | |||
| 4b41f8cb5b | |||
| 0eb6acde96 | |||
| 73f3f75712 | |||
| 70a950119c | |||
| b8e9f47dd4 | |||
| a0a9b48224 | |||
| 9363959566 | |||
| f38129b5a0 | |||
| 6542509207 | |||
| 7d55c079c9 | |||
| e44fc3817a | |||
| 1414b88ef3 | |||
| ba520db7f7 | |||
| 85248083c0 | |||
| a91433d52b | |||
| d34bd346c0 | |||
| be09ed7e2b | |||
| d224dbe334 | |||
| c62168ca5b | |||
| d0b2b33dce | |||
| 7881660ca0 | |||
| 506e5b7f11 | |||
| a5cd7ce122 | |||
| a7bf8f7cc6 | |||
| 451fbf0227 | |||
| 0eaffb37e7 | |||
| d985d044bc | |||
| c24b5ebe98 | |||
| fd900c288e | |||
| e86d88d0cf | |||
| f28c26fd54 | |||
| 421430330a | |||
| 5c2106b22e | |||
| 48241deba3 | |||
| 9cc11be9f3 | |||
| e48d6b039b | |||
| dd3c6892e9 | |||
| c552366a78 | |||
| 7215c72373 |
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -64,39 +64,23 @@ jobs:
|
||||
./gradlew publishMavenJavaPublicationToLocalRepository
|
||||
./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR"
|
||||
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" :runAllTests
|
||||
check-tangles:
|
||||
name: Check for Package Tangles
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'spring-projects' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Check for package tangles
|
||||
env:
|
||||
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
|
||||
run: |
|
||||
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
deploy-artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1
|
||||
with:
|
||||
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1
|
||||
with:
|
||||
should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
secrets: inherit
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build, test, check-samples, check-tangles ]
|
||||
needs: [ build, test, check-samples ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
@@ -116,7 +100,7 @@ jobs:
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ perform-release ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
if: ${{ !success() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
|
||||
@@ -2,9 +2,6 @@ name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@ dependencies {
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
testAspect sourceSets.main.output
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml"
|
||||
|
||||
+1
-7
@@ -20,7 +20,6 @@ plugins {
|
||||
|
||||
apply plugin: 'io.spring.nohttp'
|
||||
apply plugin: 'locks'
|
||||
apply plugin: 's101'
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
apply plugin: 'org.jetbrains.kotlin.jvm'
|
||||
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
|
||||
@@ -117,16 +116,11 @@ tasks.register('cloneRepository', IncludeRepoTask) {
|
||||
outputDirectory = project.hasProperty("cloneOutputDirectory") ? project.file("$cloneOutputDirectory") : defaultDirectory
|
||||
}
|
||||
|
||||
s101 {
|
||||
repository = 'https://structure101.com/binaries/latest'
|
||||
configurationDirectory = project.file("etc/s101")
|
||||
}
|
||||
|
||||
wrapperUpgrade {
|
||||
gradle {
|
||||
'spring-security' {
|
||||
repo = 'spring-projects/spring-security'
|
||||
baseBranch = '6.1.x' // runs only on 6.1.x and the update is merged forward to main
|
||||
baseBranch = '6.3.x' // runs only on 6.3.x and the update is merged forward to main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ dependencies {
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||
testImplementation libs.com.squareup.okhttp3.mockwebserver
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class S101Plugin implements Plugin<Project> {
|
||||
|
||||
private void configure(JavaExec exec) {
|
||||
exec.setDescription("Runs Structure101 headless analysis, installing and configuring if necessary");
|
||||
exec.dependsOn("check");
|
||||
exec.dependsOn("assemble");
|
||||
Project project = exec.getProject();
|
||||
S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class);
|
||||
exec
|
||||
|
||||
@@ -6,5 +6,7 @@ dependencies {
|
||||
implementation 'org.springframework:spring-core'
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
|
||||
@@ -5,4 +5,6 @@ dependencies {
|
||||
optional 'ch.qos.logback:logback-classic'
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -22,4 +22,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
testImplementation 'org.skyscreamer:jsonassert'
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ dependencies {
|
||||
testImplementation libs.org.instancio.instancio.junit
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
def rncToXsd = tasks.named('rncToXsd', RncToXsd)
|
||||
|
||||
+10
-49
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 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,8 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.annotation.authentication.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -63,62 +62,24 @@ class InitializeAuthenticationProviderBeanManagerConfigurer extends GlobalAuthen
|
||||
if (auth.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
List<BeanWithName<AuthenticationProvider>> authenticationProviders = getBeansWithName(
|
||||
AuthenticationProvider.class);
|
||||
if (authenticationProviders.isEmpty()) {
|
||||
String[] beanNames = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(AuthenticationProvider.class);
|
||||
if (beanNames.length == 0) {
|
||||
return;
|
||||
}
|
||||
else if (authenticationProviders.size() > 1) {
|
||||
List<String> beanNames = authenticationProviders.stream().map(BeanWithName::getName).toList();
|
||||
else if (beanNames.length > 1) {
|
||||
this.logger.info(LogMessage.format("Found %s AuthenticationProvider beans, with names %s. "
|
||||
+ "Global Authentication Manager will not be configured with AuthenticationProviders. "
|
||||
+ "Consider publishing a single AuthenticationProvider bean, or wiring your Providers directly "
|
||||
+ "using the DSL.", authenticationProviders.size(), beanNames));
|
||||
+ "using the DSL.", beanNames.length, Arrays.toString(beanNames)));
|
||||
return;
|
||||
}
|
||||
AuthenticationProvider authenticationProvider = authenticationProviders.get(0).getBean();
|
||||
String authenticationProviderBeanName = authenticationProviders.get(0).getName();
|
||||
|
||||
AuthenticationProvider authenticationProvider = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBean(beanNames[0], AuthenticationProvider.class);
|
||||
auth.authenticationProvider(authenticationProvider);
|
||||
this.logger.info(LogMessage.format(
|
||||
"Global AuthenticationManager configured with AuthenticationProvider bean with name %s",
|
||||
authenticationProviderBeanName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of beans of the requested class, along with their names. If
|
||||
* there are no registered beans of that type, the list is empty.
|
||||
*/
|
||||
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
|
||||
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
|
||||
String[] beanNames = InitializeAuthenticationProviderBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(type);
|
||||
for (String beanName : beanNames) {
|
||||
T bean = InitializeAuthenticationProviderBeanManagerConfigurer.this.context.getBean(beanName, type);
|
||||
beanWithNames.add(new BeanWithName<T>(bean, beanName));
|
||||
}
|
||||
return beanWithNames;
|
||||
}
|
||||
|
||||
static class BeanWithName<T> {
|
||||
|
||||
private final T bean;
|
||||
|
||||
private final String name;
|
||||
|
||||
BeanWithName(T bean, String name) {
|
||||
this.bean = bean;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
T getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
beanNames[0]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-48
@@ -16,8 +16,7 @@
|
||||
|
||||
package org.springframework.security.config.annotation.authentication.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -66,9 +65,10 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
|
||||
@Override
|
||||
public void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
List<BeanWithName<UserDetailsService>> userDetailsServices = getBeansWithName(UserDetailsService.class);
|
||||
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
|
||||
.getBeanNamesForType(UserDetailsService.class);
|
||||
if (auth.isConfigured()) {
|
||||
if (!userDetailsServices.isEmpty()) {
|
||||
if (beanNames.length > 0) {
|
||||
this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. "
|
||||
+ "UserDetailsService beans will not be used for username/password login. "
|
||||
+ "Consider removing the AuthenticationProvider bean. "
|
||||
@@ -78,19 +78,18 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
return;
|
||||
}
|
||||
|
||||
if (userDetailsServices.isEmpty()) {
|
||||
if (beanNames.length == 0) {
|
||||
return;
|
||||
}
|
||||
else if (userDetailsServices.size() > 1) {
|
||||
List<String> beanNames = userDetailsServices.stream().map(BeanWithName::getName).toList();
|
||||
else if (beanNames.length > 1) {
|
||||
this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. "
|
||||
+ "Global Authentication Manager will not use a UserDetailsService for username/password login. "
|
||||
+ "Consider publishing a single UserDetailsService bean.", userDetailsServices.size(),
|
||||
beanNames));
|
||||
+ "Consider publishing a single UserDetailsService bean.", beanNames.length,
|
||||
Arrays.toString(beanNames)));
|
||||
return;
|
||||
}
|
||||
UserDetailsService userDetailsService = userDetailsServices.get(0).getBean();
|
||||
String userDetailsServiceBeanName = userDetailsServices.get(0).getName();
|
||||
UserDetailsService userDetailsService = InitializeUserDetailsBeanManagerConfigurer.this.context
|
||||
.getBean(beanNames[0], UserDetailsService.class);
|
||||
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
|
||||
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
|
||||
CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class);
|
||||
@@ -111,8 +110,7 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
provider.afterPropertiesSet();
|
||||
auth.authenticationProvider(provider);
|
||||
this.logger.info(LogMessage.format(
|
||||
"Global AuthenticationManager configured with UserDetailsService bean with name %s",
|
||||
userDetailsServiceBeanName));
|
||||
"Global AuthenticationManager configured with UserDetailsService bean with name %s", beanNames[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,41 +125,6 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
|
||||
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of beans of the requested class, along with their names. If
|
||||
* there are no registered beans of that type, the list is empty.
|
||||
*/
|
||||
private <T> List<BeanWithName<T>> getBeansWithName(Class<T> type) {
|
||||
List<BeanWithName<T>> beanWithNames = new ArrayList<>();
|
||||
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type);
|
||||
for (String beanName : beanNames) {
|
||||
T bean = InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanName, type);
|
||||
beanWithNames.add(new BeanWithName<T>(bean, beanName));
|
||||
}
|
||||
return beanWithNames;
|
||||
}
|
||||
|
||||
static class BeanWithName<T> {
|
||||
|
||||
private final T bean;
|
||||
|
||||
private final String name;
|
||||
|
||||
BeanWithName(T bean, String name) {
|
||||
this.bean = bean;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
T getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -98,6 +98,10 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
private AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer = new AuthorizationCodeGrantConfigurer();
|
||||
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
/**
|
||||
* Sets the repository of client registrations.
|
||||
* @param clientRegistrationRepository the repository of client registrations
|
||||
@@ -107,6 +111,7 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -119,6 +124,7 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository);
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -283,8 +289,7 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (this.authorizationRequestResolver != null) {
|
||||
return this.authorizationRequestResolver;
|
||||
}
|
||||
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
|
||||
.getClientRegistrationRepository(getBuilder());
|
||||
ClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(getBuilder());
|
||||
return new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
|
||||
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
|
||||
}
|
||||
@@ -292,8 +297,8 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
private OAuth2AuthorizationCodeGrantFilter createAuthorizationCodeGrantFilter(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter(
|
||||
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder),
|
||||
OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), authenticationManager);
|
||||
getClientRegistrationRepository(builder), getAuthorizedClientRepository(builder),
|
||||
authenticationManager);
|
||||
if (this.authorizationRequestRepository != null) {
|
||||
authorizationCodeGrantFilter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
|
||||
}
|
||||
@@ -315,6 +320,18 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
|
||||
private ClientRegistrationRepository getClientRegistrationRepository(B builder) {
|
||||
return (OAuth2ClientConfigurer.this.clientRegistrationRepository != null)
|
||||
? OAuth2ClientConfigurer.this.clientRegistrationRepository
|
||||
: OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder);
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientRepository getAuthorizedClientRepository(B builder) {
|
||||
return (OAuth2ClientConfigurer.this.authorizedClientRepository != null)
|
||||
? OAuth2ClientConfigurer.this.authorizedClientRepository
|
||||
: OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getBeanOrNull(ResolvableType type) {
|
||||
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
|
||||
|
||||
+20
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -172,6 +172,10 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
private String loginProcessingUrl = OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
|
||||
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
/**
|
||||
* Sets the repository of client registrations.
|
||||
* @param clientRegistrationRepository the repository of client registrations
|
||||
@@ -181,6 +185,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -194,6 +199,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
|
||||
this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository);
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -339,8 +345,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
@Override
|
||||
public void init(B http) throws Exception {
|
||||
OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter(
|
||||
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
|
||||
OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()), this.loginProcessingUrl);
|
||||
this.getClientRegistrationRepository(), this.getAuthorizedClientRepository(), this.loginProcessingUrl);
|
||||
authenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
this.setAuthenticationFilter(authenticationFilter);
|
||||
super.loginProcessingUrl(this.loginProcessingUrl);
|
||||
@@ -406,8 +411,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
|
||||
}
|
||||
authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
|
||||
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
|
||||
authorizationRequestBaseUri);
|
||||
this.getClientRegistrationRepository(), authorizationRequestBaseUri);
|
||||
}
|
||||
if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {
|
||||
authorizationRequestFilter
|
||||
@@ -439,6 +443,16 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return new AntPathRequestMatcher(loginProcessingUrl);
|
||||
}
|
||||
|
||||
private ClientRegistrationRepository getClientRegistrationRepository() {
|
||||
return (this.clientRegistrationRepository != null) ? this.clientRegistrationRepository
|
||||
: OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder());
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
|
||||
return (this.authorizedClientRepository != null) ? this.authorizedClientRepository
|
||||
: OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private JwtDecoderFactory<ClientRegistration> getJwtDecoderFactoryBean() {
|
||||
ResolvableType type = ResolvableType.forClassWithGenerics(JwtDecoderFactory.class, ClientRegistration.class);
|
||||
@@ -529,8 +543,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, String> getLoginLinks() {
|
||||
Iterable<ClientRegistration> clientRegistrations = null;
|
||||
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
|
||||
.getClientRegistrationRepository(this.getBuilder());
|
||||
ClientRegistrationRepository clientRegistrationRepository = this.getClientRegistrationRepository();
|
||||
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
|
||||
if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
|
||||
clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
|
||||
|
||||
+3
@@ -183,6 +183,9 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
|
||||
BeanDefinitionBuilder csrfAuthenticationStrategy = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(CsrfAuthenticationStrategy.class);
|
||||
csrfAuthenticationStrategy.addConstructorArgReference(this.csrfRepositoryRef);
|
||||
if (StringUtils.hasText(this.requestHandlerRef)) {
|
||||
csrfAuthenticationStrategy.addPropertyReference("requestHandler", this.requestHandlerRef);
|
||||
}
|
||||
return csrfAuthenticationStrategy.getBeanDefinition();
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -147,6 +147,7 @@ final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
|
||||
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
|
||||
.addConstructorArgValue(logoutRequestResolver)
|
||||
.addPropertyValue("logoutRequestRepository", logoutRequestRepository)
|
||||
.getBeanDefinition();
|
||||
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
|
||||
.addConstructorArgValue(saml2LogoutRequestSuccessHandler)
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.config.web.server;
|
||||
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.GsonHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for {@link HttpMessageConverter}'s.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author luamas
|
||||
* @since 5.1
|
||||
*/
|
||||
final class HttpMessageConverters {
|
||||
|
||||
private static final boolean jackson2Present;
|
||||
|
||||
private static final boolean gsonPresent;
|
||||
|
||||
private static final boolean jsonbPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|
||||
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
||||
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
|
||||
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
|
||||
}
|
||||
|
||||
private HttpMessageConverters() {
|
||||
}
|
||||
|
||||
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
|
||||
if (jackson2Present) {
|
||||
return new MappingJackson2HttpMessageConverter();
|
||||
}
|
||||
if (gsonPresent) {
|
||||
return new GsonHttpMessageConverter();
|
||||
}
|
||||
if (jsonbPresent) {
|
||||
return new JsonbHttpMessageConverter();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.config.web.server;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.HttpMessageEncoder;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
class OAuth2ErrorEncoder implements HttpMessageEncoder<OAuth2Error> {
|
||||
|
||||
private final HttpMessageConverter<Object> messageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<MediaType> getStreamingMediaTypes() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
|
||||
return getEncodableMimeTypes().contains(mimeType);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Flux<DataBuffer> encode(Publisher<? extends OAuth2Error> error, DataBufferFactory bufferFactory,
|
||||
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
|
||||
return Mono.from(error).flatMap((data) -> {
|
||||
ByteArrayHttpOutputMessage bytes = new ByteArrayHttpOutputMessage();
|
||||
try {
|
||||
this.messageConverter.write(data, MediaType.APPLICATION_JSON, bytes);
|
||||
return Mono.just(bytes.getBody().toByteArray());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
}).map(bufferFactory::wrap).flux();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<MimeType> getEncodableMimeTypes() {
|
||||
return List.of(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
private static class ByteArrayHttpOutputMessage implements HttpOutputMessage {
|
||||
|
||||
private final ByteArrayOutputStream body = new ByteArrayOutputStream();
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ByteArrayOutputStream getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return new HttpHeaders();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+15
-18
@@ -16,16 +16,17 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
@@ -62,6 +63,9 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
|
||||
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
|
||||
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
|
||||
new OAuth2ErrorEncoder());
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
|
||||
@@ -84,7 +88,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
|
||||
})
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap(this.authenticationManager::authenticate)
|
||||
@@ -93,7 +97,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
if (ex instanceof AuthenticationServiceException) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
|
||||
return handleAuthenticationFailure(exchange, ex).then(Mono.empty());
|
||||
})
|
||||
.flatMap((authentication) -> {
|
||||
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
@@ -101,19 +105,12 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
|
||||
private Mono<Void> handleAuthenticationFailure(ServerWebExchange exchange, Exception ex) {
|
||||
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
|
||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
OAuth2Error error = oauth2Error(ex);
|
||||
byte[] bytes = String.format("""
|
||||
{
|
||||
"error_code": "%s",
|
||||
"error_description": "%s",
|
||||
"error_uri: "%s"
|
||||
}
|
||||
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Flux.just(buffer));
|
||||
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
return this.errorHttpMessageConverter.write(Mono.just(oauth2Error(ex)), ResolvableType.forClass(Object.class),
|
||||
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
|
||||
exchange.getResponse(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
private OAuth2Error oauth2Error(Exception ex) {
|
||||
|
||||
+15
-16
@@ -16,8 +16,8 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -25,14 +25,15 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
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;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
|
||||
@@ -44,6 +45,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.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@@ -63,6 +65,9 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
|
||||
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
||||
|
||||
private final HttpMessageWriter<OAuth2Error> errorHttpMessageConverter = new EncoderHttpMessageWriter<>(
|
||||
new OAuth2ErrorEncoder());
|
||||
|
||||
private WebClient web = WebClient.create();
|
||||
|
||||
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
|
||||
@@ -97,7 +102,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
totalCount.intValue()));
|
||||
}
|
||||
if (!list.isEmpty()) {
|
||||
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
|
||||
return handleLogoutFailure(exchange.getExchange(), oauth2Error(list));
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
@@ -148,17 +153,11 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
|
||||
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
byte[] bytes = String.format("""
|
||||
{
|
||||
"error_code": "%s",
|
||||
"error_description": "%s",
|
||||
"error_uri: "%s"
|
||||
}
|
||||
""", error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Flux.just(buffer));
|
||||
private Mono<Void> handleLogoutFailure(ServerWebExchange exchange, OAuth2Error error) {
|
||||
exchange.getResponse().setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||
return this.errorHttpMessageConverter.write(Mono.just(error), ResolvableType.forClass(Object.class),
|
||||
ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON, exchange.getRequest(),
|
||||
exchange.getResponse(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -47,6 +47,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
@@ -1007,6 +1008,21 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThat(service.getIdPath("uid")).isEqualTo("uid");
|
||||
}
|
||||
|
||||
// gh-16819
|
||||
@Test
|
||||
void autowireWhenDefaultsThenAdvisorAnnotationsAreSorted() {
|
||||
this.spring.register(MethodSecurityServiceConfig.class).autowire();
|
||||
AuthorizationAdvisorProxyFactory proxyFactory = this.spring.getContext()
|
||||
.getBean(AuthorizationAdvisorProxyFactory.class);
|
||||
AnnotationAwareOrderComparator comparator = AnnotationAwareOrderComparator.INSTANCE;
|
||||
AuthorizationAdvisor previous = null;
|
||||
for (AuthorizationAdvisor advisor : proxyFactory) {
|
||||
boolean ordered = previous == null || comparator.compare(previous, advisor) < 0;
|
||||
assertThat(ordered).isTrue();
|
||||
previous = advisor;
|
||||
}
|
||||
}
|
||||
|
||||
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
|
||||
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
|
||||
}
|
||||
|
||||
+92
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -75,6 +75,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
@@ -285,6 +286,49 @@ public class OAuth2ClientConfigurerTests {
|
||||
verify(authorizationRedirectStrategy).sendRedirect(any(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenOAuth2LoginBeansConfiguredThenNotShared() throws Exception {
|
||||
this.spring.register(OAuth2ClientConfigWithOAuth2Login.class).autowire();
|
||||
// Setup the Authorization Request in the session
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, this.registration1.getRegistrationId());
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri(this.registration1.getProviderDetails().getAuthorizationUri())
|
||||
.clientId(this.registration1.getClientId())
|
||||
.redirectUri("http://localhost/client-1")
|
||||
.state("state")
|
||||
.attributes(attributes)
|
||||
.build();
|
||||
// @formatter:on
|
||||
AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
|
||||
MockHttpSession session = (MockHttpSession) request.getSession();
|
||||
String principalName = "user1";
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password");
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder clientRequest = get("/client-1")
|
||||
.param(OAuth2ParameterNames.CODE, "code")
|
||||
.param(OAuth2ParameterNames.STATE, "state")
|
||||
.with(authentication(authentication))
|
||||
.session(session);
|
||||
this.mockMvc.perform(clientRequest)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/client-1"));
|
||||
// @formatter:on
|
||||
OAuth2AuthorizedClient authorizedClient = authorizedClientRepository
|
||||
.loadAuthorizedClient(this.registration1.getRegistrationId(), authentication, request);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
// Ensure shared objects set for OAuth2 Client are not used
|
||||
ClientRegistrationRepository clientRegistrationRepository = this.spring.getContext()
|
||||
.getBean(ClientRegistrationRepository.class);
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext()
|
||||
.getBean(OAuth2AuthorizedClientRepository.class);
|
||||
verifyNoInteractions(clientRegistrationRepository, authorizedClientRepository);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@@ -362,4 +406,51 @@ public class OAuth2ClientConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class OAuth2ClientConfigWithOAuth2Login {
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository = mock(
|
||||
ClientRegistrationRepository.class);
|
||||
|
||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository = mock(
|
||||
OAuth2AuthorizedClientRepository.class);
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2Client((oauth2Client) -> oauth2Client
|
||||
.clientRegistrationRepository(OAuth2ClientConfigurerTests.clientRegistrationRepository)
|
||||
.authorizedClientService(OAuth2ClientConfigurerTests.authorizedClientService)
|
||||
.authorizationCodeGrant((authorizationCode) -> authorizationCode
|
||||
.authorizationRequestResolver(authorizationRequestResolver)
|
||||
.authorizationRedirectStrategy(authorizationRedirectStrategy)
|
||||
.accessTokenResponseClient(accessTokenResponseClient)
|
||||
)
|
||||
)
|
||||
.oauth2Login((oauth2Login) -> oauth2Login
|
||||
.clientRegistrationRepository(this.clientRegistrationRepository)
|
||||
.authorizedClientRepository(this.authorizedClientRepository)
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clientRegistrationRepository() {
|
||||
return this.clientRegistrationRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository() {
|
||||
return this.authorizedClientRepository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+67
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -73,7 +73,9 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
@@ -115,6 +117,7 @@ import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.setAuthentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
@@ -669,6 +672,30 @@ public class OAuth2LoginConfigurerTests {
|
||||
.collect(Collectors.toList())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oidcLoginWhenOAuth2ClientBeansConfiguredThenNotShared() throws Exception {
|
||||
this.spring.register(OAuth2LoginConfigWithOAuth2Client.class, JwtDecoderFactoryConfig.class).autowire();
|
||||
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, this.request, this.response);
|
||||
this.request.setParameter("code", "code123");
|
||||
this.request.setParameter("state", authorizationRequest.getState());
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain);
|
||||
Authentication authentication = this.securityContextRepository
|
||||
.loadContext(new HttpRequestResponseHolder(this.request, this.response))
|
||||
.getAuthentication();
|
||||
assertThat(authentication.getAuthorities()).hasSize(1);
|
||||
assertThat(authentication.getAuthorities()).first()
|
||||
.isInstanceOf(OidcUserAuthority.class)
|
||||
.hasToString("OIDC_USER");
|
||||
|
||||
// Ensure shared objects set for OAuth2 Client are not used
|
||||
ClientRegistrationRepository clientRegistrationRepository = this.spring.getContext()
|
||||
.getBean(ClientRegistrationRepository.class);
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext()
|
||||
.getBean(OAuth2AuthorizedClientRepository.class);
|
||||
verifyNoInteractions(clientRegistrationRepository, authorizedClientRepository);
|
||||
}
|
||||
|
||||
private void loadConfig(Class<?>... configs) {
|
||||
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
|
||||
applicationContext.register(configs);
|
||||
@@ -1192,6 +1219,45 @@ public class OAuth2LoginConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginConfigWithOAuth2Client extends CommonLambdaSecurityFilterChainConfig {
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository = mock(
|
||||
ClientRegistrationRepository.class);
|
||||
|
||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository = mock(
|
||||
OAuth2AuthorizedClientRepository.class);
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.oauth2Login((oauth2Login) -> oauth2Login
|
||||
.clientRegistrationRepository(
|
||||
new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION))
|
||||
.authorizedClientRepository(new HttpSessionOAuth2AuthorizedClientRepository())
|
||||
)
|
||||
.oauth2Client((oauth2Client) -> oauth2Client
|
||||
.clientRegistrationRepository(this.clientRegistrationRepository)
|
||||
.authorizedClientRepository(this.authorizedClientRepository)
|
||||
);
|
||||
// @formatter:on
|
||||
return super.configureFilterChain(http);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientRegistrationRepository clientRegistrationRepository() {
|
||||
return this.clientRegistrationRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository() {
|
||||
return this.authorizedClientRepository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract static class CommonSecurityFilterChainConfig {
|
||||
|
||||
SecurityFilterChain configureFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -1560,12 +1560,15 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
|
||||
defaultBearerTokenResolver.setAllowUriQueryParameter(true);
|
||||
http
|
||||
.authorizeRequests()
|
||||
.requestMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.bearerTokenResolver(defaultBearerTokenResolver)
|
||||
.jwt()
|
||||
.jwkSetUri(this.jwkSetUri);
|
||||
return http.build();
|
||||
|
||||
+1
@@ -484,6 +484,7 @@ public class Saml2LogoutConfigurerTests {
|
||||
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
|
||||
}
|
||||
|
||||
// gh-11363
|
||||
@Test
|
||||
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
|
||||
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
|
||||
|
||||
@@ -336,6 +336,43 @@ public class CsrfConfigTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postWhenUsingCsrfAndXorCsrfTokenRequestAttributeHandlerThenCsrfAuthenticationStrategyUses()
|
||||
throws Exception {
|
||||
this.spring.configLocations(this.xml("WithXorCsrfTokenRequestAttributeHandler"), this.xml("shared-controllers"))
|
||||
.autowire();
|
||||
// @formatter:off
|
||||
MvcResult mvcResult1 = this.mvc.perform(get("/csrf"))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
MockHttpServletRequest request1 = mvcResult1.getRequest();
|
||||
MockHttpSession session = (MockHttpSession) request1.getSession();
|
||||
CsrfTokenRepository repository = WebTestUtils.getCsrfTokenRepository(request1);
|
||||
// @formatter:off
|
||||
MockHttpServletRequestBuilder login = post("/login")
|
||||
.param("username", "user")
|
||||
.param("password", "password")
|
||||
.session(session)
|
||||
.with(csrf());
|
||||
this.mvc.perform(login)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/"));
|
||||
// @formatter:on
|
||||
assertThat(repository.loadToken(request1)).isNull();
|
||||
// @formatter:off
|
||||
MvcResult mvcResult2 = this.mvc.perform(get("/csrf").session(session))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
MockHttpServletRequest request2 = mvcResult2.getRequest();
|
||||
CsrfToken csrfToken = repository.loadToken(request2);
|
||||
CsrfToken csrfTokenAttribute = (CsrfToken) request2.getAttribute(CsrfToken.class.getName());
|
||||
assertThat(csrfTokenAttribute).isNotNull();
|
||||
assertThat(csrfTokenAttribute.getToken()).isNotBlank();
|
||||
assertThat(csrfTokenAttribute.getToken()).isNotEqualTo(csrfToken.getToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postWhenHasCsrfTokenButSessionExpiresThenRequestIsCancelledAfterSuccessfulAuthentication()
|
||||
throws Exception {
|
||||
|
||||
+17
@@ -63,6 +63,7 @@ import org.springframework.web.util.UriUtils;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -380,6 +381,22 @@ public class Saml2LogoutBeanDefinitionParserTests {
|
||||
verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
|
||||
}
|
||||
|
||||
// gh-11363
|
||||
@Test
|
||||
public void saml2LogoutWhenCustomLogoutRequestRepositoryThenUses() throws Exception {
|
||||
this.spring.configLocations(this.xml("CustomComponents")).autowire();
|
||||
RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
|
||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
||||
.samlRequest(this.rpLogoutRequest)
|
||||
.id(this.rpLogoutRequestId)
|
||||
.relayState(this.rpLogoutRequestRelayState)
|
||||
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature))
|
||||
.build();
|
||||
given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest);
|
||||
this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf()));
|
||||
verify(getBean(Saml2LogoutRequestRepository.class)).saveLogoutRequest(eq(logoutRequest), any(), any());
|
||||
}
|
||||
|
||||
private <T> T getBean(Class<T> clazz) {
|
||||
return this.spring.getContext().getBean(clazz);
|
||||
}
|
||||
|
||||
+10
-4
@@ -50,6 +50,7 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
@@ -99,6 +100,7 @@ import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
@@ -197,7 +199,10 @@ public class OidcLogoutSpecTests {
|
||||
.body(BodyInserters.fromFormData("logout_token", "invalid"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest();
|
||||
.isBadRequest()
|
||||
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
|
||||
})
|
||||
.value(hasValue("invalid_request"));
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@@ -264,9 +269,10 @@ public class OidcLogoutSpecTests {
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest()
|
||||
.expectBody(String.class)
|
||||
.value(containsString("partial_logout"))
|
||||
.value(containsString("not all sessions were terminated"));
|
||||
.expectBody(new ParameterizedTypeReference<Map<String, String>>() {
|
||||
})
|
||||
.value(hasValue("partial_logout"))
|
||||
.value(hasValue(containsString("not all sessions were terminated")));
|
||||
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -25,10 +25,15 @@
|
||||
|
||||
<c:property-placeholder local-override="true"/>
|
||||
|
||||
<b:bean id="bearerTokenResolver"
|
||||
class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver">
|
||||
<b:property name="allowUriQueryParameter" value="true"/>
|
||||
</b:bean>
|
||||
|
||||
<http>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<intercept-url pattern="/requires-read-scope" access="hasAuthority('SCOPE_message:read')"/>
|
||||
<oauth2-resource-server>
|
||||
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver">
|
||||
<jwt jwk-set-uri="${jwk-set-uri:https://idp.example.org}"/>
|
||||
</oauth2-resource-server>
|
||||
</http>
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
testImplementation 'org.springframework:spring-test'
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
task springVersion(type: org.gradle.api.tasks.WriteProperties) {
|
||||
|
||||
+17
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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,6 +42,7 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authorization.AuthorizationProxyFactory;
|
||||
@@ -75,7 +76,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @since 6.3
|
||||
*/
|
||||
public final class AuthorizationAdvisorProxyFactory
|
||||
implements AuthorizationProxyFactory, Iterable<AuthorizationAdvisor> {
|
||||
implements AuthorizationProxyFactory, Iterable<AuthorizationAdvisor>, SmartInitializingSingleton {
|
||||
|
||||
private static final boolean isReactivePresent = ClassUtils.isPresent("reactor.core.publisher.Mono", null);
|
||||
|
||||
@@ -108,7 +109,9 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize());
|
||||
advisors.add(new PreFilterAuthorizationMethodInterceptor());
|
||||
advisors.add(new PostFilterAuthorizationMethodInterceptor());
|
||||
return new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AnnotationAwareOrderComparator.sort(factory.advisors);
|
||||
return factory;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +126,14 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize());
|
||||
advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor());
|
||||
advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor());
|
||||
return new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(advisors);
|
||||
AnnotationAwareOrderComparator.sort(factory.advisors);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
AnnotationAwareOrderComparator.sort(this.advisors);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +156,6 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
*/
|
||||
@Override
|
||||
public Object proxy(Object target) {
|
||||
AnnotationAwareOrderComparator.sort(this.advisors);
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -155,9 +164,9 @@ public final class AuthorizationAdvisorProxyFactory
|
||||
return proxied;
|
||||
}
|
||||
ProxyFactory factory = new ProxyFactory(target);
|
||||
for (Advisor advisor : this.advisors) {
|
||||
factory.addAdvisors(advisor);
|
||||
}
|
||||
List<Advisor> advisors = new ArrayList<>(this.advisors);
|
||||
AnnotationAwareOrderComparator.sort(advisors);
|
||||
factory.addAdvisors(advisors);
|
||||
factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers()));
|
||||
return factory.getProxy();
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -48,7 +48,7 @@ public final class AuthorizeReturnObjectMethodInterceptor implements Authorizati
|
||||
private int order = AuthorizationInterceptorsOrder.SECURE_RESULT.getOrder();
|
||||
|
||||
public AuthorizeReturnObjectMethodInterceptor(AuthorizationProxyFactory authorizationProxyFactory) {
|
||||
Assert.notNull(authorizationProxyFactory, "authorizationManager cannot be null");
|
||||
Assert.notNull(authorizationProxyFactory, "authorizationProxyFactory cannot be null");
|
||||
this.authorizationProxyFactory = authorizationProxyFactory;
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -19,6 +19,7 @@ package org.springframework.security.authorization.method;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
||||
|
||||
@@ -43,9 +44,24 @@ final class ExpressionUtils {
|
||||
"SpEL expression must return either a Boolean or an AuthorizationDecision");
|
||||
}
|
||||
catch (EvaluationException ex) {
|
||||
AuthorizationDeniedException denied = findAuthorizationException(ex);
|
||||
if (denied != null) {
|
||||
throw denied;
|
||||
}
|
||||
throw new IllegalArgumentException("Failed to evaluate expression '" + expr.getExpressionString() + "'",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
static AuthorizationDeniedException findAuthorizationException(EvaluationException ex) {
|
||||
Throwable cause = ex.getCause();
|
||||
while (cause != null) {
|
||||
if (cause instanceof AuthorizationDeniedException denied) {
|
||||
return denied;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -38,6 +38,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.authentication.TestAuthentication;
|
||||
@@ -336,6 +337,32 @@ public class AuthorizationAdvisorProxyFactoryTests {
|
||||
assertThat(factory.proxy(35)).isEqualTo(35);
|
||||
}
|
||||
|
||||
// gh-16819
|
||||
@Test
|
||||
void advisorsWhenWithDefaultsThenAreSorted() {
|
||||
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
|
||||
AnnotationAwareOrderComparator comparator = AnnotationAwareOrderComparator.INSTANCE;
|
||||
AuthorizationAdvisor previous = null;
|
||||
for (AuthorizationAdvisor advisor : proxyFactory) {
|
||||
boolean ordered = previous == null || comparator.compare(previous, advisor) < 0;
|
||||
assertThat(ordered).isTrue();
|
||||
previous = advisor;
|
||||
}
|
||||
}
|
||||
|
||||
// gh-16819
|
||||
@Test
|
||||
void advisorsWhenWithReactiveDefaultsThenAreSorted() {
|
||||
AuthorizationAdvisorProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withReactiveDefaults();
|
||||
AnnotationAwareOrderComparator comparator = AnnotationAwareOrderComparator.INSTANCE;
|
||||
AuthorizationAdvisor previous = null;
|
||||
for (AuthorizationAdvisor advisor : proxyFactory) {
|
||||
boolean ordered = previous == null || comparator.compare(previous, advisor) < 0;
|
||||
assertThat(ordered).isTrue();
|
||||
previous = advisor;
|
||||
}
|
||||
}
|
||||
|
||||
private Authentication authenticated(String user, String... authorities) {
|
||||
return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
|
||||
}
|
||||
|
||||
+15
@@ -22,9 +22,11 @@ import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
public class ExpressionUtilsTests {
|
||||
|
||||
@@ -48,10 +50,23 @@ public class ExpressionUtilsTests {
|
||||
assertThat(ExpressionUtils.evaluate(expression, context)).isInstanceOf(ExpressionAuthorizationDecision.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateWhenExpressionThrowsAuthorizationDeniedExceptionThenPropagates() {
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expression = parser.parseExpression("#root.throwException()");
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(this);
|
||||
assertThatExceptionOfType(AuthorizationDeniedException.class)
|
||||
.isThrownBy(() -> ExpressionUtils.evaluate(expression, context));
|
||||
}
|
||||
|
||||
public AuthorizationDecision returnDecision() {
|
||||
return new AuthorizationDecisionDetails(false, this.details);
|
||||
}
|
||||
|
||||
public Object throwException() {
|
||||
throw new AuthorizationDeniedException("denied", new AuthorizationDecision(false));
|
||||
}
|
||||
|
||||
public boolean returnResult() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -611,6 +611,10 @@ public class BCrypt {
|
||||
int rounds, off;
|
||||
StringBuilder rs = new StringBuilder();
|
||||
|
||||
// Enforce max length for new passwords only
|
||||
if (!for_check && passwordb.length > 72) {
|
||||
throw new IllegalArgumentException("password cannot be more than 72 bytes");
|
||||
}
|
||||
if (salt == null) {
|
||||
throw new IllegalArgumentException("salt cannot be null");
|
||||
}
|
||||
|
||||
+31
@@ -222,4 +222,35 @@ public class BCryptPasswordEncoderTests {
|
||||
assertThat(encoder.matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenPasswordOverMaxLengthThenThrowIllegalArgumentException() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
|
||||
String password72chars = "123456789012345678901234567890123456789012345678901234567890123456789012";
|
||||
encoder.encode(password72chars);
|
||||
|
||||
String password73chars = password72chars + "3";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> encoder.encode(password73chars));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenPasswordOverMaxLengthThenAllowToMatch() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
|
||||
String password71chars = "12345678901234567890123456789012345678901234567890123456789012345678901";
|
||||
String encodedPassword71chars = "$2a$10$jx3x2FaF.iX5QZ9i3O424Os2Ou5P5JrnedmWYHuDyX8JKA4Unp4xq";
|
||||
assertThat(encoder.matches(password71chars, encodedPassword71chars)).isTrue();
|
||||
|
||||
String password72chars = password71chars + "2";
|
||||
String encodedPassword72chars = "$2a$10$oXYO6/UvbsH5rQEraBkl6uheccBqdB3n.RaWbrimog9hS2GX4lo/O";
|
||||
assertThat(encoder.matches(password72chars, encodedPassword72chars)).isTrue();
|
||||
|
||||
// Max length is 72 bytes, however, we need to ensure backwards compatibility
|
||||
// for previously encoded passwords that are greater than 72 bytes and allow the
|
||||
// match to be performed.
|
||||
String password73chars = password72chars + "3";
|
||||
String encodedPassword73chars = "$2a$10$1l9.kvQTsqNLiCYFqmKtQOHkp.BrgIrwsnTzWo9jdbQRbuBYQ/AVK";
|
||||
assertThat(encoder.matches(password73chars, encodedPassword73chars)).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,4 +15,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ urls:
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
*** xref:servlet/appendix/faq.adoc[FAQ]
|
||||
* xref:reactive/index.adoc[Reactive Applications]
|
||||
** xref:reactive/getting-started.adoc[Getting Started]
|
||||
** Authentication
|
||||
** xref:reactive/authentication/index.adoc[Authentication]
|
||||
*** xref:reactive/authentication/x509.adoc[X.509 Authentication]
|
||||
*** xref:reactive/authentication/logout.adoc[Logout]
|
||||
*** Session Management
|
||||
|
||||
@@ -8,4 +8,4 @@ Once authentication is performed we know the identity and can perform authorizat
|
||||
|
||||
Spring Security provides built-in support for authenticating users.
|
||||
This section is dedicated to generic authentication support that applies in both Servlet and WebFlux environments.
|
||||
Refer to the sections on authentication for xref:servlet/authentication/index.adoc#servlet-authentication[Servlet] and xref:servlet/authentication/index.adoc[WebFlux] for details on what is supported for each stack.
|
||||
Refer to the sections on authentication for xref:servlet/authentication/index.adoc[Servlet] and xref:reactive/authentication/index.adoc[WebFlux] for details on what is supported for each stack.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
= Spring Security
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Spring Security's documentation can be https://docs.spring.io/spring-security/reference/spring-security-docs.zip[downloaded] as a zip file.
|
||||
====
|
||||
|
||||
Spring Security is a framework that provides xref:features/authentication/index.adoc[authentication], xref:features/authorization/index.adoc[authorization], and xref:features/exploits/index.adoc[protection against common attacks].
|
||||
With first class support for securing both xref:servlet/index.adoc[imperative] and xref:reactive/index.adoc[reactive] applications, it is the de-facto standard for securing Spring-based applications.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[[webflux-authentication]]
|
||||
= Authentication
|
||||
:page-section-summary-toc: 1
|
||||
@@ -649,7 +649,7 @@ class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
|
||||
|
||||
You would then add a bean of this type to your application context and inject it into the `LdapAuthenticationProvider`. This is covered in the section on configuring LDAP by using explicit Spring beans in the LDAP chapter of the reference manual.
|
||||
Note that you cannot use the namespace for configuration in this case.
|
||||
You should also consult the security-api-url[Javadoc] for the relevant classes and interfaces.
|
||||
You should also consult the {security-api-url}[Javadoc] for the relevant classes and interfaces.
|
||||
|
||||
|
||||
[[appendix-faq-namespace-post-processor]]
|
||||
|
||||
@@ -34,7 +34,7 @@ The attributes on the `<http>` element control some of the properties on the cor
|
||||
Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true)
|
||||
|
||||
[[nsa-http-authorization-manager-ref]]
|
||||
* **access-decision-manager-ref**
|
||||
* **use-authorization-manager**
|
||||
Use this AuthorizationManager instead of deriving one from <intercept-url> elements
|
||||
|
||||
[[nsa-http-access-decision-manager-ref]]
|
||||
|
||||
@@ -14,6 +14,7 @@ The following image shows the typical layering of the handlers for a single HTTP
|
||||
|
||||
.FilterChain
|
||||
[[servlet-filterchain-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/filterchain.png[]
|
||||
|
||||
The client sends a request to the application, and the container creates a `FilterChain`, which contains the `Filter` instances and `Servlet` that should process the `HttpServletRequest`, based on the path of the request URI.
|
||||
@@ -66,6 +67,7 @@ Here is a picture of how `DelegatingFilterProxy` fits into the <<servlet-filters
|
||||
|
||||
.DelegatingFilterProxy
|
||||
[[servlet-delegatingfilterproxy-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/delegatingfilterproxy.png[]
|
||||
|
||||
`DelegatingFilterProxy` looks up __Bean Filter~0~__ from the `ApplicationContext` and then invokes __Bean Filter~0~__.
|
||||
@@ -113,6 +115,7 @@ The following image shows the role of `FilterChainProxy`.
|
||||
|
||||
.FilterChainProxy
|
||||
[[servlet-filterchainproxy-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/filterchainproxy.png[]
|
||||
|
||||
[[servlet-securityfilterchain]]
|
||||
@@ -124,6 +127,7 @@ The following image shows the role of `SecurityFilterChain`.
|
||||
|
||||
.SecurityFilterChain
|
||||
[[servlet-securityfilterchain-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/securityfilterchain.png[]
|
||||
|
||||
The <<servlet-security-filters,Security Filters>> in `SecurityFilterChain` are typically Beans, but they are registered with `FilterChainProxy` instead of <<servlet-delegatingfilterproxy>>.
|
||||
@@ -145,6 +149,7 @@ The following image shows multiple `SecurityFilterChain` instances:
|
||||
|
||||
.Multiple SecurityFilterChain
|
||||
[[servlet-multi-securityfilterchain-figure]]
|
||||
[.invert-dark]
|
||||
image::{figures}/multi-securityfilterchain.png[]
|
||||
|
||||
In the <<servlet-multi-securityfilterchain-figure>> figure, `FilterChainProxy` decides which `SecurityFilterChain` should be used.
|
||||
@@ -398,6 +403,7 @@ The {security-api-url}org/springframework/security/web/access/ExceptionTranslati
|
||||
|
||||
The following image shows the relationship of `ExceptionTranslationFilter` to other components:
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/exceptiontranslationfilter.png[]
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ This also gives a good idea of the high level flow of authentication and how pie
|
||||
At the heart of Spring Security's authentication model is the `SecurityContextHolder`.
|
||||
It contains the <<servlet-authentication-securitycontext>>.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/securitycontextholder.png[]
|
||||
|
||||
The `SecurityContextHolder` is where Spring Security stores the details of who is xref:features/authentication/index.adoc#authentication[authenticated].
|
||||
@@ -175,6 +176,7 @@ While the implementation of `AuthenticationManager` could be anything, the most
|
||||
Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide.
|
||||
If none of the configured `AuthenticationProvider` instances can authenticate, authentication fails with a `ProviderNotFoundException`, which is a special `AuthenticationException` that indicates that the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/providermanager.png[]
|
||||
|
||||
In practice each `AuthenticationProvider` knows how to perform a specific type of authentication.
|
||||
@@ -184,11 +186,13 @@ This lets each `AuthenticationProvider` do a very specific type of authenticatio
|
||||
`ProviderManager` also allows configuring an optional parent `AuthenticationManager`, which is consulted in the event that no `AuthenticationProvider` can perform authentication.
|
||||
The parent can be any type of `AuthenticationManager`, but it is often an instance of `ProviderManager`.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/providermanager-parent.png[]
|
||||
|
||||
In fact, multiple `ProviderManager` instances might share the same parent `AuthenticationManager`.
|
||||
This is somewhat common in scenarios where there are multiple xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] instances that have some authentication in common (the shared parent `AuthenticationManager`), but also different authentication mechanisms (the different `ProviderManager` instances).
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/providermanagers-parent.png[]
|
||||
|
||||
[[servlet-authentication-providermanager-erasing-credentials]]
|
||||
@@ -234,6 +238,7 @@ Before the credentials can be authenticated, Spring Security typically requests
|
||||
|
||||
Next, the `AbstractAuthenticationProcessingFilter` can authenticate any authentication requests that are submitted to it.
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/abstractauthenticationprocessingfilter.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] When the user submits their credentials, the `AbstractAuthenticationProcessingFilter` creates an <<servlet-authentication-authentication,`Authentication`>> from the `HttpServletRequest` to be authenticated.
|
||||
|
||||
@@ -9,6 +9,7 @@ This section describes how HTTP Basic Authentication works within Spring Securit
|
||||
First, we see the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
|
||||
|
||||
.Sending WWW-Authenticate Header
|
||||
[.invert-dark]
|
||||
image::{figures}/basicauthenticationentrypoint.png[]
|
||||
|
||||
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
@@ -26,6 +27,7 @@ The following image shows the flow for the username and password being processed
|
||||
|
||||
[[servlet-authentication-basicauthenticationfilter]]
|
||||
.Authenticating Username and Password
|
||||
[.invert-dark]
|
||||
image::{figures}/basicauthenticationfilter.png[]
|
||||
|
||||
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ This section examines how `DaoAuthenticationProvider` works within Spring Securi
|
||||
The following figure explains the workings of the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section.
|
||||
|
||||
.`DaoAuthenticationProvider` Usage
|
||||
[.invert-dark]
|
||||
image::{figures}/daoauthenticationprovider.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] The authentication `Filter` from the xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] section passes a `UsernamePasswordAuthenticationToken` to the `AuthenticationManager`, which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
|
||||
|
||||
@@ -10,6 +10,7 @@ This section examines how form-based login works within Spring Security.
|
||||
First, we see how the user is redirected to the login form:
|
||||
|
||||
.Redirecting to the Login Page
|
||||
[.invert-dark]
|
||||
image::{figures}/loginurlauthenticationentrypoint.png[]
|
||||
|
||||
The preceding figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
@@ -30,6 +31,7 @@ When the username and password are submitted, the `UsernamePasswordAuthenticatio
|
||||
The `UsernamePasswordAuthenticationFilter` extends xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[AbstractAuthenticationProcessingFilter], so the following diagram should look pretty similar:
|
||||
|
||||
.Authenticating Username and Password
|
||||
[.invert-dark]
|
||||
image::{figures}/usernamepasswordauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
@@ -191,6 +191,7 @@ In Spring Security 6, the example shown above is the default configuration.
|
||||
|
||||
The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/securitycontextpersistencefilter.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
@@ -212,6 +213,7 @@ To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `
|
||||
|
||||
The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/securitycontextholderfilter.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
|
||||
|
||||
@@ -126,6 +126,7 @@ For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` a
|
||||
|
||||
[[authz-authorization-manager-implementations]]
|
||||
.Authorization Manager Implementations
|
||||
[.invert-dark]
|
||||
image::{figures}/authorizationhierarchy.png[]
|
||||
|
||||
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
|
||||
@@ -342,6 +343,7 @@ The following image shows the `AccessDecisionManager` interface:
|
||||
|
||||
[[authz-access-voting]]
|
||||
.Voting Decision Manager
|
||||
[.invert-dark]
|
||||
image::{figures}/access-decision-voting.png[]
|
||||
|
||||
By using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
|
||||
@@ -402,6 +404,7 @@ For example, on the Spring web site, you can find a https://spring.io/blog/2009/
|
||||
|
||||
[[authz-after-invocation]]
|
||||
.After Invocation Implementation
|
||||
[.invert-dark]
|
||||
image::{figures}/after-invocation.png[]
|
||||
|
||||
Like many other parts of Spring Security, `AfterInvocationManager` has a single concrete implementation, `AfterInvocationProviderManager`, which polls a list of ``AfterInvocationProvider``s.
|
||||
|
||||
@@ -65,6 +65,7 @@ In many cases, your authorization rules will be more sophisticated than that, so
|
||||
This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works at the request level in Servlet-based applications.
|
||||
|
||||
.Authorize HttpServletRequest
|
||||
[.invert-dark]
|
||||
image::{figures}/authorizationfilter.png[]
|
||||
|
||||
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` constructs a `Supplier` that retrieves an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
|
||||
@@ -1034,8 +1035,8 @@ public class SecurityConfig {
|
||||
http
|
||||
.securityMatcher("/api/**") <1>
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/user/**").hasRole("USER") <2>
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") <3>
|
||||
.requestMatchers("/api/user/**").hasRole("USER") <2>
|
||||
.requestMatchers("/api/admin/**").hasRole("ADMIN") <3>
|
||||
.anyRequest().authenticated() <4>
|
||||
)
|
||||
.formLogin(withDefaults());
|
||||
@@ -1057,8 +1058,8 @@ open class SecurityConfig {
|
||||
http {
|
||||
securityMatcher("/api/**") <1>
|
||||
authorizeHttpRequests {
|
||||
authorize("/user/**", hasRole("USER")) <2>
|
||||
authorize("/admin/**", hasRole("ADMIN")) <3>
|
||||
authorize("/api/user/**", hasRole("USER")) <2>
|
||||
authorize("/api/admin/**", hasRole("ADMIN")) <3>
|
||||
authorize(anyRequest, authenticated) <4>
|
||||
}
|
||||
}
|
||||
@@ -1070,8 +1071,8 @@ open class SecurityConfig {
|
||||
======
|
||||
|
||||
<1> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`
|
||||
<2> Allow access to URLs that start with `/user/` to users with the `USER` role
|
||||
<3> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role
|
||||
<2> Allow access to URLs that start with `/api/user/` to users with the `USER` role
|
||||
<3> Allow access to URLs that start with `/api/admin/` to users with the `ADMIN` role
|
||||
<4> Any other request that doesn't match the rules above, will require authentication
|
||||
|
||||
The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If {spring-framework-reference-url}web.html#spring-web[Spring MVC] is in the classpath, then {security-api-url}org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.html[`MvcRequestMatcher`] will be used, otherwise, {security-api-url}org/springframework/security/web/servlet/util/matcher/AntPathRequestMatcher.html[`AntPathRequestMatcher`] will be used.
|
||||
@@ -1097,8 +1098,8 @@ public class SecurityConfig {
|
||||
http
|
||||
.securityMatcher(antMatcher("/api/**")) <2>
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers(antMatcher("/user/**")).hasRole("USER") <3>
|
||||
.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") <4>
|
||||
.requestMatchers(antMatcher("/api/user/**")).hasRole("USER") <3>
|
||||
.requestMatchers(regexMatcher("/api/admin/.*")).hasRole("ADMIN") <4>
|
||||
.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") <5>
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
@@ -1132,8 +1133,8 @@ open class SecurityConfig {
|
||||
http {
|
||||
securityMatcher(antMatcher("/api/**")) <2>
|
||||
authorizeHttpRequests {
|
||||
authorize(antMatcher("/user/**"), hasRole("USER")) <3>
|
||||
authorize(regexMatcher("/admin/**"), hasRole("ADMIN")) <4>
|
||||
authorize(antMatcher("/api/user/**"), hasRole("USER")) <3>
|
||||
authorize(regexMatcher("/api/admin/**"), hasRole("ADMIN")) <4>
|
||||
authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR")) <5>
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
@@ -1147,8 +1148,8 @@ open class SecurityConfig {
|
||||
|
||||
<1> Import the static factory methods from `AntPathRequestMatcher` and `RegexRequestMatcher` to create `RequestMatcher` instances.
|
||||
<2> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`, using `AntPathRequestMatcher`
|
||||
<3> Allow access to URLs that start with `/user/` to users with the `USER` role, using `AntPathRequestMatcher`
|
||||
<4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
|
||||
<3> Allow access to URLs that start with `/api/user/` to users with the `USER` role, using `AntPathRequestMatcher`
|
||||
<4> Allow access to URLs that start with `/api/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
|
||||
<5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher`
|
||||
|
||||
== Further Reading
|
||||
|
||||
@@ -115,6 +115,7 @@ open class MyCustomerService {
|
||||
|
||||
A given invocation to `MyCustomerService#readCustomer` may look something like this when Method Security <<activate-method-security,is activated>>:
|
||||
|
||||
[.invert-dark]
|
||||
image::{figures}/methodsecurity.png[]
|
||||
|
||||
1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PreAuthorize` pointcut>>
|
||||
|
||||
@@ -82,6 +82,7 @@ To learn more about CSRF protection for your application, consider the following
|
||||
CSRF protection is provided by several components that are composed within the {security-api-url}org/springframework/security/web/csrf/CsrfFilter.html[`CsrfFilter`]:
|
||||
|
||||
.`CsrfFilter` Components
|
||||
[.invert-dark]
|
||||
image::{figures}/csrf.png[]
|
||||
|
||||
CSRF protection is divided into two parts:
|
||||
@@ -90,6 +91,7 @@ CSRF protection is divided into two parts:
|
||||
2. Determine if the request requires CSRF protection, load and validate the token, and <<csrf-access-denied-handler,handle `AccessDeniedException`>>.
|
||||
|
||||
.`CsrfFilter` Processing
|
||||
[.invert-dark]
|
||||
image::{figures}/csrf-processing.png[]
|
||||
|
||||
* image:{icondir}/number_1.png[] First, the {security-api-url}org/springframework/security/web/csrf/DeferredCsrfToken.html[`DeferredCsrfToken`] is loaded, which holds a reference to the <<csrf-token-repository,`CsrfTokenRepository`>> so that the persisted `CsrfToken` can be loaded later (in image:{icondir}/number_4.png[]).
|
||||
|
||||
@@ -205,6 +205,78 @@ This will ensure that:
|
||||
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
|
||||
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
|
||||
|
||||
[[migrating-spel-expressions]]
|
||||
=== Migrating SpEL Expressions
|
||||
|
||||
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
|
||||
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
|
||||
|
||||
However, to ease migration, you can also use a class like the following:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
|
||||
|
||||
private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
|
||||
|
||||
private Expression expression;
|
||||
|
||||
public MessageExpressionAuthorizationManager(String expressionString) {
|
||||
Assert.hasText(expressionString, "expressionString cannot be empty");
|
||||
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
|
||||
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
|
||||
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
|
||||
return new ExpressionAuthorizationDecision(granted, this.expression);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
And specify an instance for each matcher that you cannot get migrate:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
|
||||
messages
|
||||
// ...
|
||||
.simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
|
||||
// ...
|
||||
|
||||
return messages.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class WebSocketSecurityConfig {
|
||||
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
|
||||
messages
|
||||
// ..
|
||||
.simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
|
||||
// ...
|
||||
|
||||
return messages.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[websocket-authorization-notes]]
|
||||
=== WebSocket Authorization Notes
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ Now we can consider how Bearer Token Authentication works within Spring Security
|
||||
First, we see that, as with xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client:
|
||||
|
||||
.Sending WWW-Authenticate Header
|
||||
[.invert-dark]
|
||||
image::{figures}/bearerauthenticationentrypoint.png[]
|
||||
|
||||
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
@@ -38,6 +39,7 @@ The following image shows the flow for the bearer token being processed:
|
||||
|
||||
[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
|
||||
.Authenticating Bearer Token
|
||||
[.invert-dark]
|
||||
image::{figures}/bearertokenauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
||||
|
||||
@@ -92,6 +92,7 @@ Let's take a look at how `JwtAuthenticationProvider` works within Spring Securit
|
||||
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
|
||||
|
||||
.`JwtAuthenticationProvider` Usage
|
||||
[.invert-dark]
|
||||
image::{figures}/jwtauthenticationprovider.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
|
||||
|
||||
@@ -88,6 +88,7 @@ Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring
|
||||
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works.
|
||||
|
||||
.`OpaqueTokenAuthenticationProvider` Usage
|
||||
[.invert-dark]
|
||||
image::{figures}/opaquetokenauthenticationprovider.png[]
|
||||
|
||||
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
|
||||
|
||||
@@ -7,6 +7,7 @@ First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes
|
||||
It does this through a series of redirects:
|
||||
|
||||
.Redirecting to Asserting Party Authentication
|
||||
[.invert-dark]
|
||||
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
|
||||
|
||||
[NOTE]
|
||||
@@ -34,6 +35,7 @@ The following image shows how Spring Security authenticates a `<saml2:Response>`
|
||||
|
||||
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
|
||||
.Authenticating a `<saml2:Response>`
|
||||
[.invert-dark]
|
||||
image::{figures}/saml2webssoauthenticationfilter.png[]
|
||||
|
||||
[NOTE]
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
= Spring MVC Test Integration
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
Spring Security provides comprehensive integration with https://docs.spring.io/spring/docs/current/spring-framework-reference/html/testing.html#spring-mvc-test-framework[Spring MVC Test]
|
||||
Spring Security provides comprehensive integration with {spring-framework-reference-url}testing/mockmvc.html[Spring MVC Test]
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.6",
|
||||
"antora": "3.2.0-alpha.8",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-beta.5",
|
||||
"@antora/collector-extension": "1.0.1",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
|
||||
"@springio/antora-extensions": "1.14.4",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=3.1.1
|
||||
version=6.3.5
|
||||
version=6.3.9
|
||||
samplesBranch=6.3.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+14
-14
@@ -1,40 +1,40 @@
|
||||
[versions]
|
||||
com-squareup-okhttp3 = "3.14.9"
|
||||
io-rsocket = "1.1.4"
|
||||
io-rsocket = "1.1.5"
|
||||
io-spring-javaformat = "0.0.43"
|
||||
io-spring-nohttp = "0.0.11"
|
||||
jakarta-websocket = "2.1.1"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-apache-maven-resolver = "1.9.22"
|
||||
org-aspectj = "1.9.22.1"
|
||||
org-aspectj = "1.9.24"
|
||||
org-bouncycastle = "1.78.1"
|
||||
org-eclipse-jetty = "11.0.24"
|
||||
org-eclipse-jetty = "11.0.25"
|
||||
org-jetbrains-kotlin = "1.9.25"
|
||||
org-jetbrains-kotlinx = "1.8.1"
|
||||
org-mockito = "5.11.0"
|
||||
org-opensaml = "4.3.2"
|
||||
org-springframework = "6.1.15"
|
||||
org-springframework = "6.1.19"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.12"
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.18"
|
||||
com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.3"
|
||||
com-google-inject-guice = "com.google.inject:guice:3.0"
|
||||
com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.3"
|
||||
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:9.43.4"
|
||||
com-nimbusds-oauth2-oidc-sdk = "com.nimbusds:oauth2-oidc-sdk:9.43.6"
|
||||
com-squareup-okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "com-squareup-okhttp3" }
|
||||
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.13"
|
||||
io-mockk = "io.mockk:mockk:1.13.13"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.12"
|
||||
io-mockk = "io.mockk:mockk:1.13.17"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.17"
|
||||
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.3"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.4"
|
||||
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"
|
||||
@@ -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.3"
|
||||
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.16"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.6"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.8"
|
||||
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.10"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.12"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
|
||||
@@ -95,11 +95,11 @@ net-sourceforge-saxon-saxon = "net.sourceforge.saxon:saxon:9.1.0.8"
|
||||
org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33"
|
||||
org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.3.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.5"
|
||||
org-gretty-gretty = "org.gretty:gretty:4.1.6"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.14"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.22"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.24"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969"
|
||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
|
||||
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -206,7 +205,7 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
@@ -22,6 +22,7 @@ dependencies {
|
||||
|
||||
testRuntimeOnly project(':spring-security-config')
|
||||
testRuntimeOnly 'org.aspectj:aspectjweaver'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
System.setProperty('python.cachedir.skip', 'true')
|
||||
|
||||
+2
@@ -25,4 +25,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+2
@@ -25,4 +25,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+2
@@ -18,4 +18,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -17,4 +17,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+2
@@ -18,4 +18,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ dependencies {
|
||||
|
||||
testRuntimeOnly project(':spring-security-config')
|
||||
testRuntimeOnly project(':spring-security-ldap')
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
|
||||
@@ -35,6 +35,8 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
testImplementation 'org.skyscreamer:jsonassert'
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
|
||||
@@ -27,4 +27,5 @@ dependencies {
|
||||
testImplementation "ch.qos.logback:logback-classic"
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
provided 'jakarta.servlet:jakarta.servlet-api'
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 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.
|
||||
@@ -80,7 +80,13 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
|
||||
if (registration == null) {
|
||||
return null;
|
||||
}
|
||||
return (T) this.authorizedClients.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
|
||||
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients
|
||||
.get(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
|
||||
if (cachedAuthorizedClient == null) {
|
||||
return null;
|
||||
}
|
||||
return (T) new OAuth2AuthorizedClient(registration, cachedAuthorizedClient.getPrincipalName(),
|
||||
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+10
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 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.
|
||||
@@ -62,8 +62,15 @@ public final class InMemoryReactiveOAuth2AuthorizedClientService implements Reac
|
||||
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
|
||||
Assert.hasText(principalName, "principalName cannot be empty");
|
||||
return (Mono<T>) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
|
||||
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
|
||||
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
|
||||
.mapNotNull((clientRegistration) -> {
|
||||
OAuth2AuthorizedClientId id = new OAuth2AuthorizedClientId(clientRegistrationId, principalName);
|
||||
OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients.get(id);
|
||||
if (cachedAuthorizedClient == null) {
|
||||
return null;
|
||||
}
|
||||
return new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(),
|
||||
cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -245,7 +245,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
* be used to create an Authentication for saving.</li>
|
||||
* </ul>
|
||||
* @param authorizedClient the {@link OAuth2AuthorizedClient} to use.
|
||||
* @return the {@link Consumer} to populate the
|
||||
* @return the {@link Consumer} to populate the attributes
|
||||
*/
|
||||
public static Consumer<Map<String, Object>> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) {
|
||||
return (attributes) -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient);
|
||||
|
||||
+72
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 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.
|
||||
@@ -18,22 +18,25 @@ package org.springframework.security.oauth2.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatObject;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link InMemoryOAuth2AuthorizedClientService}.
|
||||
@@ -79,9 +82,11 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
|
||||
@Test
|
||||
public void constructorWhenAuthorizedClientsProvidedThenUseProvidedAuthorizedClients() {
|
||||
String registrationId = this.registration3.getRegistrationId();
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration3, this.principalName1,
|
||||
mock(OAuth2AccessToken.class));
|
||||
Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients = Collections.singletonMap(
|
||||
new OAuth2AuthorizedClientId(this.registration3.getRegistrationId(), this.principalName1),
|
||||
mock(OAuth2AuthorizedClient.class));
|
||||
authorizedClient);
|
||||
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
|
||||
given(clientRegistrationRepository.findByRegistrationId(eq(registrationId))).willReturn(this.registration3);
|
||||
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
|
||||
@@ -124,7 +129,35 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
|
||||
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
|
||||
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
|
||||
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
|
||||
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
|
||||
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnAuthorizedClientWithUpdatedClientRegistration() {
|
||||
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.registration1)
|
||||
.clientSecret("updated secret")
|
||||
.build();
|
||||
|
||||
ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
|
||||
given(clientRegistrationRepository.findByRegistrationId(this.registration1.getRegistrationId()))
|
||||
.willReturn(this.registration1, updatedRegistration);
|
||||
|
||||
InMemoryOAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService(
|
||||
clientRegistrationRepository);
|
||||
|
||||
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.registration1,
|
||||
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
|
||||
authorizedClientService.saveAuthorizedClient(cachedAuthorizedClient,
|
||||
new TestingAuthenticationToken(this.principalName1, null));
|
||||
|
||||
OAuth2AuthorizedClient authorizedClientWithUpdatedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
|
||||
this.principalName1, mock(OAuth2AccessToken.class), mock(OAuth2RefreshToken.class));
|
||||
OAuth2AuthorizedClient firstLoadedClient = authorizedClientService
|
||||
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
|
||||
OAuth2AuthorizedClient secondLoadedClient = authorizedClientService
|
||||
.loadAuthorizedClient(this.registration1.getRegistrationId(), this.principalName1);
|
||||
assertThat(firstLoadedClient).satisfies(isEqualTo(cachedAuthorizedClient));
|
||||
assertThat(secondLoadedClient).satisfies(isEqualTo(authorizedClientWithUpdatedRegistration));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -148,7 +181,7 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
|
||||
this.authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);
|
||||
OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientService
|
||||
.loadAuthorizedClient(this.registration3.getRegistrationId(), this.principalName2);
|
||||
assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient);
|
||||
assertThat(loadedAuthorizedClient).satisfies(isEqualTo(authorizedClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -180,4 +213,38 @@ public class InMemoryOAuth2AuthorizedClientServiceTests {
|
||||
assertThat(loadedAuthorizedClient).isNull();
|
||||
}
|
||||
|
||||
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
|
||||
return (actual) -> {
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(expected.getClientRegistration().getRegistrationId());
|
||||
assertThat(actual.getClientRegistration().getClientName())
|
||||
.isEqualTo(expected.getClientRegistration().getClientName());
|
||||
assertThat(actual.getClientRegistration().getRedirectUri())
|
||||
.isEqualTo(expected.getClientRegistration().getRedirectUri());
|
||||
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
|
||||
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
|
||||
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
|
||||
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
|
||||
assertThat(actual.getClientRegistration().getClientId())
|
||||
.isEqualTo(expected.getClientRegistration().getClientId());
|
||||
assertThat(actual.getClientRegistration().getClientSecret())
|
||||
.isEqualTo(expected.getClientRegistration().getClientSecret());
|
||||
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
|
||||
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
|
||||
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
|
||||
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
|
||||
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
|
||||
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
|
||||
if (expected.getRefreshToken() != null) {
|
||||
assertThat(actual.getRefreshToken()).isNotNull();
|
||||
assertThat(actual.getRefreshToken().getTokenValue())
|
||||
.isEqualTo(expected.getRefreshToken().getTokenValue());
|
||||
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
|
||||
assertThat(actual.getRefreshToken().getExpiresAt())
|
||||
.isEqualTo(expected.getRefreshToken().getExpiresAt());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+74
-4
@@ -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.
|
||||
@@ -18,12 +18,14 @@ package org.springframework.security.oauth2.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
@@ -34,7 +36,9 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
@@ -56,8 +60,9 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
|
||||
|
||||
private Authentication principal = new TestingAuthenticationToken(this.principalName, "notused");
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", Instant.now(),
|
||||
Instant.now().plus(Duration.ofDays(1)));
|
||||
private OAuth2AccessToken accessToken;
|
||||
|
||||
private OAuth2RefreshToken refreshToken;
|
||||
|
||||
// @formatter:off
|
||||
private ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(this.clientRegistrationId)
|
||||
@@ -79,6 +84,11 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
|
||||
public void setup() {
|
||||
this.authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(
|
||||
this.clientRegistrationRepository);
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(Duration.ofDays(1));
|
||||
this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt);
|
||||
this.refreshToken = new OAuth2RefreshToken("refresh", issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -153,11 +163,37 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
|
||||
.saveAuthorizedClient(authorizedClient, this.principal)
|
||||
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
|
||||
StepVerifier.create(saveAndLoad)
|
||||
.expectNext(authorizedClient)
|
||||
.assertNext(isEqualTo(authorizedClient))
|
||||
.verifyComplete();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void loadAuthorizedClientWhenClientRegistrationIsUpdatedThenReturnsAuthorizedClientWithUpdatedClientRegistration() {
|
||||
ClientRegistration updatedRegistration = ClientRegistration.withClientRegistration(this.clientRegistration)
|
||||
.clientSecret("updated secret")
|
||||
.build();
|
||||
|
||||
given(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId))
|
||||
.willReturn(Mono.just(this.clientRegistration), Mono.just(updatedRegistration));
|
||||
|
||||
OAuth2AuthorizedClient cachedAuthorizedClient = new OAuth2AuthorizedClient(this.clientRegistration,
|
||||
this.principalName, this.accessToken, this.refreshToken);
|
||||
OAuth2AuthorizedClient authorizedClientWithChangedRegistration = new OAuth2AuthorizedClient(updatedRegistration,
|
||||
this.principalName, this.accessToken, this.refreshToken);
|
||||
|
||||
Flux<OAuth2AuthorizedClient> saveAndLoadTwice = this.authorizedClientService
|
||||
.saveAuthorizedClient(cachedAuthorizedClient, this.principal)
|
||||
.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
|
||||
.concatWith(
|
||||
this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
|
||||
StepVerifier.create(saveAndLoadTwice)
|
||||
.assertNext(isEqualTo(cachedAuthorizedClient))
|
||||
.assertNext(isEqualTo(authorizedClientWithChangedRegistration))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveAuthorizedClientWhenAuthorizedClientNullThenIllegalArgumentException() {
|
||||
OAuth2AuthorizedClient authorizedClient = null;
|
||||
@@ -246,4 +282,38 @@ public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static Consumer<OAuth2AuthorizedClient> isEqualTo(OAuth2AuthorizedClient expected) {
|
||||
return (actual) -> {
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.getClientRegistration().getRegistrationId())
|
||||
.isEqualTo(expected.getClientRegistration().getRegistrationId());
|
||||
assertThat(actual.getClientRegistration().getClientName())
|
||||
.isEqualTo(expected.getClientRegistration().getClientName());
|
||||
assertThat(actual.getClientRegistration().getRedirectUri())
|
||||
.isEqualTo(expected.getClientRegistration().getRedirectUri());
|
||||
assertThat(actual.getClientRegistration().getAuthorizationGrantType())
|
||||
.isEqualTo(expected.getClientRegistration().getAuthorizationGrantType());
|
||||
assertThat(actual.getClientRegistration().getClientAuthenticationMethod())
|
||||
.isEqualTo(expected.getClientRegistration().getClientAuthenticationMethod());
|
||||
assertThat(actual.getClientRegistration().getClientId())
|
||||
.isEqualTo(expected.getClientRegistration().getClientId());
|
||||
assertThat(actual.getClientRegistration().getClientSecret())
|
||||
.isEqualTo(expected.getClientRegistration().getClientSecret());
|
||||
assertThat(actual.getPrincipalName()).isEqualTo(expected.getPrincipalName());
|
||||
assertThat(actual.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType());
|
||||
assertThat(actual.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue());
|
||||
assertThat(actual.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt());
|
||||
assertThat(actual.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt());
|
||||
assertThat(actual.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes());
|
||||
if (expected.getRefreshToken() != null) {
|
||||
assertThat(actual.getRefreshToken()).isNotNull();
|
||||
assertThat(actual.getRefreshToken().getTokenValue())
|
||||
.isEqualTo(expected.getRefreshToken().getTokenValue());
|
||||
assertThat(actual.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt());
|
||||
assertThat(actual.getRefreshToken().getExpiresAt())
|
||||
.isEqualTo(expected.getRefreshToken().getExpiresAt());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,4 +17,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -23,4 +23,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -26,4 +26,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+65
-60
@@ -52,26 +52,77 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
|
||||
|
||||
@Override
|
||||
public String resolve(final HttpServletRequest request) {
|
||||
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
|
||||
final String parameterToken = isParameterTokenSupportedForRequest(request)
|
||||
? resolveFromRequestParameters(request) : null;
|
||||
if (authorizationHeaderToken != null) {
|
||||
if (parameterToken != null) {
|
||||
// @formatter:off
|
||||
return resolveToken(
|
||||
resolveFromAuthorizationHeader(request),
|
||||
resolveAccessTokenFromQueryString(request),
|
||||
resolveAccessTokenFromBody(request)
|
||||
);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static String resolveToken(String... accessTokens) {
|
||||
if (accessTokens == null || accessTokens.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String accessToken = null;
|
||||
for (String token : accessTokens) {
|
||||
if (accessToken == null) {
|
||||
accessToken = token;
|
||||
}
|
||||
else if (token != null) {
|
||||
BearerTokenError error = BearerTokenErrors
|
||||
.invalidRequest("Found multiple bearer tokens in the request");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return authorizationHeaderToken;
|
||||
}
|
||||
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
|
||||
if (!StringUtils.hasText(parameterToken)) {
|
||||
BearerTokenError error = BearerTokenErrors
|
||||
.invalidRequest("The requested token parameter is an empty string");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return parameterToken;
|
||||
|
||||
if (accessToken != null && accessToken.isBlank()) {
|
||||
BearerTokenError error = BearerTokenErrors
|
||||
.invalidRequest("The requested token parameter is an empty string");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return null;
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
|
||||
String authorization = request.getHeader(this.bearerTokenHeaderName);
|
||||
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher matcher = authorizationPattern.matcher(authorization);
|
||||
if (!matcher.matches()) {
|
||||
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
return matcher.group("token");
|
||||
}
|
||||
|
||||
private String resolveAccessTokenFromQueryString(HttpServletRequest request) {
|
||||
if (!this.allowUriQueryParameter || !HttpMethod.GET.name().equals(request.getMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
private String resolveAccessTokenFromBody(HttpServletRequest request) {
|
||||
if (!this.allowFormEncodedBodyParameter
|
||||
|| !MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())
|
||||
|| HttpMethod.GET.name().equals(request.getMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String queryString = request.getQueryString();
|
||||
if (queryString != null && queryString.contains(ACCESS_TOKEN_PARAMETER_NAME)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,50 +160,4 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
|
||||
this.bearerTokenHeaderName = bearerTokenHeaderName;
|
||||
}
|
||||
|
||||
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
|
||||
String authorization = request.getHeader(this.bearerTokenHeaderName);
|
||||
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
|
||||
return null;
|
||||
}
|
||||
Matcher matcher = authorizationPattern.matcher(authorization);
|
||||
if (!matcher.matches()) {
|
||||
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return matcher.group("token");
|
||||
}
|
||||
|
||||
private static String resolveFromRequestParameters(HttpServletRequest request) {
|
||||
String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME);
|
||||
if (values == null || values.length == 0) {
|
||||
return null;
|
||||
}
|
||||
if (values.length == 1) {
|
||||
return values[0];
|
||||
}
|
||||
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
|
||||
return isFormEncodedRequest(request) || isGetRequest(request);
|
||||
}
|
||||
|
||||
private static boolean isGetRequest(HttpServletRequest request) {
|
||||
return HttpMethod.GET.name().equals(request.getMethod());
|
||||
}
|
||||
|
||||
private static boolean isFormEncodedRequest(HttpServletRequest request) {
|
||||
return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType());
|
||||
}
|
||||
|
||||
private static boolean hasAccessTokenInQueryString(HttpServletRequest request) {
|
||||
return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME);
|
||||
}
|
||||
|
||||
private boolean isParameterTokenEnabledForRequest(HttpServletRequest request) {
|
||||
return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request)
|
||||
&& !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -77,18 +77,18 @@ public class ServerBearerTokenAuthenticationConverter implements ServerAuthentic
|
||||
}
|
||||
return authorizationHeaderToken;
|
||||
}
|
||||
if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
|
||||
if (!StringUtils.hasText(parameterToken)) {
|
||||
BearerTokenError error = BearerTokenErrors
|
||||
.invalidRequest("The requested token parameter is an empty string");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return parameterToken;
|
||||
if (parameterToken != null && !StringUtils.hasText(parameterToken)) {
|
||||
BearerTokenError error = BearerTokenErrors
|
||||
.invalidRequest("The requested token parameter is an empty string");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return null;
|
||||
return parameterToken;
|
||||
}
|
||||
|
||||
private static String resolveAccessTokenFromRequest(ServerHttpRequest request) {
|
||||
private String resolveAccessTokenFromRequest(ServerHttpRequest request) {
|
||||
if (!isParameterTokenSupportedForRequest(request)) {
|
||||
return null;
|
||||
}
|
||||
List<String> parameterTokens = request.getQueryParams().get("access_token");
|
||||
if (CollectionUtils.isEmpty(parameterTokens)) {
|
||||
return null;
|
||||
|
||||
+37
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -110,6 +110,7 @@ public class DefaultBearerTokenResolverTests {
|
||||
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() {
|
||||
this.resolver.setAllowFormEncodedBodyParameter(true);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
|
||||
request.setMethod("POST");
|
||||
@@ -121,6 +122,7 @@ public class DefaultBearerTokenResolverTests {
|
||||
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
|
||||
this.resolver.setAllowUriQueryParameter(true);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
|
||||
request.setMethod("GET");
|
||||
@@ -133,6 +135,7 @@ public class DefaultBearerTokenResolverTests {
|
||||
// gh-10326
|
||||
@Test
|
||||
public void resolveWhenRequestContainsTwoAccessTokenQueryParametersThenAuthenticationExceptionIsThrown() {
|
||||
this.resolver.setAllowUriQueryParameter(true);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("GET");
|
||||
request.addParameter("access_token", "token1", "token2");
|
||||
@@ -143,6 +146,7 @@ public class DefaultBearerTokenResolverTests {
|
||||
// gh-10326
|
||||
@Test
|
||||
public void resolveWhenRequestContainsTwoAccessTokenFormParametersThenAuthenticationExceptionIsThrown() {
|
||||
this.resolver.setAllowFormEncodedBodyParameter(true);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("POST");
|
||||
request.setContentType("application/x-www-form-urlencoded");
|
||||
@@ -233,6 +237,19 @@ public class DefaultBearerTokenResolverTests {
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenPostAndQueryParameterIsSupportedAndFormParameterIsPresentThenTokenIsNotResolved() {
|
||||
this.resolver.setAllowUriQueryParameter(true);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("POST");
|
||||
request.setContentType("application/x-www-form-urlencoded");
|
||||
request.setQueryString("access_token=" + TEST_TOKEN);
|
||||
request.addParameter("access_token", TEST_TOKEN);
|
||||
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
@@ -261,6 +278,25 @@ public class DefaultBearerTokenResolverTests {
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
// gh-16038
|
||||
@Test
|
||||
public void resolveWhenRequestContainsTwoAccessTokenFormParametersAndSupportIsDisabledThenTokenIsNotResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("POST");
|
||||
request.setContentType("application/x-www-form-urlencoded");
|
||||
request.addParameter("access_token", "token1", "token2");
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
// gh-16038
|
||||
@Test
|
||||
public void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setMethod("GET");
|
||||
request.addParameter("access_token", "token1", "token2");
|
||||
assertThat(this.resolver.resolve(request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenQueryParameterIsPresentAndEmptyStringThenTokenIsNotResolved() {
|
||||
this.resolver.setAllowUriQueryParameter(true);
|
||||
|
||||
+11
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -157,6 +157,7 @@ public class ServerBearerTokenAuthenticationConverterTests {
|
||||
@Test
|
||||
public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() {
|
||||
// @formatter:off
|
||||
this.converter.setAllowUriQueryParameter(true);
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
|
||||
.queryParam("access_token", TEST_TOKEN)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN);
|
||||
@@ -205,6 +206,7 @@ public class ServerBearerTokenAuthenticationConverterTests {
|
||||
|
||||
@Test
|
||||
void resolveWhenQueryParameterHasMultipleAccessTokensThenOAuth2AuthenticationException() {
|
||||
this.converter.setAllowUriQueryParameter(true);
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
|
||||
.queryParam("access_token", TEST_TOKEN, TEST_TOKEN);
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> convertToToken(request))
|
||||
@@ -217,6 +219,14 @@ public class ServerBearerTokenAuthenticationConverterTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-16038
|
||||
@Test
|
||||
void resolveWhenRequestContainsTwoAccessTokenQueryParametersAndSupportIsDisabledThenTokenIsNotResolved() {
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/")
|
||||
.queryParam("access_token", TEST_TOKEN, TEST_TOKEN);
|
||||
assertThat(convertToToken(request)).isNull();
|
||||
}
|
||||
|
||||
private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder<?> request) {
|
||||
return convertToToken(request.build());
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -24,4 +24,6 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "org.mockito:mockito-junit-jupiter"
|
||||
testImplementation "org.springframework:spring-test"
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
configure(project.tasks.withType(Test)) {
|
||||
|
||||
@@ -32,4 +32,6 @@ dependencies {
|
||||
testImplementation 'org.skyscreamer:jsonassert'
|
||||
testImplementation 'org.springframework:spring-webmvc'
|
||||
testImplementation 'org.springframework:spring-tx'
|
||||
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
@@ -39,4 +39,5 @@ dependencies {
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver'
|
||||
|
||||
testRuntimeOnly 'org.hsqldb:hsqldb'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
+11
-3
@@ -29,6 +29,9 @@ public interface WebInvocationPrivilegeEvaluator {
|
||||
/**
|
||||
* Determines whether the user represented by the supplied <tt>Authentication</tt>
|
||||
* object is allowed to invoke the supplied URI.
|
||||
* <p>
|
||||
* Note this will only match authorization rules that don't require a certain
|
||||
* {@code HttpMethod}.
|
||||
* @param uri the URI excluding the context path (a default context path setting will
|
||||
* be used)
|
||||
*/
|
||||
@@ -36,13 +39,18 @@ public interface WebInvocationPrivilegeEvaluator {
|
||||
|
||||
/**
|
||||
* Determines whether the user represented by the supplied <tt>Authentication</tt>
|
||||
* object is allowed to invoke the supplied URI, with the given .
|
||||
* object is allowed to invoke the supplied URI, with the given parameters.
|
||||
* <p>
|
||||
* Note the default implementation of <tt>FilterInvocationSecurityMetadataSource</tt>
|
||||
* Note:
|
||||
* <ul>
|
||||
* <li>The default implementation of <tt>FilterInvocationSecurityMetadataSource</tt>
|
||||
* disregards the <code>contextPath</code> when evaluating which secure object
|
||||
* metadata applies to a given request URI, so generally the <code>contextPath</code>
|
||||
* is unimportant unless you are using a custom
|
||||
* <code>FilterInvocationSecurityMetadataSource</code>.
|
||||
* <code>FilterInvocationSecurityMetadataSource</code>.</li>
|
||||
* <li>this will only match authorization rules that don't require a certain
|
||||
* {@code HttpMethod}.</li>
|
||||
* </ul>
|
||||
* @param uri the URI excluding the context path
|
||||
* @param contextPath the context path (may be null).
|
||||
* @param method the HTTP method (or null, for any method)
|
||||
|
||||
+14
-3
@@ -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.
|
||||
@@ -23,6 +23,7 @@ import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -97,8 +98,18 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
|
||||
|
||||
this.cookieCustomizer.accept(cookieBuilder);
|
||||
|
||||
Cookie cookie = mapToCookie(cookieBuilder.build());
|
||||
response.addCookie(cookie);
|
||||
ResponseCookie responseCookie = cookieBuilder.build();
|
||||
if (!StringUtils.hasLength(responseCookie.getSameSite())) {
|
||||
Cookie cookie = mapToCookie(responseCookie);
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
else if (request.getServletContext().getMajorVersion() > 5) {
|
||||
Cookie cookie = mapToCookie(responseCookie);
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
else {
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
|
||||
}
|
||||
|
||||
// Set request attribute to signal that response has blank cookie value,
|
||||
// which allows loadToken to return null when token has been removed
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -84,7 +84,7 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
|
||||
*/
|
||||
public static CookieServerCsrfTokenRepository withHttpOnlyFalse() {
|
||||
CookieServerCsrfTokenRepository result = new CookieServerCsrfTokenRepository();
|
||||
result.setCookieCustomizer((cookie) -> cookie.httpOnly(false));
|
||||
result.cookieHttpOnly = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+64
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.web.server.firewall;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -23,6 +25,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -33,6 +36,7 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.http.server.reactive.SslInfo;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -743,6 +747,11 @@ public class StrictServerWebExchangeFirewall implements ServerWebExchangeFirewal
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder mutate() {
|
||||
return new StrictFirewallBuilder(super.mutate());
|
||||
}
|
||||
|
||||
private final class StrictFirewallHttpHeaders extends HttpHeaders {
|
||||
|
||||
private StrictFirewallHttpHeaders(HttpHeaders delegate) {
|
||||
@@ -783,6 +792,61 @@ public class StrictServerWebExchangeFirewall implements ServerWebExchangeFirewal
|
||||
|
||||
}
|
||||
|
||||
private final class StrictFirewallBuilder implements Builder {
|
||||
|
||||
private final Builder delegate;
|
||||
|
||||
private StrictFirewallBuilder(Builder delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder method(HttpMethod httpMethod) {
|
||||
return this.delegate.method(httpMethod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder uri(URI uri) {
|
||||
return this.delegate.uri(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder path(String path) {
|
||||
return this.delegate.path(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder contextPath(String contextPath) {
|
||||
return this.delegate.contextPath(contextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder header(String headerName, String... headerValues) {
|
||||
return this.delegate.header(headerName, headerValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
return this.delegate.headers(headersConsumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder sslInfo(SslInfo sslInfo) {
|
||||
return this.delegate.sslInfo(sslInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder remoteAddress(InetSocketAddress remoteAddress) {
|
||||
return this.delegate.remoteAddress(remoteAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerHttpRequest build() {
|
||||
return new StrictFirewallHttpRequest(this.delegate.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user