Compare commits
47 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 |
@@ -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 assemble && ./gradlew 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 }}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
+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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+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'
|
||||
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -1035,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());
|
||||
@@ -1058,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>
|
||||
}
|
||||
}
|
||||
@@ -1071,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.
|
||||
@@ -1098,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()
|
||||
)
|
||||
@@ -1133,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)
|
||||
}
|
||||
@@ -1148,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
|
||||
|
||||
@@ -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]
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@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.16"
|
||||
"@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.7-SNAPSHOT
|
||||
version=6.3.9
|
||||
samplesBranch=6.3.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
+11
-11
@@ -6,17 +6,17 @@ 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.17"
|
||||
org-springframework = "6.1.19"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.16"
|
||||
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"
|
||||
@@ -27,14 +27,14 @@ com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.
|
||||
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.16"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.15"
|
||||
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.9"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.2.11"
|
||||
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"
|
||||
|
||||
@@ -99,7 +99,7 @@ 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.23"
|
||||
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=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.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'
|
||||
}
|
||||
|
||||
+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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
|
||||
+16
-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.
|
||||
@@ -290,6 +290,21 @@ class CookieServerCsrfTokenRepositoryTests {
|
||||
loadAndAssertExpectedValues();
|
||||
}
|
||||
|
||||
// gh-16820
|
||||
@Test
|
||||
void withHttpOnlyFalseWhenCookieCustomizerThenStillDefaultsToFalse() {
|
||||
CookieServerCsrfTokenRepository repository = CookieServerCsrfTokenRepository.withHttpOnlyFalse();
|
||||
repository.setCookieCustomizer((customizer) -> customizer.maxAge(1000));
|
||||
MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/dummy");
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
CsrfToken csrfToken = repository.generateToken(exchange).block();
|
||||
repository.saveToken(exchange, csrfToken).block();
|
||||
ResponseCookie cookie = exchange.getResponse().getCookies().getFirst("XSRF-TOKEN");
|
||||
assertThat(cookie).isNotNull();
|
||||
assertThat(cookie.getMaxAge().getSeconds()).isEqualTo(1000);
|
||||
assertThat(cookie.isHttpOnly()).isEqualTo(Boolean.FALSE);
|
||||
}
|
||||
|
||||
private void setExpectedHeaderName(String expectedHeaderName) {
|
||||
this.csrfTokenRepository.setHeaderName(expectedHeaderName);
|
||||
this.expectedHeaderName = expectedHeaderName;
|
||||
|
||||
Reference in New Issue
Block a user