Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdc3feaa6d | |||
| 134f8b0c5d | |||
| 835ee559d7 | |||
| cfe48556d4 | |||
| c7b73d7b3b | |||
| 618edc2e12 | |||
| f7fef100c0 | |||
| 9be97e76dc | |||
| 603e0a4140 | |||
| b081627f52 | |||
| 10a0bda63f | |||
| 7db5149163 | |||
| b206670245 | |||
| b067aa4653 | |||
| ba16d91971 | |||
| dfebd6d9d4 | |||
| bd703ff4ac | |||
| 7abaefda10 | |||
| 4440020217 | |||
| c2b9c0856d | |||
| a06f47fac3 | |||
| 8c0103498a | |||
| e269e5cb91 | |||
| ed8b08fa24 | |||
| 208e327629 | |||
| 51dfae1b91 | |||
| 2fd94752c9 | |||
| 61284ce22d | |||
| fdd017d935 | |||
| bfeb6bd756 |
@@ -1,6 +1,9 @@
|
||||
# Normalize line endings to auto.
|
||||
* text auto
|
||||
|
||||
# Ensure that line endings for DOS batch files are not modified.
|
||||
*.bat -text
|
||||
|
||||
# Ensure the following are treated as binary.
|
||||
*.cer binary
|
||||
*.graffle binary
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ git clone git@github.com:spring-projects/spring-security.git
|
||||
=== Install all spring-\* jars into your local Maven cache
|
||||
[indent=0]
|
||||
----
|
||||
./gradlew install
|
||||
./gradlew publishToMavenLocal
|
||||
----
|
||||
|
||||
=== Compile and test; build all jars, distribution zips, and docs
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ Generate the Release Notes replacing:
|
||||
* <next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
|
||||
----
|
||||
$ ./gradlew generateChangelog -P<next-version>
|
||||
$ ./gradlew generateChangelog -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
* Copy the release notes to your clipboard (your mileage may vary with
|
||||
|
||||
@@ -87,6 +87,14 @@ updateDependenciesSettings {
|
||||
if (candidate.getGroup().equals("org.opensaml")) {
|
||||
selection.reject("org.opensaml maintains two different versions, so it must be updated manually");
|
||||
}
|
||||
if (candidate.getGroup().equals("org.jetbrains.kotlin")) {
|
||||
if (candidate.getVersion().endsWith("-RC")) {
|
||||
selection.reject("On a maintenance branch, we should not take RC versions")
|
||||
}
|
||||
if (candidate.getVersion().contains("-M")) {
|
||||
selection.reject("On a maintenance branch, we should not take milestone versions")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ dependencies {
|
||||
implementation localGroovy()
|
||||
|
||||
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
||||
implementation 'io.projectreactor:reactor-core:3.4.6'
|
||||
implementation 'io.projectreactor:reactor-core:3.4.9'
|
||||
implementation 'gradle.plugin.org.gretty:gretty:3.0.1'
|
||||
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
|
||||
+1
-1
@@ -94,8 +94,8 @@ class HttpSecurityConfiguration {
|
||||
.requestCache(withDefaults())
|
||||
.anonymous(withDefaults())
|
||||
.servletApi(withDefaults())
|
||||
.logout(withDefaults())
|
||||
.apply(new DefaultLoginPageConfigurer<>());
|
||||
http.logout(withDefaults());
|
||||
// @formatter:on
|
||||
return http;
|
||||
}
|
||||
|
||||
+11
-2
@@ -260,8 +260,17 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return this.authenticationConverter;
|
||||
}
|
||||
|
||||
private String version() {
|
||||
String version = Version.getVersion();
|
||||
if (version != null) {
|
||||
return version;
|
||||
}
|
||||
return Version.class.getModule().getDescriptor().version().map(Object::toString)
|
||||
.orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
|
||||
}
|
||||
|
||||
private void registerDefaultAuthenticationProvider(B http) {
|
||||
if (Version.getVersion().startsWith("4")) {
|
||||
if (version().startsWith("4")) {
|
||||
http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider()));
|
||||
}
|
||||
else {
|
||||
@@ -346,7 +355,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
private Saml2AuthenticationRequestFactory getResolver(B http) {
|
||||
Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
|
||||
if (resolver == null) {
|
||||
if (Version.getVersion().startsWith("4")) {
|
||||
if (version().startsWith("4")) {
|
||||
return new OpenSaml4AuthenticationRequestFactory();
|
||||
}
|
||||
return new OpenSamlAuthenticationRequestFactory();
|
||||
|
||||
+12
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -57,6 +57,8 @@ public class AuthenticationManagerBeanDefinitionParser implements BeanDefinition
|
||||
|
||||
private static final String ATT_ERASE_CREDENTIALS = "erase-credentials";
|
||||
|
||||
private static final String AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME = "defaultAuthenticationEventPublisher";
|
||||
|
||||
@Override
|
||||
public BeanDefinition parse(Element element, ParserContext pc) {
|
||||
String id = element.getAttribute("id");
|
||||
@@ -86,11 +88,15 @@ public class AuthenticationManagerBeanDefinitionParser implements BeanDefinition
|
||||
if ("false".equals(element.getAttribute(ATT_ERASE_CREDENTIALS))) {
|
||||
providerManagerBldr.addPropertyValue("eraseCredentialsAfterAuthentication", false);
|
||||
}
|
||||
// Add the default event publisher
|
||||
BeanDefinition publisher = new RootBeanDefinition(DefaultAuthenticationEventPublisher.class);
|
||||
String pubId = pc.getReaderContext().generateBeanName(publisher);
|
||||
pc.registerBeanComponent(new BeanComponentDefinition(publisher, pubId));
|
||||
providerManagerBldr.addPropertyReference("authenticationEventPublisher", pubId);
|
||||
|
||||
if (!pc.getRegistry().containsBeanDefinition(AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME)) {
|
||||
// Add the default event publisher to the context
|
||||
BeanDefinition publisher = new RootBeanDefinition(DefaultAuthenticationEventPublisher.class);
|
||||
pc.registerBeanComponent(new BeanComponentDefinition(publisher, AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME));
|
||||
}
|
||||
|
||||
providerManagerBldr.addPropertyReference("authenticationEventPublisher",
|
||||
AUTHENTICATION_EVENT_PUBLISHER_BEAN_NAME);
|
||||
pc.registerBeanComponent(new BeanComponentDefinition(providerManagerBldr.getBeanDefinition(), id));
|
||||
if (StringUtils.hasText(alias)) {
|
||||
pc.getRegistry().registerAlias(id, alias);
|
||||
|
||||
+12
@@ -187,6 +187,18 @@ public class HttpSecurityConfigurationTests {
|
||||
this.mockMvc.perform(get("/login")).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenUsingDefaultsThenDefaultLoginFailurePageGenerated() throws Exception {
|
||||
this.spring.register(SecurityEnabledConfig.class).autowire();
|
||||
this.mockMvc.perform(get("/login?error")).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWhenUsingDefaultsThenDefaultLogoutSuccessPageGenerated() throws Exception {
|
||||
this.spring.register(SecurityEnabledConfig.class).autowire();
|
||||
this.mockMvc.perform(get("/login?logout")).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class NameController {
|
||||
|
||||
|
||||
+25
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -25,6 +25,7 @@ import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.security.authentication.AuthenticationEventPublisher;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
@@ -54,6 +55,17 @@ public class AuthenticationManagerBeanDefinitionParserTests {
|
||||
+ "</authentication-manager>";
|
||||
// @formatter:on
|
||||
|
||||
// Issue #7282
|
||||
// @formatter:off
|
||||
private static final String CONTEXT_MULTI = "<authentication-manager id='amSecondary'>"
|
||||
+ " <authentication-provider>"
|
||||
+ " <user-service>"
|
||||
+ " <user name='john' password='{noop}doe' authorities='ROLE_C,ROLE_D' />"
|
||||
+ " </user-service>"
|
||||
+ " </authentication-provider>"
|
||||
+ "</authentication-manager>";
|
||||
// @formatter:on
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@@ -64,6 +76,18 @@ public class AuthenticationManagerBeanDefinitionParserTests {
|
||||
assertThat(context.getBeansOfType(AuthenticationProvider.class)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventPublishersAreRegisteredAsTopLevelBeans() {
|
||||
ConfigurableApplicationContext context = this.spring.context(CONTEXT).getContext();
|
||||
assertThat(context.getBeansOfType(AuthenticationEventPublisher.class)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyOneEventPublisherIsRegisteredForMultipleAuthenticationManagers() {
|
||||
ConfigurableApplicationContext context = this.spring.context(CONTEXT + '\n' + CONTEXT_MULTI).getContext();
|
||||
assertThat(context.getBeansOfType(AuthenticationEventPublisher.class)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventsArePublishedByDefault() throws Exception {
|
||||
ConfigurableApplicationContext appContext = this.spring.context(CONTEXT).getContext();
|
||||
|
||||
+13
-13
@@ -7,18 +7,18 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:5.3.8")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.7")
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.10")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.1")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.0.1")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.0.4")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.0")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.12.3")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.1")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.12.4")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.2.3"
|
||||
api "ch.qos.logback:logback-classic:1.2.5"
|
||||
api "com.google.inject:guice:3.0"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.10"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.9"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.10.1"
|
||||
api "com.nimbusds:oauth2-oidc-sdk:9.9.1"
|
||||
api "com.squareup.okhttp3:mockwebserver:3.14.9"
|
||||
api "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
api "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
@@ -49,8 +49,8 @@ dependencies {
|
||||
api "org.assertj:assertj-core:3.19.0"
|
||||
api "org.bouncycastle:bcpkix-jdk15on:1.68"
|
||||
api "org.bouncycastle:bcprov-jdk15on:1.68"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.42.v20210604"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.42.v20210604"
|
||||
api "org.eclipse.jetty:jetty-server:9.4.43.v20210629"
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.43.v20210629"
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hibernate:hibernate-entitymanager:5.4.32.Final"
|
||||
api "org.hsqldb:hsqldb:2.6.0"
|
||||
@@ -71,9 +71,9 @@ dependencies {
|
||||
api "org.seleniumhq.selenium:selenium-java:3.141.59"
|
||||
api "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
api "org.skyscreamer:jsonassert:1.5.0"
|
||||
api "org.slf4j:jcl-over-slf4j:1.7.31"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.31"
|
||||
api "org.slf4j:slf4j-api:1.7.31"
|
||||
api "org.slf4j:jcl-over-slf4j:1.7.32"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.32"
|
||||
api "org.slf4j:slf4j-api:1.7.32"
|
||||
api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE"
|
||||
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ asciidoctor {
|
||||
baseDir = file('src/docs/asciidoc')
|
||||
options eruby: 'erubis'
|
||||
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$ghTag"
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
|
||||
attributes copycss : '',
|
||||
icons : 'font',
|
||||
'source-highlighter': 'prettify',
|
||||
|
||||
@@ -18,23 +18,22 @@ asciidoctorj {
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
|
||||
def ghOldSamplesUrl = "https://github.com/spring-projects/spring-security/tree/5.4.x/samples"
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$ghTag"
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def oldSecurityApiUrl = "https://docs.spring.io/spring-security/site/docs/5.4.x/api/"
|
||||
def springVersion = "5.3.x"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
|
||||
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
|
||||
attributes 'spring-security-version' : project.version,
|
||||
'spring-boot-version' : springBootVersion,
|
||||
revnumber : project.version,
|
||||
'gh-url': ghUrl,
|
||||
'gh-samples-url': ghSamplesUrl,
|
||||
'gh-old-samples-url': ghOldSamplesUrl,
|
||||
'old-security-api-url': oldSecurityApiUrl,
|
||||
'security-api-url': securityApiUrl,
|
||||
'security-reference-url': securityReferenceUrl,
|
||||
'spring-framework-api-url': springFrameworkApiUrl
|
||||
'spring-framework-api-url': springFrameworkApiUrl,
|
||||
'spring-framework-reference-url': springFrameworkReferenceUrl
|
||||
attributeProvider resolvedVersions(project.configurations.testRuntimeClasspath)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ The picture below shows the typical layering of the handlers for a single HTTP r
|
||||
image::{figures}/filterchain.png[]
|
||||
|
||||
The client sends a request to the application, and the container creates a `FilterChain` which contains the ``Filter``s and `Servlet` that should process the `HttpServletRequest` based on the path of the request URI.
|
||||
In a Spring MVC application the `Servlet` is an instance of https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-servlet[`DispatcherServlet`].
|
||||
In a Spring MVC application the `Servlet` is an instance of {spring-framework-reference-url}web.html#mvc-servlet[`DispatcherServlet`].
|
||||
At most one `Servlet` can handle a single `HttpServletRequest` and `HttpServletResponse`.
|
||||
However, more than one `Filter` can be used to:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[[servlet-events]]
|
||||
== Authentication Events
|
||||
|
||||
For each authentication that succeeds or fails, a `AuthenticationSuccessEvent` or `AuthenticationFailureEvent` is fired, respectively.
|
||||
For each authentication that succeeds or fails, a `AuthenticationSuccessEvent` or `AbstractAuthenticationFailureEvent` is fired, respectively.
|
||||
|
||||
To listen for these events, you must first publish an `AuthenticationEventPublisher`.
|
||||
Spring Security's `DefaultAuthenticationEventPublisher` will probably do fine:
|
||||
@@ -42,7 +42,7 @@ public class AuthenticationEvents {
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onFailure(AuthenticationFailureEvent failures) {
|
||||
public void onFailure(AbstractAuthenticationFailureEvent failures) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ While similar to `AuthenticationSuccessHandler` and `AuthenticationFailureHandle
|
||||
|
||||
=== Adding Exception Mappings
|
||||
|
||||
`DefaultAuthenticationEventPublisher` by default will publish an `AuthenticationFailureEvent` for the following events:
|
||||
`DefaultAuthenticationEventPublisher` by default will publish an `AbstractAuthenticationFailureEvent` for the following events:
|
||||
|
||||
|============
|
||||
| Exception | Event
|
||||
@@ -97,7 +97,7 @@ To that end, you may want to supply additional mappings to the publisher via the
|
||||
public AuthenticationEventPublisher authenticationEventPublisher
|
||||
(ApplicationEventPublisher applicationEventPublisher) {
|
||||
Map<Class<? extends AuthenticationException>,
|
||||
Class<? extends AuthenticationFailureEvent>> mapping =
|
||||
Class<? extends AbstractAuthenticationFailureEvent>> mapping =
|
||||
Collections.singletonMap(FooException.class, FooEvent.class);
|
||||
AuthenticationEventPublisher authenticationEventPublisher =
|
||||
new DefaultAuthenticationEventPublisher(applicationEventPublisher);
|
||||
|
||||
@@ -2537,7 +2537,7 @@ The issuer should be one that the code can verify from a trusted source like a l
|
||||
|
||||
You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> later on in the request.
|
||||
|
||||
This extra parsing can be alleviated by configuring the <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> directly with a `JWTClaimSetAwareJWSKeySelector` from Nimbus:
|
||||
This extra parsing can be alleviated by configuring the <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> directly with a `JWTClaimsSetAwareJWSKeySelector` from Nimbus:
|
||||
|
||||
====
|
||||
.Java
|
||||
@@ -2545,7 +2545,7 @@ This extra parsing can be alleviated by configuring the <<oauth2resourceserver-j
|
||||
----
|
||||
@Component
|
||||
public class TenantJWSKeySelector
|
||||
implements JWTClaimSetAwareJWSKeySelector<SecurityContext> {
|
||||
implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
|
||||
|
||||
private final TenantRepository tenants; <1>
|
||||
private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); <2>
|
||||
@@ -2586,7 +2586,7 @@ public class TenantJWSKeySelector
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimSetAwareJWSKeySelector<SecurityContext> {
|
||||
class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
|
||||
private val tenants: TenantRepository <1>
|
||||
private val selectors: MutableMap<String, JWSKeySelector<SecurityContext>> = ConcurrentHashMap() <2>
|
||||
|
||||
@@ -3028,7 +3028,7 @@ Additionally, it is published as an `AuthenticationFailureBadCredentialsEvent`,
|
||||
@Component
|
||||
public class FailureEvents {
|
||||
@EventListener
|
||||
public void onFailure(AuthenticationFailureEvent failure) {
|
||||
public void onFailure(AuthenticationFailureBadCredentialsEvent badCredentials) {
|
||||
if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) {
|
||||
// ... handle
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
|
||||
The above requires the role of `USER` for any URL that starts with `/messages/`.
|
||||
|
||||
[[servlet-saml2login-relyingpartyregistrationrepository]]
|
||||
The second `@Bean` Spring Boot creates is a {old-security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`RelyingPartyRegistrationRepository`], which represents the asserting party and relying party metadata.
|
||||
The second `@Bean` Spring Boot creates is a {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`RelyingPartyRegistrationRepository`], which represents the asserting party and relying party metadata.
|
||||
This includes things like the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
|
||||
|
||||
You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean.
|
||||
@@ -422,7 +422,7 @@ A relying party can be multi-tenant by registering more than one relying party i
|
||||
|
||||
[[servlet-saml2login-relyingpartyregistration]]
|
||||
=== RelyingPartyRegistration
|
||||
A {old-security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
|
||||
A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
|
||||
instance represents a link between an relying party and assering party's metadata.
|
||||
|
||||
In a `RelyingPartyRegistration`, you can provide relying party metadata like its `Issuer` value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.
|
||||
|
||||
+5
-3
@@ -1,9 +1,11 @@
|
||||
aspectjVersion=1.9.6
|
||||
aspectjVersion=1.9.7
|
||||
springJavaformatVersion=0.0.28
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=5.3.9
|
||||
openSamlVersion=3.4.6
|
||||
version=5.5.1
|
||||
kotlinVersion=1.5.10
|
||||
version=5.5.2
|
||||
kotlinVersion=1.5.21
|
||||
samplesBranch=5.5.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
+2
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -100,19 +97,7 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())
|
||||
|| ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
String clientId = encodeClientCredential(clientRegistration.getClientId());
|
||||
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
|
||||
headers.setBasicAuth(clientId, clientSecret);
|
||||
}
|
||||
}
|
||||
|
||||
private static String encodeClientCredential(String clientCredential) {
|
||||
try {
|
||||
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
// Will not happen since UTF-8 is a standard charset
|
||||
throw new IllegalArgumentException(ex);
|
||||
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
@@ -51,23 +48,11 @@ final class OAuth2AuthorizationGrantRequestEntityUtils {
|
||||
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())
|
||||
|| ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
String clientId = encodeClientCredential(clientRegistration.getClientId());
|
||||
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
|
||||
headers.setBasicAuth(clientId, clientSecret);
|
||||
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static String encodeClientCredential(String clientCredential) {
|
||||
try {
|
||||
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
// Will not happen since UTF-8 is a standard charset
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpHeaders getDefaultTokenRequestHeaders() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
|
||||
|
||||
-38
@@ -16,11 +16,6 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
@@ -133,37 +128,4 @@ public class OAuth2ClientCredentialsGrantRequestEntityConverterTests {
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
|
||||
}
|
||||
|
||||
// gh-9610
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void convertWhenSpecialCharactersThenConvertsWithEncodedClientCredentials()
|
||||
throws UnsupportedEncodingException {
|
||||
String clientCredentialWithAnsiKeyboardSpecialCharacters = "~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./ ";
|
||||
// @formatter:off
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
|
||||
.clientId(clientCredentialWithAnsiKeyboardSpecialCharacters)
|
||||
.clientSecret(clientCredentialWithAnsiKeyboardSpecialCharacters)
|
||||
.build();
|
||||
// @formatter:on
|
||||
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
|
||||
clientRegistration);
|
||||
RequestEntity<?> requestEntity = this.converter.convert(clientCredentialsGrantRequest);
|
||||
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
|
||||
assertThat(requestEntity.getUrl().toASCIIString())
|
||||
.isEqualTo(clientRegistration.getProviderDetails().getTokenUri());
|
||||
HttpHeaders headers = requestEntity.getHeaders();
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||
assertThat(headers.getContentType())
|
||||
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
|
||||
String urlEncodedClientCredential = URLEncoder.encode(clientCredentialWithAnsiKeyboardSpecialCharacters,
|
||||
StandardCharsets.UTF_8.toString());
|
||||
String clientCredentials = Base64.getEncoder().encodeToString(
|
||||
(urlEncodedClientCredential + ":" + urlEncodedClientCredential).getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic " + clientCredentials);
|
||||
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
|
||||
.isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue());
|
||||
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-34
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2020 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,10 +16,6 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
@@ -93,35 +89,6 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests {
|
||||
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser");
|
||||
}
|
||||
|
||||
// gh-9610
|
||||
@Test
|
||||
public void getTokenResponseWhenSpecialCharactersThenSuccessWithEncodedClientCredentials() throws Exception {
|
||||
// @formatter:off
|
||||
enqueueJson("{\n"
|
||||
+ " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n"
|
||||
+ " \"token_type\":\"bearer\",\n"
|
||||
+ " \"expires_in\":3600,\n"
|
||||
+ " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\",\n"
|
||||
+ " \"scope\":\"create\"\n"
|
||||
+ "}");
|
||||
// @formatter:on
|
||||
String clientCredentialWithAnsiKeyboardSpecialCharacters = "~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./ ";
|
||||
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(
|
||||
this.clientRegistration.clientId(clientCredentialWithAnsiKeyboardSpecialCharacters)
|
||||
.clientSecret(clientCredentialWithAnsiKeyboardSpecialCharacters).build());
|
||||
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
|
||||
RecordedRequest actualRequest = this.server.takeRequest();
|
||||
String body = actualRequest.getBody().readUtf8();
|
||||
assertThat(response.getAccessToken()).isNotNull();
|
||||
String urlEncodedClientCredentialecret = URLEncoder.encode(clientCredentialWithAnsiKeyboardSpecialCharacters,
|
||||
StandardCharsets.UTF_8.toString());
|
||||
String clientCredentials = Base64.getEncoder()
|
||||
.encodeToString((urlEncodedClientCredentialecret + ":" + urlEncodedClientCredentialecret)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic " + clientCredentials);
|
||||
assertThat(body).isEqualTo("grant_type=client_credentials&scope=read%3Auser");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTokenResponseWhenPostThenSuccess() throws Exception {
|
||||
ClientRegistration registration = this.clientRegistration
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
+19
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -52,18 +52,19 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
|
||||
|
||||
private final Map<String, Converter<Object, ?>> claimTypeConverters;
|
||||
|
||||
private final Converter<Map<String, Object>, Map<String, Object>> delegate;
|
||||
|
||||
/**
|
||||
* Constructs a {@link MappedJwtClaimSetConverter} with the provided arguments
|
||||
*
|
||||
* This will completely replace any set of default converters.
|
||||
*
|
||||
* A converter that returns {@code null} removes the claim from the claim set. A
|
||||
* converter that returns a non-{@code null} value adds or replaces that claim in the
|
||||
* claim set.
|
||||
* @param claimTypeConverters The {@link Map} of converters to use
|
||||
*/
|
||||
public MappedJwtClaimSetConverter(Map<String, Converter<Object, ?>> claimTypeConverters) {
|
||||
Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null");
|
||||
this.claimTypeConverters = claimTypeConverters;
|
||||
this.delegate = new ClaimTypeConverter(claimTypeConverters);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +88,10 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
|
||||
*
|
||||
* To completely replace the underlying {@link Map} of converters, see
|
||||
* {@link MappedJwtClaimSetConverter#MappedJwtClaimSetConverter(Map)}.
|
||||
*
|
||||
* A converter that returns {@code null} removes the claim from the claim set. A
|
||||
* converter that returns a non-{@code null} value adds or replaces that claim in the
|
||||
* claim set.
|
||||
* @param claimTypeConverters
|
||||
* @return An instance of {@link MappedJwtClaimSetConverter} that contains the
|
||||
* converters provided, plus any defaults that were not overridden.
|
||||
@@ -143,9 +148,16 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
|
||||
@Override
|
||||
public Map<String, Object> convert(Map<String, Object> claims) {
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
Map<String, Object> mappedClaims = this.delegate.convert(claims);
|
||||
mappedClaims = removeClaims(mappedClaims);
|
||||
mappedClaims = addClaims(mappedClaims);
|
||||
Map<String, Object> mappedClaims = new HashMap<>(claims);
|
||||
for (Map.Entry<String, Converter<Object, ?>> entry : this.claimTypeConverters.entrySet()) {
|
||||
String claimName = entry.getKey();
|
||||
Converter<Object, ?> converter = entry.getValue();
|
||||
if (converter != null) {
|
||||
Object claim = claims.get(claimName);
|
||||
Object mappedClaim = converter.convert(claim);
|
||||
mappedClaims.compute(claimName, (key, value) -> mappedClaim);
|
||||
}
|
||||
}
|
||||
Instant issuedAt = (Instant) mappedClaims.get(JwtClaimNames.IAT);
|
||||
Instant expiresAt = (Instant) mappedClaims.get(JwtClaimNames.EXP);
|
||||
if (issuedAt == null && expiresAt != null) {
|
||||
@@ -154,24 +166,4 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
|
||||
return mappedClaims;
|
||||
}
|
||||
|
||||
private Map<String, Object> removeClaims(Map<String, Object> claims) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
for (Map.Entry<String, Object> entry : claims.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> addClaims(Map<String, Object> claims) {
|
||||
Map<String, Object> result = new HashMap<>(claims);
|
||||
for (Map.Entry<String, Converter<Object, ?>> entry : this.claimTypeConverters.entrySet()) {
|
||||
if (!claims.containsKey(entry.getKey()) && entry.getValue().convert(null) != null) {
|
||||
result.put(entry.getKey(), entry.getValue().convert(null));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -123,8 +123,18 @@ public class MappedJwtClaimSetConverterTests {
|
||||
assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234");
|
||||
}
|
||||
|
||||
// gh-10135
|
||||
@Test
|
||||
public void convertWhenConverterReturnsNullThenClaimIsRemoved() {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
||||
.withDefaults(Collections.singletonMap(JwtClaimNames.NBF, (nbfClaimValue) -> null));
|
||||
Map<String, Object> source = Collections.singletonMap(JwtClaimNames.NBF, Instant.now());
|
||||
Map<String, Object> target = converter.convert(source);
|
||||
assertThat(target).doesNotContainKey(JwtClaimNames.NBF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenClaimValueIsNullThenClaimIsRemoved() {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
|
||||
Map<String, Object> source = Collections.singletonMap(JwtClaimNames.ISS, null);
|
||||
Map<String, Object> target = converter.convert(source);
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ import org.springframework.web.util.UriUtils;
|
||||
*
|
||||
* <p>
|
||||
* By default, this {@code Filter} responds to authentication requests at the {@code URI}
|
||||
* {@code /oauth2/authorization/{registrationId}}. The {@code URI} template variable
|
||||
* {@code /saml2/authenticate/{registrationId}}. The {@code URI} template variable
|
||||
* {@code {registrationId}} represents the
|
||||
* {@link RelyingPartyRegistration#getRegistrationId() registration identifier} of the
|
||||
* relying party that is used for initiating the authentication request.
|
||||
|
||||
Reference in New Issue
Block a user