1
0
mirror of synced 2026-09-07 09:49:52 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Josh Cummings e3e7d76b70 Add Workflow to Finalize a Release 2025-11-04 09:32:41 -07:00
22 changed files with 253 additions and 799 deletions
+19 -5
View File
@@ -2,10 +2,6 @@ name: Finalize Release
on: on:
workflow_dispatch: # Manual trigger workflow_dispatch: # Manual trigger
inputs:
version:
description: The Spring Security release to finalize (e.g. 7.0.0-RC2)
required: true
env: env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
@@ -14,14 +10,32 @@ permissions:
contents: read contents: read
jobs: jobs:
project-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.project-version.outputs.version }}
steps:
- id: project-version
run: echo "version=$(grep '^version=' gradle.properties | cut -d'=' -f2)" >> $GITHUB_OUTPUT
perform-release: perform-release:
name: Perform Release name: Perform Release
needs: [ project-version ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1 uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
with: with:
should-perform-release: true should-perform-release: true
project-version: ${{ inputs.version }} project-version: ${{ needs.project-version.outputs.version }}
milestone-repo-url: https://repo1.maven.org/maven2 milestone-repo-url: https://repo1.maven.org/maven2
release-repo-url: https://repo1.maven.org/maven2 release-repo-url: https://repo1.maven.org/maven2
artifact-path: org/springframework/security/spring-security-core artifact-path: org/springframework/security/spring-security-core
slack-announcing-id: spring-security-announcing slack-announcing-id: spring-security-announcing
secrets: inherit secrets: inherit
send-notification:
name: Send Notification
needs: [ perform-release ]
if: ${{ !success() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -31,7 +31,6 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebDriverException;
@@ -56,7 +55,6 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -69,7 +67,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* *
* @author Daniel Garnier-Moiroux * @author Daniel Garnier-Moiroux
*/ */
@Disabled @org.junit.jupiter.api.Disabled
class WebAuthnWebDriverTests { class WebAuthnWebDriverTests {
private String baseUrl; private String baseUrl;
@@ -84,8 +82,6 @@ class WebAuthnWebDriverTests {
private static final String PASSWORD = "password"; private static final String PASSWORD = "password";
private String authenticatorId = null;
@BeforeAll @BeforeAll
static void startChromeDriverService() throws Exception { static void startChromeDriverService() throws Exception {
driverService = new ChromeDriverService.Builder().usingAnyFreePort().build(); driverService = new ChromeDriverService.Builder().usingAnyFreePort().build();
@@ -148,7 +144,7 @@ class WebAuthnWebDriverTests {
@Test @Test
void loginWhenNoValidAuthenticatorCredentialsThenRejects() { void loginWhenNoValidAuthenticatorCredentialsThenRejects() {
createVirtualAuthenticator(true); createVirtualAuthenticator(true);
this.getAndWait("/", "/login"); this.driver.get(this.baseUrl);
this.driver.findElement(signinWithPasskeyButton()).click(); this.driver.findElement(signinWithPasskeyButton()).click();
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?error")); await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?error"));
} }
@@ -157,7 +153,7 @@ class WebAuthnWebDriverTests {
void registerWhenNoLabelThenRejects() { void registerWhenNoLabelThenRejects() {
login(); login();
this.getAndWait("/webauthn/register"); this.driver.get(this.baseUrl + "/webauthn/register");
this.driver.findElement(registerPasskeyButton()).click(); this.driver.findElement(registerPasskeyButton()).click();
assertHasAlertStartingWith("error", "Error: Passkey Label is required"); assertHasAlertStartingWith("error", "Error: Passkey Label is required");
@@ -167,7 +163,7 @@ class WebAuthnWebDriverTests {
void registerWhenAuthenticatorNoUserVerificationThenRejects() { void registerWhenAuthenticatorNoUserVerificationThenRejects() {
createVirtualAuthenticator(false); createVirtualAuthenticator(false);
login(); login();
this.getAndWait("/webauthn/register"); this.driver.get(this.baseUrl + "/webauthn/register");
this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator"); this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator");
this.driver.findElement(registerPasskeyButton()).click(); this.driver.findElement(registerPasskeyButton()).click();
@@ -182,8 +178,7 @@ class WebAuthnWebDriverTests {
* <li>Step 1: Log in with username / password</li> * <li>Step 1: Log in with username / password</li>
* <li>Step 2: Register a credential from the virtual authenticator</li> * <li>Step 2: Register a credential from the virtual authenticator</li>
* <li>Step 3: Log out</li> * <li>Step 3: Log out</li>
* <li>Step 4: Log in with the authenticator (no allowCredentials)</li> * <li>Step 4: Log in with the authenticator</li>
* <li>Step 5: Log in again with the same authenticator (with allowCredentials)</li>
* </ul> * </ul>
*/ */
@Test @Test
@@ -195,7 +190,7 @@ class WebAuthnWebDriverTests {
login(); login();
// Step 2: register a credential from the virtual authenticator // Step 2: register a credential from the virtual authenticator
this.getAndWait("/webauthn/register"); this.driver.get(this.baseUrl + "/webauthn/register");
this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator"); this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator");
this.driver.findElement(registerPasskeyButton()).click(); this.driver.findElement(registerPasskeyButton()).click();
@@ -217,58 +212,9 @@ class WebAuthnWebDriverTests {
logout(); logout();
// Step 4: log in with the virtual authenticator // Step 4: log in with the virtual authenticator
this.getAndWait("/webauthn/register", "/login"); this.driver.get(this.baseUrl + "/webauthn/register");
this.driver.findElement(signinWithPasskeyButton()).click(); this.driver.findElement(signinWithPasskeyButton()).click();
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?continue")); await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?continue"));
// Step 5: authenticate while being already logged in
// This simulates some use-cases with MFA. Since the user is already logged in,
// the "allowCredentials" property is populated
this.getAndWait("/login");
this.driver.findElement(signinWithPasskeyButton()).click();
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/"));
}
@Test
void registerWhenAuthenticatorAlreadyRegisteredThenRejects() {
createVirtualAuthenticator(true);
login();
registerAuthenticator("Virtual authenticator");
// Cannot re-register the same authenticator because excludeCredentials
// is not empty and contains the given authenticator
this.driver.findElement(passkeyLabel()).sendKeys("Same authenticator");
this.driver.findElement(registerPasskeyButton()).click();
await(() -> assertHasAlertStartingWith("error", "Registration failed"));
}
@Test
void registerSecondAuthenticatorThenSucceeds() {
createVirtualAuthenticator(true);
login();
registerAuthenticator("Virtual authenticator");
this.getAndWait("/webauthn/register");
List<WebElement> passkeyRows = this.driver.findElements(passkeyTableRows());
assertThat(passkeyRows).hasSize(1)
.first()
.extracting((row) -> row.findElement(firstCell()))
.extracting(WebElement::getText)
.isEqualTo("Virtual authenticator");
// Create second authenticator and register
removeAuthenticator();
createVirtualAuthenticator(true);
registerAuthenticator("Second virtual authenticator");
this.getAndWait("/webauthn/register");
passkeyRows = this.driver.findElements(passkeyTableRows());
assertThat(passkeyRows).hasSize(2)
.extracting((row) -> row.findElement(firstCell()))
.extracting(WebElement::getText)
.contains("Second virtual authenticator");
} }
/** /**
@@ -285,14 +231,11 @@ class WebAuthnWebDriverTests {
* "https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/">https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/</a> * "https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/">https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/</a>
*/ */
private void createVirtualAuthenticator(boolean userIsVerified) { private void createVirtualAuthenticator(boolean userIsVerified) {
if (StringUtils.hasText(this.authenticatorId)) {
throw new IllegalStateException("Authenticator already exists, please remove it before re-creating one");
}
HasCdp cdpDriver = (HasCdp) this.driver; HasCdp cdpDriver = (HasCdp) this.driver;
cdpDriver.executeCdpCommand("WebAuthn.enable", Map.of("enableUI", false)); cdpDriver.executeCdpCommand("WebAuthn.enable", Map.of("enableUI", false));
// this.driver.addVirtualAuthenticator(createVirtualAuthenticatorOptions()); // this.driver.addVirtualAuthenticator(createVirtualAuthenticatorOptions());
//@formatter:off //@formatter:off
Map<String, Object> cmdResponse = cdpDriver.executeCdpCommand("WebAuthn.addVirtualAuthenticator", cdpDriver.executeCdpCommand("WebAuthn.addVirtualAuthenticator",
Map.of( Map.of(
"options", "options",
Map.of( Map.of(
@@ -305,38 +248,21 @@ class WebAuthnWebDriverTests {
) )
)); ));
//@formatter:on //@formatter:on
this.authenticatorId = cmdResponse.get("authenticatorId").toString();
}
private void removeAuthenticator() {
HasCdp cdpDriver = (HasCdp) this.driver;
cdpDriver.executeCdpCommand("WebAuthn.removeVirtualAuthenticator",
Map.of("authenticatorId", this.authenticatorId));
this.authenticatorId = null;
} }
private void login() { private void login() {
this.getAndWait("/", "/login"); this.driver.get(this.baseUrl);
this.driver.findElement(usernameField()).sendKeys(USERNAME); this.driver.findElement(usernameField()).sendKeys(USERNAME);
this.driver.findElement(passwordField()).sendKeys(PASSWORD); this.driver.findElement(passwordField()).sendKeys(PASSWORD);
this.driver.findElement(signinWithUsernamePasswordButton()).click(); this.driver.findElement(signinWithUsernamePasswordButton()).click();
// Ensure login has completed
await(() -> assertThat(this.driver.getCurrentUrl()).doesNotContain("/login"));
} }
private void logout() { private void logout() {
this.getAndWait("/logout"); this.driver.get(this.baseUrl + "/logout");
this.driver.findElement(logoutButton()).click(); this.driver.findElement(logoutButton()).click();
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?logout")); await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?logout"));
} }
private void registerAuthenticator(String passkeyName) {
this.getAndWait("/webauthn/register");
this.driver.findElement(passkeyLabel()).sendKeys(passkeyName);
this.driver.findElement(registerPasskeyButton()).click();
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?success"));
}
private AbstractStringAssert<?> assertHasAlertStartingWith(String alertType, String alertMessage) { private AbstractStringAssert<?> assertHasAlertStartingWith(String alertType, String alertMessage) {
WebElement alert = this.driver.findElement(new By.ById(alertType)); WebElement alert = this.driver.findElement(new By.ById(alertType));
assertThat(alert.isDisplayed()) assertThat(alert.isDisplayed())
@@ -363,15 +289,6 @@ class WebAuthnWebDriverTests {
}); });
} }
private void getAndWait(String endpoint) {
this.getAndWait(endpoint, endpoint);
}
private void getAndWait(String endpoint, String redirectUrl) {
this.driver.get(this.baseUrl + endpoint);
this.await(() -> assertThat(this.driver.getCurrentUrl()).endsWith(redirectUrl));
}
private static By.ById passkeyLabel() { private static By.ById passkeyLabel() {
return new By.ById("label"); return new By.ById("label");
} }
@@ -408,10 +325,6 @@ class WebAuthnWebDriverTests {
return new By.ByCssSelector("button"); return new By.ByCssSelector("button");
} }
private static By.ByCssSelector deletePasskeyButton() {
return new By.ByCssSelector("table > tbody > tr > button");
}
/** /**
* The configuration for WebAuthN tests. It accesses the Server's current port, so we * The configuration for WebAuthN tests. It accesses the Server's current port, so we
* can configurer WebAuthnConfigurer#allowedOrigin * can configurer WebAuthnConfigurer#allowedOrigin
@@ -2052,6 +2052,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* http * http
* // ... * // ...
* .webAuthn((webAuthn) -&gt; webAuthn * .webAuthn((webAuthn) -&gt; webAuthn
* .rpName("Spring Security Relying Party")
* .rpId("example.com") * .rpId("example.com")
* .allowedOrigins("https://example.com") * .allowedOrigins("https://example.com")
* ); * );
@@ -177,7 +177,6 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
webAuthnAuthnFilter.setAuthenticationManager( webAuthnAuthnFilter.setAuthenticationManager(
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService))); new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
webAuthnAuthnFilter = postProcess(webAuthnAuthnFilter);
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials, WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
rpOperations); rpOperations);
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter( PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
@@ -257,10 +256,9 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
PublicKeyCredentialUserEntityRepository userEntities, UserCredentialRepository userCredentials) { PublicKeyCredentialUserEntityRepository userEntities, UserCredentialRepository userCredentials) {
Optional<WebAuthnRelyingPartyOperations> webauthnOperationsBean = getBeanOrNull( Optional<WebAuthnRelyingPartyOperations> webauthnOperationsBean = getBeanOrNull(
WebAuthnRelyingPartyOperations.class); WebAuthnRelyingPartyOperations.class);
String rpName = (this.rpName != null) ? this.rpName : this.rpId; return webauthnOperationsBean.orElseGet(() -> new Webauthn4JRelyingPartyOperations(userEntities,
return webauthnOperationsBean userCredentials, PublicKeyCredentialRpEntity.builder().id(this.rpId).name(this.rpName).build(),
.orElseGet(() -> new Webauthn4JRelyingPartyOperations(userEntities, userCredentials, this.allowedOrigins));
PublicKeyCredentialRpEntity.builder().id(this.rpId).name(rpName).build(), this.allowedOrigins));
} }
} }
@@ -19,13 +19,10 @@ package org.springframework.security.config.annotation.web.configurers;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpOutputMessage;
@@ -45,7 +42,6 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions; import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions; import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository; import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -56,8 +52,6 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@@ -94,14 +88,6 @@ public class WebAuthnConfigurerTests {
.andExpect(content().string(containsString("body {"))); .andExpect(content().string(containsString("body {")));
} }
// gh-18128
@Test
public void webAuthnAuthenticationFilterIsPostProcessed() throws Exception {
this.spring.register(DefaultWebauthnConfiguration.class, PostProcessorConfiguration.class).autowire();
PostProcessorConfiguration postProcess = this.spring.getContext().getBean(PostProcessorConfiguration.class);
assertThat(postProcess.webauthnFilter).isNotNull();
}
@Test @Test
public void webauthnWhenNoFormLoginAndDefaultRegistrationPageConfiguredThenServesJavascript() throws Exception { public void webauthnWhenNoFormLoginAndDefaultRegistrationPageConfiguredThenServesJavascript() throws Exception {
this.spring.register(NoFormLoginAndDefaultRegistrationPageConfiguration.class).autowire(); this.spring.register(NoFormLoginAndDefaultRegistrationPageConfiguration.class).autowire();
@@ -141,42 +127,6 @@ public class WebAuthnConfigurerTests {
.hasSize(1); .hasSize(1);
} }
@Test
void webauthnWhenConfiguredDefaultsRpNameToRpId() throws Exception {
ObjectMapper mapper = new ObjectMapper();
this.spring.register(DefaultWebauthnConfiguration.class).autowire();
String response = this.mvc
.perform(post("/webauthn/register/options").with(csrf())
.with(authentication(new TestingAuthenticationToken("test", "ignored", "ROLE_user"))))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
JsonNode parsedResponse = mapper.readTree(response);
assertThat(parsedResponse.get("rp").get("id").asText()).isEqualTo("example.com");
assertThat(parsedResponse.get("rp").get("name").asText()).isEqualTo("example.com");
}
@Test
void webauthnWhenRpNameConfiguredUsesRpName() throws Exception {
ObjectMapper mapper = new ObjectMapper();
this.spring.register(CustomRpNameWebauthnConfiguration.class).autowire();
String response = this.mvc
.perform(post("/webauthn/register/options").with(csrf())
.with(authentication(new TestingAuthenticationToken("test", "ignored", "ROLE_user"))))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
JsonNode parsedResponse = mapper.readTree(response);
assertThat(parsedResponse.get("rp").get("id").asText()).isEqualTo("example.com");
assertThat(parsedResponse.get("rp").get("name").asText()).isEqualTo("Test RP Name");
}
@Test @Test
public void webauthnWhenConfiguredAndFormLoginThenDoesServesJavascript() throws Exception { public void webauthnWhenConfiguredAndFormLoginThenDoesServesJavascript() throws Exception {
this.spring.register(FormLoginAndNoDefaultRegistrationPageConfiguration.class).autowire(); this.spring.register(FormLoginAndNoDefaultRegistrationPageConfiguration.class).autowire();
@@ -339,26 +289,6 @@ public class WebAuthnConfigurerTests {
} }
@Configuration(proxyBeanMethods = false)
static class PostProcessorConfiguration {
WebAuthnAuthenticationFilter webauthnFilter;
@Bean
BeanPostProcessor beanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof WebAuthnAuthenticationFilter filter) {
PostProcessorConfiguration.this.webauthnFilter = filter;
}
return bean;
}
};
}
}
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
static class DefaultWebauthnConfiguration { static class DefaultWebauthnConfiguration {
@@ -374,7 +304,8 @@ public class WebAuthnConfigurerTests {
http http
.formLogin(Customizer.withDefaults()) .formLogin(Customizer.withDefaults())
.webAuthn((authn) -> authn .webAuthn((authn) -> authn
.rpId("example.com") .rpId("spring.io")
.rpName("spring")
); );
// @formatter:on // @formatter:on
return http.build(); return http.build();
@@ -382,24 +313,6 @@ public class WebAuthnConfigurerTests {
} }
@Configuration
@EnableWebSecurity
static class CustomRpNameWebauthnConfiguration {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.formLogin(Customizer.withDefaults())
.webAuthn((webauthn) -> webauthn.rpId("example.com").rpName("Test RP Name"))
.build();
}
}
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
static class NoFormLoginAndDefaultRegistrationPageConfiguration { static class NoFormLoginAndDefaultRegistrationPageConfiguration {
@@ -16,7 +16,6 @@
package org.springframework.security.crypto.bcrypt; package org.springframework.security.crypto.bcrypt;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom; import java.security.SecureRandom;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -26,7 +25,6 @@ import org.springframework.security.crypto.password.AbstractPasswordEncoderValid
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/** /**
* @author Dave Syer * @author Dave Syer
@@ -238,23 +236,4 @@ public class BCryptPasswordEncoderTests extends AbstractPasswordEncoderValidatio
assertThat(getEncoder().matches(password73chars, encodedPassword73chars)).isTrue(); assertThat(getEncoder().matches(password73chars, encodedPassword73chars)).isTrue();
} }
/**
* Fixes gh-18133
* @author StringManolo
*/
@Test
void passwordLargerThan72BytesShouldThrowIllegalArgumentException() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String singleByteChars = "a".repeat(68);
String password72Bytes = singleByteChars + "😀";
assertThat(password72Bytes.length()).isEqualTo(70);
assertThat(password72Bytes.getBytes(StandardCharsets.UTF_8).length).isEqualTo(72);
assertThatNoException().isThrownBy(() -> encoder.encode(password72Bytes));
String singleByteCharsTooLong = "a".repeat(69);
String password73Bytes = singleByteCharsTooLong + "😀";
assertThat(password73Bytes.getBytes(StandardCharsets.UTF_8).length).isEqualTo(73);
assertThatIllegalArgumentException().isThrownBy(() -> encoder.encode(password73Bytes))
.withMessageContaining("password cannot be more than 72 bytes");
}
} }
-1
View File
@@ -90,7 +90,6 @@
**** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy] **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy]
**** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens] **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
**** xref:servlet/oauth2/resource-server/dpop-tokens.adoc[DPoP-bound Access Tokens] **** xref:servlet/oauth2/resource-server/dpop-tokens.adoc[DPoP-bound Access Tokens]
**** xref:servlet/oauth2/resource-server/protected-resource-metadata.adoc[Protected Resource Metadata]
*** xref:servlet/oauth2/authorization-server/index.adoc[OAuth2 Authorization Server] *** xref:servlet/oauth2/authorization-server/index.adoc[OAuth2 Authorization Server]
**** xref:servlet/oauth2/authorization-server/getting-started.adoc[Getting Started] **** xref:servlet/oauth2/authorization-server/getting-started.adoc[Getting Started]
**** xref:servlet/oauth2/authorization-server/configuration-model.adoc[Configuration Model] **** xref:servlet/oauth2/authorization-server/configuration-model.adoc[Configuration Model]
@@ -1,3 +1,4 @@
[[webflux-cors]] [[webflux-cors]]
= CORS = CORS
@@ -74,11 +75,3 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
} }
---- ----
====== ======
[WARNING]
====
CORS is a browser-based security feature.
By disabling CORS in Spring Security, you are not removing CORS protection from your browser.
Instead, you are removing CORS support from Spring Security, and users will not be able to interact with your Spring backend from a cross-origin browser application.
To fix CORS errors in your application, you must enable CORS support, and provide an appropriate configuration source.
====
@@ -65,6 +65,7 @@ SecurityFilterChain filterChain(HttpSecurity http) {
// ... // ...
.formLogin(withDefaults()) .formLogin(withDefaults())
.webAuthn((webAuthn) -> webAuthn .webAuthn((webAuthn) -> webAuthn
.rpName("Spring Security Relying Party")
.rpId("example.com") .rpId("example.com")
.allowedOrigins("https://example.com") .allowedOrigins("https://example.com")
// optional properties // optional properties
@@ -95,6 +96,7 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
// ... // ...
http { http {
webAuthn { webAuthn {
rpName = "Spring Security Relying Party"
rpId = "example.com" rpId = "example.com"
allowedOrigins = setOf("https://example.com") allowedOrigins = setOf("https://example.com")
// optional properties // optional properties
@@ -183,11 +183,3 @@ fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
} }
---- ----
====== ======
[WARNING]
====
CORS is a browser-based security feature.
By disabling CORS in Spring Security with `.cors(CorsConfigurer::disable)`, you are not removing CORS protection from your browser.
Instead, you are removing CORS support from Spring Security, and users will not be able to interact with your Spring backend from a cross-origin browser application.
To fix CORS errors in your application, you must enable CORS support, and provide an appropriate configuration source.
====
@@ -108,34 +108,6 @@ spring:
require-authorization-consent: true require-authorization-consent: true
---- ----
If you want to customize the default `HttpSecurity` configuration, you may override Spring Boot's auto-configuration with the following example:
[[oauth2AuthorizationServer-minimal-sample-gettingstarted]]
.SecurityConfig.java
[source,java]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http
.authorizeHttpRequests((authorize) ->
authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
);
return http.build();
}
}
----
TIP: Beyond the Getting Started experience, most users will want to customize the default configuration. The xref:servlet/oauth2/authorization-server/getting-started.adoc#oauth2AuthorizationServer-defining-required-components[next section] demonstrates providing all of the necessary beans yourself. TIP: Beyond the Getting Started experience, most users will want to customize the default configuration. The xref:servlet/oauth2/authorization-server/getting-started.adoc#oauth2AuthorizationServer-defining-required-components[next section] demonstrates providing all of the necessary beans yourself.
[[oauth2AuthorizationServer-defining-required-components]] [[oauth2AuthorizationServer-defining-required-components]]
@@ -1,28 +0,0 @@
[[oauth2resourceserver-protected-resource-metadata]]
= OAuth 2.0 Protected Resource Metadata
`OAuth2ResourceServerConfigurer.ProtectedResourceMetadataConfigurer` provides the ability to customize the https://www.rfc-editor.org/rfc/rfc9728.html#section-3[OAuth 2.0 Protected Resource Metadata endpoint].
It defines an extension point that lets you customize the https://www.rfc-editor.org/rfc/rfc9728.html#section-3.2[OAuth 2.0 Protected Resource Metadata response].
`OAuth2ResourceServerConfigurer.ProtectedResourceMetadataConfigurer` provides the following configuration option:
[source,java]
----
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer((resourceServer) ->
resourceServer
.protectedResourceMetadata(protectedResourceMetadata ->
protectedResourceMetadata
.protectedResourceMetadataCustomizer(protectedResourceMetadataCustomizer) <1>
)
);
return http.build();
}
----
<1> `protectedResourceMetadataCustomizer()`: The `Consumer` providing access to the `OAuth2ProtectedResourceMetadata.Builder` allowing the ability to customize the claims of the Resource Server's configuration.
`OAuth2ResourceServerConfigurer.ProtectedResourceMetadataConfigurer` configures the `OAuth2ProtectedResourceMetadataFilter` and registers it with the Resource Server `SecurityFilterChain` `@Bean`.
`OAuth2ProtectedResourceMetadataFilter` is the `Filter` that returns the https://www.rfc-editor.org/rfc/rfc9728.html#section-3.2[OAuth2ProtectedResourceMetadata response].
+1 -1
View File
@@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
# #
springBootVersion=4.0.0-SNAPSHOT springBootVersion=4.0.0-SNAPSHOT
version=7.0.0 version=7.0.0-RC2
samplesBranch=main samplesBranch=main
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true org.gradle.parallel=true
+6 -6
View File
@@ -8,11 +8,11 @@ org-apache-maven-resolver = "1.9.24"
org-aspectj = "1.9.24" org-aspectj = "1.9.24"
org-bouncycastle = "1.80" org-bouncycastle = "1.80"
org-eclipse-jetty = "11.0.26" org-eclipse-jetty = "11.0.26"
org-jetbrains-kotlin = "2.2.21" org-jetbrains-kotlin = "2.2.20"
org-jetbrains-kotlinx = "1.10.2" org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.17.0" org-mockito = "5.17.0"
org-opensaml5 = "5.1.6" org-opensaml5 = "5.1.6"
org-springframework = "7.0.0" org-springframework = "7.0.0-RC1"
com-password4j = "1.8.4" com-password4j = "1.8.4"
[libraries] [libraries]
@@ -30,7 +30,7 @@ commons-collections = "commons-collections:commons-collections:3.2.2"
io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.3" io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.3"
io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.12" io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.12"
io-mockk = "io.mockk:mockk:1.14.6" io-mockk = "io.mockk:mockk:1.14.6"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.0" io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.0-RC1"
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" } 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-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-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
@@ -68,7 +68,7 @@ org-hamcrest = "org.hamcrest:hamcrest:2.2"
org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.0.10.Final" org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:7.0.10.Final"
org-hsqldb = "org.hsqldb:hsqldb:2.7.4" org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21" org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.20"
org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" } org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" }
org-junit-junit-bom = "org.junit:junit-bom:6.0.0" org-junit-junit-bom = "org.junit:junit-bom:6.0.0"
org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" } org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" }
@@ -81,8 +81,8 @@ org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-sup
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3" org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36" org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36"
org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17" org-slf4j-slf4j-api = "org.slf4j:slf4j-api:2.0.17"
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.0" org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2025.1.0-RC1"
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.0" org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:4.0.0-RC1"
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" } 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" org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.1" tools-jackson-jackson-bom = "tools.jackson:jackson-bom:3.0.1"
@@ -466,7 +466,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
/** /**
* The default {@link RowMapper} that maps the current row in * The default {@link RowMapper} that maps the current row in
* {@code java.sql.ResultSet} to {@link OAuth2Authorization} using Jackson 3's * {@code java.sql.ResultSet} to {@link OAuth2Authorization} using Jackson 3's
* {@link JsonMapper}. * {@link JsonMapper} to read all {@code Map<String,Object>} within the result.
* *
* @author Rob Winch * @author Rob Winch
* @since 7.0 * @since 7.0
@@ -482,7 +482,6 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
public JsonMapperOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository, public JsonMapperOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository,
JsonMapper jsonMapper) { JsonMapper jsonMapper) {
super(registeredClientRepository); super(registeredClientRepository);
Assert.notNull(jsonMapper, "jsonMapper cannot be null");
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
} }
@@ -545,7 +544,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
private LobHandler lobHandler = new DefaultLobHandler(); private LobHandler lobHandler = new DefaultLobHandler();
private AbstractOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) { AbstractOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.registeredClientRepository = registeredClientRepository; this.registeredClientRepository = registeredClientRepository;
} }
@@ -714,36 +713,42 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
} }
/** /**
* The default {@code Function} that maps {@link OAuth2Authorization} to a * Nested class to protect from getting {@link NoClassDefFoundError} when Jackson 2 is
* {@code List} of {@link SqlParameterValue} using an instance of Jackson 3's * not on the classpath.
* {@link JsonMapper}. *
* @deprecated This is used to allow transition to Jackson 3. Use {@link Jackson3}
* instead.
*/ */
public static class JsonMapperOAuth2AuthorizationParametersMapper @Deprecated(forRemoval = true, since = "7.0")
extends AbstractOAuth2AuthorizationParametersMapper { private static final class Jackson2 {
private final JsonMapper jsonMapper; static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
public JsonMapperOAuth2AuthorizationParametersMapper() { ClassLoader classLoader = Jackson2.class.getClassLoader();
this(Jackson3.createJsonMapper()); List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
} objectMapper.registerModules(securityModules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
public JsonMapperOAuth2AuthorizationParametersMapper(JsonMapper jsonMapper) { return objectMapper;
Assert.notNull(jsonMapper, "jsonMapper cannot be null");
this.jsonMapper = jsonMapper;
}
@Override
String writeValueAsString(Map<String, Object> data) throws Exception {
return this.jsonMapper.writeValueAsString(data);
} }
} }
/** /**
* A {@code Function} that maps {@link OAuth2Authorization} to a {@code List} of * Nested class used to get a common default instance of {@link JsonMapper}. It is in
* {@link SqlParameterValue} using an instance of Jackson 2's {@link ObjectMapper}. * a nested class to protect from getting {@link NoClassDefFoundError} when Jackson 3
* * is not on the classpath.
* @deprecated Use {@link JsonMapperOAuth2AuthorizationParametersMapper} to switch to */
private static final class Jackson3 {
static JsonMapper createJsonMapper() {
List<JacksonModule> modules = SecurityJacksonModules.getModules(Jackson3.class.getClassLoader());
return JsonMapper.builder().addModules(modules).build();
}
}
/**
* @deprecated Use {@link JsonMapperOAuth2AuthorizationParametersMapper} to migrate to
* Jackson 3. * Jackson 3.
*/ */
@Deprecated(forRemoval = true, since = "7.0") @Deprecated(forRemoval = true, since = "7.0")
@@ -767,6 +772,32 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
} }
/**
* The default {@code Function} that maps {@link OAuth2Authorization} to a
* {@code List} of {@link SqlParameterValue} using an instance of Jackson 3's
* {@link JsonMapper}.
*/
public static final class JsonMapperOAuth2AuthorizationParametersMapper
extends AbstractOAuth2AuthorizationParametersMapper {
private final JsonMapper mapper;
public JsonMapperOAuth2AuthorizationParametersMapper() {
this(Jackson3.createJsonMapper());
}
public JsonMapperOAuth2AuthorizationParametersMapper(JsonMapper mapper) {
Assert.notNull(mapper, "mapper cannot be null");
this.mapper = mapper;
}
@Override
String writeValueAsString(Map<String, Object> data) throws Exception {
return this.mapper.writeValueAsString(data);
}
}
/** /**
* The base {@code Function} that maps {@link OAuth2Authorization} to a {@code List} * The base {@code Function} that maps {@link OAuth2Authorization} to a {@code List}
* of {@link SqlParameterValue}. * of {@link SqlParameterValue}.
@@ -774,7 +805,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
private abstract static class AbstractOAuth2AuthorizationParametersMapper private abstract static class AbstractOAuth2AuthorizationParametersMapper
implements Function<OAuth2Authorization, List<SqlParameterValue>> { implements Function<OAuth2Authorization, List<SqlParameterValue>> {
private AbstractOAuth2AuthorizationParametersMapper() { protected AbstractOAuth2AuthorizationParametersMapper() {
} }
@Override @Override
@@ -885,41 +916,6 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
} }
/**
* Nested class to protect from getting {@link NoClassDefFoundError} when Jackson 2 is
* not on the classpath.
*
* @deprecated This is used to allow transition to Jackson 3. Use {@link Jackson3}
* instead.
*/
@Deprecated(forRemoval = true, since = "7.0")
private static final class Jackson2 {
private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
ClassLoader classLoader = Jackson2.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
objectMapper.registerModules(securityModules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
return objectMapper;
}
}
/**
* Nested class used to get a common default instance of {@link JsonMapper}. It is in
* a nested class to protect from getting {@link NoClassDefFoundError} when Jackson 3
* is not on the classpath.
*/
private static final class Jackson3 {
private static JsonMapper createJsonMapper() {
List<JacksonModule> modules = SecurityJacksonModules.getModules(Jackson3.class.getClassLoader());
return JsonMapper.builder().addModules(modules).build();
}
}
private static final class LobCreatorArgumentPreparedStatementSetter extends ArgumentPreparedStatementSetter { private static final class LobCreatorArgumentPreparedStatementSetter extends ArgumentPreparedStatementSetter {
private final LobCreator lobCreator; private final LobCreator lobCreator;
@@ -33,7 +33,6 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User;
import org.springframework.security.jackson.CoreJacksonModule;
import org.springframework.security.jackson2.CoreJackson2Module; import org.springframework.security.jackson2.CoreJackson2Module;
import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -49,11 +48,9 @@ import org.springframework.security.oauth2.server.authorization.JdbcOAuth2Author
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationServerJacksonModule;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.jackson.WebServletJacksonModule;
import org.springframework.security.web.jackson2.WebServletJackson2Module; import org.springframework.security.web.jackson2.WebServletJackson2Module;
import org.springframework.security.web.savedrequest.DefaultSavedRequest; import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@@ -70,18 +67,7 @@ import org.springframework.util.ClassUtils;
*/ */
class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
private static final boolean jackson2Present; private boolean jackson2Contributed;
private static final boolean jackson3Present;
static {
ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson3Present = ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", classLoader);
}
private boolean jacksonContributed;
@Override @Override
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
@@ -93,17 +79,17 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis
// @formatter:off // @formatter:off
if ((isJdbcBasedOAuth2AuthorizationService || isJdbcBasedRegisteredClientRepository) if ((isJdbcBasedOAuth2AuthorizationService || isJdbcBasedRegisteredClientRepository)
&& !this.jacksonContributed) { && !this.jackson2Contributed) {
JacksonConfigurationBeanRegistrationAotContribution jacksonContribution = Jackson2ConfigurationBeanRegistrationAotContribution jackson2Contribution =
new JacksonConfigurationBeanRegistrationAotContribution(); new Jackson2ConfigurationBeanRegistrationAotContribution();
this.jacksonContributed = true; this.jackson2Contributed = true;
return jacksonContribution; return jackson2Contribution;
} }
// @formatter:on // @formatter:on
return null; return null;
} }
private static class JacksonConfigurationBeanRegistrationAotContribution private static class Jackson2ConfigurationBeanRegistrationAotContribution
implements BeanRegistrationAotContribution { implements BeanRegistrationAotContribution {
private final BindingReflectionHintsRegistrar reflectionHintsRegistrar = new BindingReflectionHintsRegistrar(); private final BindingReflectionHintsRegistrar reflectionHintsRegistrar = new BindingReflectionHintsRegistrar();
@@ -123,6 +109,7 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis
.registerType(HashSet.class, MemberCategory.DECLARED_FIELDS, .registerType(HashSet.class, MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
// Spring Security and Spring Authorization Server
hints.reflection() hints.reflection()
.registerTypes(Arrays.asList(TypeReference.of(AbstractAuthenticationToken.class), .registerTypes(Arrays.asList(TypeReference.of(AbstractAuthenticationToken.class),
TypeReference.of(DefaultSavedRequest.Builder.class), TypeReference.of(DefaultSavedRequest.Builder.class),
@@ -141,30 +128,16 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis
(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS, (builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS));
// Jackson Modules // Jackson Modules - Spring Security and Spring Authorization Server
if (jackson2Present) {
hints.reflection() hints.reflection()
.registerTypes( .registerTypes(
Arrays.asList(TypeReference.of(CoreJackson2Module.class), Arrays.asList(TypeReference.of(CoreJackson2Module.class),
TypeReference.of(WebServletJackson2Module.class), TypeReference.of(WebServletJackson2Module.class),
TypeReference.of(OAuth2AuthorizationServerJackson2Module.class)), TypeReference.of(OAuth2AuthorizationServerJackson2Module.class)),
(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS, (builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS));
MemberCategory.INVOKE_DECLARED_METHODS));
}
if (jackson3Present) {
hints.reflection()
.registerTypes(
Arrays.asList(TypeReference.of(CoreJacksonModule.class),
TypeReference.of(WebServletJacksonModule.class),
TypeReference.of(OAuth2AuthorizationServerJacksonModule.class)),
(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS));
}
// Jackson Mixins // Jackson Mixins - Spring Security and Spring Authorization Server
if (jackson2Present) {
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.jackson2.UnmodifiableSetMixin")); loadClass("org.springframework.security.jackson2.UnmodifiableSetMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
@@ -193,60 +166,23 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis
"org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenExchangeCompositeAuthenticationTokenMixin")); "org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenExchangeCompositeAuthenticationTokenMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass( this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
"org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenFormatMixin")); "org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenFormatMixin"));
}
if (jackson3Present) {
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.web.jackson.DefaultSavedRequestMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.web.jackson.WebAuthenticationDetailsMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.jackson.UsernamePasswordAuthenticationTokenMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.jackson.UserMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.jackson.SimpleGrantedAuthorityMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
"org.springframework.security.oauth2.server.authorization.jackson.OAuth2TokenExchangeActorMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
"org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationRequestMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
"org.springframework.security.oauth2.server.authorization.jackson.OAuth2TokenExchangeCompositeAuthenticationTokenMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
"org.springframework.security.oauth2.server.authorization.jackson.OAuth2TokenFormatMixin"));
}
// Check if OAuth2 Client is on classpath // Check if Spring Security OAuth2 Client is on classpath
if (ClassUtils.isPresent("org.springframework.security.oauth2.client.registration.ClientRegistration", if (ClassUtils.isPresent("org.springframework.security.oauth2.client.registration.ClientRegistration",
ClassUtils.getDefaultClassLoader())) { ClassUtils.getDefaultClassLoader())) {
// Jackson Module (and required types) - Spring Security OAuth2 Client
hints.reflection() hints.reflection()
.registerType(TypeReference .registerTypes(Arrays.asList(
.of("org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken"),
(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS));
// Jackson Module
if (jackson2Present) {
hints.reflection()
.registerType(TypeReference
.of("org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module"),
(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS));
}
if (jackson3Present) {
hints.reflection()
.registerType(
TypeReference TypeReference
.of("org.springframework.security.oauth2.client.jackson.OAuth2ClientJacksonModule"), .of("org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module"),
TypeReference
.of("org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken")),
(builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS, (builder) -> builder.withMembers(MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS)); MemberCategory.INVOKE_DECLARED_METHODS));
}
// Jackson Mixins // Jackson Mixins - Spring Security OAuth2 Client
if (jackson2Present) {
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass( this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
"org.springframework.security.oauth2.client.jackson2.OAuth2AuthenticationTokenMixin")); "org.springframework.security.oauth2.client.jackson2.OAuth2AuthenticationTokenMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
@@ -262,23 +198,6 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.oauth2.client.jackson2.OidcUserInfoMixin")); loadClass("org.springframework.security.oauth2.client.jackson2.OidcUserInfoMixin"));
} }
if (jackson3Present) {
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(), loadClass(
"org.springframework.security.oauth2.client.jackson.OAuth2AuthenticationTokenMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.oauth2.client.jackson.DefaultOidcUserMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.oauth2.client.jackson.DefaultOAuth2UserMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.oauth2.client.jackson.OidcUserAuthorityMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.oauth2.client.jackson.OAuth2UserAuthorityMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.oauth2.client.jackson.OidcIdTokenMixin"));
this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
loadClass("org.springframework.security.oauth2.client.jackson.OidcUserInfoMixin"));
}
}
} }
private static Class<?> loadClass(String className) { private static Class<?> loadClass(String className) {
@@ -28,23 +28,19 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameterValue; import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.security.jackson.SecurityJacksonModules;
import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -138,8 +134,8 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
public JdbcRegisteredClientRepository(JdbcOperations jdbcOperations) { public JdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null"); Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
this.jdbcOperations = jdbcOperations; this.jdbcOperations = jdbcOperations;
this.registeredClientRowMapper = new JsonMapperRegisteredClientRowMapper(); this.registeredClientRowMapper = new RegisteredClientRowMapper();
this.registeredClientParametersMapper = new JsonMapperRegisteredClientParametersMapper(); this.registeredClientParametersMapper = new RegisteredClientParametersMapper();
} }
@Override @Override
@@ -210,7 +206,7 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
/** /**
* Sets the {@link RowMapper} used for mapping the current row in * Sets the {@link RowMapper} used for mapping the current row in
* {@code java.sql.ResultSet} to {@link RegisteredClient}. The default is * {@code java.sql.ResultSet} to {@link RegisteredClient}. The default is
* {@link JsonMapperRegisteredClientRowMapper}. * {@link RegisteredClientRowMapper}.
* @param registeredClientRowMapper the {@link RowMapper} used for mapping the current * @param registeredClientRowMapper the {@link RowMapper} used for mapping the current
* row in {@code ResultSet} to {@link RegisteredClient} * row in {@code ResultSet} to {@link RegisteredClient}
*/ */
@@ -222,7 +218,7 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
/** /**
* Sets the {@code Function} used for mapping {@link RegisteredClient} to a * Sets the {@code Function} used for mapping {@link RegisteredClient} to a
* {@code List} of {@link SqlParameterValue}. The default is * {@code List} of {@link SqlParameterValue}. The default is
* {@link JsonMapperRegisteredClientParametersMapper}. * {@link RegisteredClientParametersMapper}.
* @param registeredClientParametersMapper the {@code Function} used for mapping * @param registeredClientParametersMapper the {@code Function} used for mapping
* {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue} * {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}
*/ */
@@ -246,76 +242,17 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
/** /**
* The default {@link RowMapper} that maps the current row in * The default {@link RowMapper} that maps the current row in
* {@code java.sql.ResultSet} to {@link RegisteredClient} using Jackson 3's * {@code java.sql.ResultSet} to {@link RegisteredClient}.
* {@link JsonMapper}.
*
* @author Joe Grandja
* @since 7.0
*/ */
public static class JsonMapperRegisteredClientRowMapper extends AbstractRegisteredClientRowMapper { public static class RegisteredClientRowMapper implements RowMapper<RegisteredClient> {
private final JsonMapper jsonMapper; private ObjectMapper objectMapper = new ObjectMapper();
public JsonMapperRegisteredClientRowMapper() { public RegisteredClientRowMapper() {
this(Jackson3.createJsonMapper()); ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
} List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
this.objectMapper.registerModules(securityModules);
public JsonMapperRegisteredClientRowMapper(JsonMapper jsonMapper) { this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
Assert.notNull(jsonMapper, "jsonMapper cannot be null");
this.jsonMapper = jsonMapper;
}
@Override
Map<String, Object> readValue(String data) {
final ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<>() {
};
tools.jackson.databind.JavaType javaType = this.jsonMapper.getTypeFactory()
.constructType(typeReference.getType());
return this.jsonMapper.readValue(data, javaType);
}
}
/**
* A {@link RowMapper} that maps the current row in {@code java.sql.ResultSet} to
* {@link RegisteredClient} using Jackson 2's {@link ObjectMapper}.
*
* @deprecated Use {@link JsonMapperRegisteredClientRowMapper} to switch to Jackson 3.
*/
@Deprecated(forRemoval = true, since = "7.0")
public static class RegisteredClientRowMapper extends AbstractRegisteredClientRowMapper {
private ObjectMapper objectMapper = Jackson2.createObjectMapper();
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
@Override
Map<String, Object> readValue(String data) throws JsonProcessingException {
final ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<>() {
};
com.fasterxml.jackson.databind.JavaType javaType = this.objectMapper.getTypeFactory()
.constructType(typeReference.getType());
return this.objectMapper.readValue(data, javaType);
}
}
/**
* The base {@link RowMapper} that maps the current row in {@code java.sql.ResultSet}
* to {@link RegisteredClient}. This is extracted to a distinct class so that
* {@link RegisteredClientRowMapper} can be deprecated in favor of
* {@link JsonMapperRegisteredClientRowMapper}.
*/
private abstract static class AbstractRegisteredClientRowMapper implements RowMapper<RegisteredClient> {
private AbstractRegisteredClientRowMapper() {
} }
@Override @Override
@@ -362,17 +299,25 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
return builder.build(); return builder.build();
} }
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
private Map<String, Object> parseMap(String data) { private Map<String, Object> parseMap(String data) {
try { try {
return readValue(data); return this.objectMapper.readValue(data, new TypeReference<>() {
});
} }
catch (Exception ex) { catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex); throw new IllegalArgumentException(ex.getMessage(), ex);
} }
} }
abstract Map<String, Object> readValue(String data) throws Exception;
private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) { private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) { if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
return AuthorizationGrantType.AUTHORIZATION_CODE; return AuthorizationGrantType.AUTHORIZATION_CODE;
@@ -405,64 +350,18 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
/** /**
* The default {@code Function} that maps {@link RegisteredClient} to a {@code List} * The default {@code Function} that maps {@link RegisteredClient} to a {@code List}
* of {@link SqlParameterValue} using an instance of Jackson 3's {@link JsonMapper}. * of {@link SqlParameterValue}.
*/ */
public static class JsonMapperRegisteredClientParametersMapper extends AbstractRegisteredClientParametersMapper { public static class RegisteredClientParametersMapper
private final JsonMapper jsonMapper;
public JsonMapperRegisteredClientParametersMapper() {
this(Jackson3.createJsonMapper());
}
public JsonMapperRegisteredClientParametersMapper(JsonMapper jsonMapper) {
Assert.notNull(jsonMapper, "jsonMapper cannot be null");
this.jsonMapper = jsonMapper;
}
@Override
String writeValueAsString(Map<String, Object> data) throws Exception {
return this.jsonMapper.writeValueAsString(data);
}
}
/**
* A {@code Function} that maps {@link RegisteredClient} to a {@code List} of
* {@link SqlParameterValue} using an instance of Jackson 2's {@link ObjectMapper}.
*
* @deprecated Use {@link JsonMapperRegisteredClientParametersMapper} to switch to
* Jackson 3.
*/
@Deprecated(forRemoval = true, since = "7.0")
public static class RegisteredClientParametersMapper extends AbstractRegisteredClientParametersMapper {
private ObjectMapper objectMapper = Jackson2.createObjectMapper();
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
@Override
String writeValueAsString(Map<String, Object> data) throws JsonProcessingException {
return this.objectMapper.writeValueAsString(data);
}
}
/**
* The base {@code Function} that maps {@link RegisteredClient} to a {@code List} of
* {@link SqlParameterValue}.
*/
private abstract static class AbstractRegisteredClientParametersMapper
implements Function<RegisteredClient, List<SqlParameterValue>> { implements Function<RegisteredClient, List<SqlParameterValue>> {
private AbstractRegisteredClientParametersMapper() { private ObjectMapper objectMapper = new ObjectMapper();
public RegisteredClientParametersMapper() {
ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
this.objectMapper.registerModules(securityModules);
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
} }
@Override @Override
@@ -504,52 +403,24 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().getSettings()))); new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().getSettings())));
} }
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
private String writeMap(Map<String, Object> data) { private String writeMap(Map<String, Object> data) {
try { try {
return writeValueAsString(data); return this.objectMapper.writeValueAsString(data);
} }
catch (Exception ex) { catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex); throw new IllegalArgumentException(ex.getMessage(), ex);
} }
} }
abstract String writeValueAsString(Map<String, Object> data) throws Exception;
}
/**
* Nested class to protect from getting {@link NoClassDefFoundError} when Jackson 2 is
* not on the classpath.
*
* @deprecated This is used to allow transition to Jackson 3. Use {@link Jackson3}
* instead.
*/
@Deprecated(forRemoval = true, since = "7.0")
private static final class Jackson2 {
private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
ClassLoader classLoader = Jackson2.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
objectMapper.registerModules(securityModules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
return objectMapper;
}
}
/**
* Nested class used to get a common default instance of {@link JsonMapper}. It is in
* a nested class to protect from getting {@link NoClassDefFoundError} when Jackson 3
* is not on the classpath.
*/
private static final class Jackson3 {
private static JsonMapper createJsonMapper() {
List<JacksonModule> modules = SecurityJacksonModules.getModules(Jackson3.class.getClassLoader());
return JsonMapper.builder().addModules(modules).build();
}
} }
static class JdbcRegisteredClientRepositoryRuntimeHintsRegistrar implements RuntimeHintsRegistrar { static class JdbcRegisteredClientRepositoryRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
@@ -25,13 +25,13 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@@ -41,12 +41,13 @@ import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.jackson.SecurityJacksonModules; import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.JsonMapperRegisteredClientParametersMapper; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.JsonMapperRegisteredClientRowMapper; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientRowMapper;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -221,9 +222,9 @@ public class JdbcRegisteredClientRepositoryTests {
@Test @Test
public void saveLoadRegisteredClientWhenCustomStrategiesSetThenCalled() throws Exception { public void saveLoadRegisteredClientWhenCustomStrategiesSetThenCalled() throws Exception {
RowMapper<RegisteredClient> registeredClientRowMapper = spy(new JsonMapperRegisteredClientRowMapper()); RowMapper<RegisteredClient> registeredClientRowMapper = spy(new RegisteredClientRowMapper());
this.registeredClientRepository.setRegisteredClientRowMapper(registeredClientRowMapper); this.registeredClientRepository.setRegisteredClientRowMapper(registeredClientRowMapper);
JsonMapperRegisteredClientParametersMapper clientParametersMapper = new JsonMapperRegisteredClientParametersMapper(); RegisteredClientParametersMapper clientParametersMapper = new RegisteredClientParametersMapper();
Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper = spy( Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper = spy(
clientParametersMapper); clientParametersMapper);
this.registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper); this.registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
@@ -364,14 +365,16 @@ public class JdbcRegisteredClientRepositoryTests {
return !result.isEmpty() ? result.get(0) : null; return !result.isEmpty() ? result.get(0) : null;
} }
@SuppressWarnings("removal")
private static final class CustomRegisteredClientRowMapper implements RowMapper<RegisteredClient> { private static final class CustomRegisteredClientRowMapper implements RowMapper<RegisteredClient> {
private final JsonMapper jsonMapper; private final ObjectMapper objectMapper = new ObjectMapper();
private CustomRegisteredClientRowMapper() { private CustomRegisteredClientRowMapper() {
List<JacksonModule> modules = SecurityJacksonModules ClassLoader classLoader = CustomJdbcRegisteredClientRepository.class.getClassLoader();
.getModules(CustomRegisteredClientRowMapper.class.getClassLoader()); List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
this.jsonMapper = JsonMapper.builder().addModules(modules).build(); this.objectMapper.registerModules(securityModules);
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
} }
@Override @Override
@@ -415,12 +418,9 @@ public class JdbcRegisteredClientRepositoryTests {
} }
private Map<String, Object> parseMap(String data) { private Map<String, Object> parseMap(String data) {
final ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<>() {
};
try { try {
tools.jackson.databind.JavaType javaType = this.jsonMapper.getTypeFactory() return this.objectMapper.readValue(data, new TypeReference<>() {
.constructType(typeReference.getType()); });
return this.jsonMapper.readValue(data, javaType);
} }
catch (Exception ex) { catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex); throw new IllegalArgumentException(ex.getMessage(), ex);
@@ -58,7 +58,7 @@ public class RequestAttributeAuthenticationFilter extends AbstractPreAuthenticat
* missing and {@code exceptionIfVariableMissing} is set to {@code true}. * missing and {@code exceptionIfVariableMissing} is set to {@code true}.
*/ */
@Override @Override
protected @Nullable Object getPreAuthenticatedPrincipal(HttpServletRequest request) { protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String principal = (String) request.getAttribute(this.principalEnvironmentVariable); String principal = (String) request.getAttribute(this.principalEnvironmentVariable);
if (principal == null && this.exceptionIfVariableMissing) { if (principal == null && this.exceptionIfVariableMissing) {
throw new PreAuthenticatedCredentialsNotFoundException( throw new PreAuthenticatedCredentialsNotFoundException(
@@ -73,7 +73,7 @@ public class RequestAttributeAuthenticationFilter extends AbstractPreAuthenticat
* credentials value. Otherwise a dummy value will be used. * credentials value. Otherwise a dummy value will be used.
*/ */
@Override @Override
protected @Nullable Object getPreAuthenticatedCredentials(HttpServletRequest request) { protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (this.credentialsEnvironmentVariable != null) { if (this.credentialsEnvironmentVariable != null) {
return request.getAttribute(this.credentialsEnvironmentVariable); return request.getAttribute(this.credentialsEnvironmentVariable);
} }
@@ -59,7 +59,7 @@ public class RequestHeaderAuthenticationFilter extends AbstractPreAuthenticatedP
* {@code exceptionIfHeaderMissing} is set to {@code true}. * {@code exceptionIfHeaderMissing} is set to {@code true}.
*/ */
@Override @Override
protected @Nullable Object getPreAuthenticatedPrincipal(HttpServletRequest request) { protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String principal = request.getHeader(this.principalRequestHeader); String principal = request.getHeader(this.principalRequestHeader);
if (principal == null && this.exceptionIfHeaderMissing) { if (principal == null && this.exceptionIfHeaderMissing) {
throw new PreAuthenticatedCredentialsNotFoundException( throw new PreAuthenticatedCredentialsNotFoundException(
@@ -74,7 +74,7 @@ public class RequestHeaderAuthenticationFilter extends AbstractPreAuthenticatedP
* will be used. * will be used.
*/ */
@Override @Override
protected @Nullable Object getPreAuthenticatedCredentials(HttpServletRequest request) { protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (this.credentialsRequestHeader != null) { if (this.credentialsRequestHeader != null) {
return request.getHeader(this.credentialsRequestHeader); return request.getHeader(this.credentialsRequestHeader);
} }
@@ -22,15 +22,15 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.webauthn4j.WebAuthnManager; import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.authenticator.Authenticator;
import com.webauthn4j.authenticator.AuthenticatorImpl;
import com.webauthn4j.converter.util.CborConverter; import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.ObjectConverter; import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.credential.CredentialRecordImpl;
import com.webauthn4j.data.AuthenticationData; import com.webauthn4j.data.AuthenticationData;
import com.webauthn4j.data.AuthenticationParameters; import com.webauthn4j.data.AuthenticationParameters;
import com.webauthn4j.data.RegistrationData; import com.webauthn4j.data.RegistrationData;
@@ -95,7 +95,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
private final PublicKeyCredentialRpEntity rp; private final PublicKeyCredentialRpEntity rp;
private ObjectConverter objectConverter = new ObjectConverter(); private final ObjectConverter objectConverter = new ObjectConverter();
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@@ -137,15 +137,6 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
this.webAuthnManager = webAuthnManager; this.webAuthnManager = webAuthnManager;
} }
/**
* Sets the {@link ObjectConverter} to use.
* @param objectConverter the {@link ObjectConverter} to use. Cannot be null.
*/
void setObjectConverter(ObjectConverter objectConverter) {
Assert.notNull(objectConverter, "objectConverter cannot be null");
this.objectConverter = objectConverter;
}
/** /**
* Sets a {@link Consumer} used to customize the * Sets a {@link Consumer} used to customize the
* {@link PublicKeyCredentialCreationOptionsBuilder} for * {@link PublicKeyCredentialCreationOptionsBuilder} for
@@ -257,7 +248,9 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
byte[] attestationObject = response.getAttestationObject().getBytes(); byte[] attestationObject = response.getAttestationObject().getBytes();
byte[] clientDataJSON = response.getClientDataJSON().getBytes(); byte[] clientDataJSON = response.getClientDataJSON().getBytes();
Challenge challenge = new DefaultChallenge(base64Challenge); Challenge challenge = new DefaultChallenge(base64Challenge);
ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge); byte[] tokenBindingId = null /* set tokenBindingId */; // FIXME:
// https://www.w3.org/TR/webauthn-1/#dom-collectedclientdata-tokenbinding
ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge, tokenBindingId);
boolean userVerificationRequired = creationOptions.getAuthenticatorSelection() boolean userVerificationRequired = creationOptions.getAuthenticatorSelection()
.getUserVerification() == UserVerificationRequirement.REQUIRED; .getUserVerification() == UserVerificationRequirement.REQUIRED;
// requireUserPresence The constant Boolean value true // requireUserPresence The constant Boolean value true
@@ -270,7 +263,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
transports); transports);
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, pubKeyCredParams, RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, pubKeyCredParams,
userVerificationRequired, userPresenceRequired); userVerificationRequired, userPresenceRequired);
RegistrationData wa4jRegistrationData = this.webAuthnManager.verify(webauthn4jRegistrationRequest, RegistrationData wa4jRegistrationData = this.webAuthnManager.validate(webauthn4jRegistrationRequest,
registrationParameters); registrationParameters);
AttestationObject wa4jAttestationObject = wa4jRegistrationData.getAttestationObject(); AttestationObject wa4jAttestationObject = wa4jRegistrationData.getAttestationObject();
Assert.notNull(wa4jAttestationObject, "attestationObject cannot be null"); Assert.notNull(wa4jAttestationObject, "attestationObject cannot be null");
@@ -313,7 +306,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
private List<com.webauthn4j.data.PublicKeyCredentialParameters> convertCredentialParamsToWebauthn4j( private List<com.webauthn4j.data.PublicKeyCredentialParameters> convertCredentialParamsToWebauthn4j(
List<PublicKeyCredentialParameters> parameters) { List<PublicKeyCredentialParameters> parameters) {
return parameters.stream().map(this::convertParamToWebauthn4j).toList(); return parameters.stream().map(this::convertParamToWebauthn4j).collect(Collectors.toUnmodifiableList());
} }
private com.webauthn4j.data.PublicKeyCredentialParameters convertParamToWebauthn4j( private com.webauthn4j.data.PublicKeyCredentialParameters convertParamToWebauthn4j(
@@ -389,29 +382,28 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
.getAuthenticatorData(); .getAuthenticatorData();
AttestedCredentialData wa4jCredData = wa4jAuthData.getAttestedCredentialData(); AttestedCredentialData wa4jCredData = wa4jAuthData.getAttestedCredentialData();
Assert.notNull(wa4jCredData, "attestedCredentialData cannot be null"); Assert.notNull(wa4jCredData, "attestedCredentialData cannot be null");
AttestedCredentialData data = new AttestedCredentialData(wa4jCredData.getAaguid(), keyId.getBytes(),
wa4jCredData.getCOSEKey());
Authenticator authenticator = new AuthenticatorImpl(data, wa4jAttestationObject.getAttestationStatement(),
credentialRecord.getSignatureCount());
Set<Origin> origins = toOrigins(); Set<Origin> origins = toOrigins();
Challenge challenge = new DefaultChallenge(requestOptions.getChallenge().getBytes()); Challenge challenge = new DefaultChallenge(requestOptions.getChallenge().getBytes());
// FIXME: should populate this
byte[] tokenBindingId = null /* set tokenBindingId */;
String rpId = requestOptions.getRpId(); String rpId = requestOptions.getRpId();
Assert.notNull(rpId, "rpId cannot be null"); Assert.notNull(rpId, "rpId cannot be null");
ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge); ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge, tokenBindingId);
boolean userVerificationRequired = request.getRequestOptions() boolean userVerificationRequired = request.getRequestOptions()
.getUserVerification() == UserVerificationRequirement.REQUIRED; .getUserVerification() == UserVerificationRequirement.REQUIRED;
com.webauthn4j.data.AuthenticationRequest authenticationRequest = new com.webauthn4j.data.AuthenticationRequest( com.webauthn4j.data.AuthenticationRequest authenticationRequest = new com.webauthn4j.data.AuthenticationRequest(
request.getPublicKey().getRawId().getBytes(), assertionResponse.getAuthenticatorData().getBytes(), request.getPublicKey().getId().getBytes(), assertionResponse.getAuthenticatorData().getBytes(),
assertionResponse.getClientDataJSON().getBytes(), assertionResponse.getSignature().getBytes()); assertionResponse.getClientDataJSON().getBytes(), assertionResponse.getSignature().getBytes());
AuthenticationParameters authenticationParameters = new AuthenticationParameters(serverProperty, authenticator,
userVerificationRequired);
// CollectedClientData and ExtensionsClientOutputs is registration data, and can AuthenticationData wa4jAuthenticationData = this.webAuthnManager.validate(authenticationRequest,
// be null at authentication time.
com.webauthn4j.credential.CredentialRecord wa4jCredentialRecord = new CredentialRecordImpl(
wa4jAttestationObject, null, null, convertTransportsToWebauthn4j(credentialRecord.getTransports()));
List<byte[]> allowCredentials = convertAllowedCredentialsToWebauthn4j(
request.getRequestOptions().getAllowCredentials());
AuthenticationParameters authenticationParameters = new AuthenticationParameters(serverProperty,
wa4jCredentialRecord, allowCredentials.isEmpty() ? null : allowCredentials, userVerificationRequired);
AuthenticationData wa4jAuthenticationData = this.webAuthnManager.verify(authenticationRequest,
authenticationParameters); authenticationParameters);
AuthenticatorData<AuthenticationExtensionAuthenticatorOutput> wa4jValidatedAuthData = wa4jAuthenticationData AuthenticatorData<AuthenticationExtensionAuthenticatorOutput> wa4jValidatedAuthData = wa4jAuthenticationData
@@ -432,21 +424,4 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
return userEntity; return userEntity;
} }
private static Set<com.webauthn4j.data.AuthenticatorTransport> convertTransportsToWebauthn4j(
Set<AuthenticatorTransport> transports) {
return transports.stream()
.map(AuthenticatorTransport::getValue)
.map(com.webauthn4j.data.AuthenticatorTransport::create)
.collect(Collectors.toSet());
}
private static List<byte[]> convertAllowedCredentialsToWebauthn4j(
List<PublicKeyCredentialDescriptor> allowedCredentials) {
return allowedCredentials.stream()
.map(PublicKeyCredentialDescriptor::getId)
.filter(Objects::nonNull)
.map(Bytes::getBytes)
.collect(Collectors.toList());
}
} }
@@ -27,20 +27,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.converter.AttestationObjectConverter; import com.webauthn4j.converter.AttestationObjectConverter;
import com.webauthn4j.converter.util.ObjectConverter; import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.data.AuthenticationData;
import com.webauthn4j.data.AuthenticationRequest;
import com.webauthn4j.data.attestation.AttestationObject; import com.webauthn4j.data.attestation.AttestationObject;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
import com.webauthn4j.data.attestation.authenticator.AuthenticatorData; import com.webauthn4j.data.attestation.authenticator.AuthenticatorData;
import com.webauthn4j.data.extension.authenticator.RegistrationExtensionAuthenticatorOutput; import com.webauthn4j.data.extension.authenticator.RegistrationExtensionAuthenticatorOutput;
import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
@@ -49,14 +44,12 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse; import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder; import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder;
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria; import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport; import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
import org.springframework.security.web.webauthn.api.Bytes; import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord; import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredential; import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions; import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor; import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor;
@@ -64,11 +57,9 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialParamete
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity; import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.TestAuthenticationAssertionResponses;
import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses; import org.springframework.security.web.webauthn.api.TestAuthenticatorAttestationResponses;
import org.springframework.security.web.webauthn.api.TestCredentialRecords; import org.springframework.security.web.webauthn.api.TestCredentialRecords;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions; import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities; import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentials; import org.springframework.security.web.webauthn.api.TestPublicKeyCredentials;
import org.springframework.security.web.webauthn.api.UserVerificationRequirement; import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
@@ -76,9 +67,7 @@ import org.springframework.security.web.webauthn.api.UserVerificationRequirement
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatRuntimeException; import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@@ -598,50 +587,6 @@ class Webauthn4jRelyingPartyOperationsTests {
.containsExactly(credentialRecord.getCredentialId()); .containsExactly(credentialRecord.getCredentialId());
} }
// gh-18158
@Test
void authenticateThenWa4jRequestCredentialIdIsRawIdBytes() throws Exception {
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create().build();
AuthenticatorAssertionResponse response = TestAuthenticationAssertionResponses
.createAuthenticatorAssertionResponse()
.build();
PublicKeyCredential<AuthenticatorAssertionResponse> credentials = TestPublicKeyCredentials
.createPublicKeyCredential(response)
.build();
RelyingPartyAuthenticationRequest request = new RelyingPartyAuthenticationRequest(options, credentials);
PublicKeyCredential<AuthenticatorAssertionResponse> publicKey = request.getPublicKey();
ImmutableCredentialRecord credentialRecord = TestCredentialRecords.fullUserCredential().build();
given(this.userCredentials.findByCredentialId(publicKey.getRawId())).willReturn(credentialRecord);
ObjectMapper json = mock(ObjectMapper.class);
ObjectMapper cbor = mock(ObjectMapper.class);
given(cbor.getFactory()).willReturn(mock(CBORFactory.class));
AttestationObject attestationObject = mock(AttestationObject.class);
AuthenticatorData wa4jAuthData = mock(AuthenticatorData.class);
given(attestationObject.getAuthenticatorData()).willReturn(wa4jAuthData);
given(wa4jAuthData.getAttestedCredentialData()).willReturn(mock(AttestedCredentialData.class));
given(cbor.readValue(credentialRecord.getAttestationObject().getBytes(), AttestationObject.class))
.willReturn(attestationObject);
this.rpOperations.setObjectConverter(new ObjectConverter(json, cbor));
WebAuthnManager manager = mock(WebAuthnManager.class);
ArgumentCaptor<AuthenticationRequest> wa4jRequest = ArgumentCaptor.forClass(AuthenticationRequest.class);
AuthenticationData wa4jData = mock(AuthenticationData.class);
given(wa4jData.getAuthenticatorData()).willReturn(mock(AuthenticatorData.class));
given(manager.verify(wa4jRequest.capture(), any())).willReturn(wa4jData);
given(this.userEntities.findById(any())).willReturn(TestPublicKeyCredentialUserEntities.userEntity().build());
this.rpOperations.setWebAuthnManager(manager);
this.rpOperations.authenticate(request);
// this ensures that our next assertion is valid (we want the rawId bytes, not the
// id bytes to be used)
assertThat(publicKey.getRawId().getBytes()).isNotEqualTo(publicKey.getId().getBytes());
// ensure that the raw id bytes are passed into webauthn4j (not the id bytes which
// are base64 encoded)
assertThat(wa4jRequest.getValue().getCredentialId()).isEqualTo(publicKey.getRawId().getBytes());
}
private static AuthenticatorAttestationResponse setFlag(byte... flags) throws Exception { private static AuthenticatorAttestationResponse setFlag(byte... flags) throws Exception {
AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponses AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponses
.createAuthenticatorAttestationResponse(); .createAuthenticatorAttestationResponse();