Skip to content

Commit 10ca8c7

Browse files
authored
Merge branch 'spring-projects:main' into feature/AbstractConfiguredSecurityBuilder-getSharedObject-nullable-ann
2 parents 7184cb4 + 6d6552a commit 10ca8c7

File tree

40 files changed

+820
-344
lines changed

40 files changed

+820
-344
lines changed

.github/dependabot.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,3 @@ updates:
111111
labels:
112112
- 'type: task'
113113
- 'in: build'
114-
- package-ecosystem: npm
115-
target-branch: 6.3.x
116-
directory: /docs
117-
schedule:
118-
interval: weekly
119-
labels:
120-
- 'type: task'
121-
- 'in: build'

.github/workflows/continuous-integration-workflow.yml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ jobs:
3535
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
3636
default-publish-milestones-central: true
3737
secrets: inherit
38-
deploy-docs:
39-
name: Deploy Docs
40-
needs: [ build ]
41-
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1
42-
with:
43-
should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }}
44-
secrets: inherit
4538
deploy-schema:
4639
name: Deploy Schema
4740
needs: [ build ]
@@ -51,7 +44,7 @@ jobs:
5144
secrets: inherit
5245
perform-release:
5346
name: Perform Release
54-
needs: [ deploy-artifacts, deploy-docs, deploy-schema ]
47+
needs: [ deploy-artifacts, deploy-schema ]
5548
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
5649
with:
5750
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ springRelease {
4242
weekOfMonth = 3
4343
dayOfWeek = 1
4444
referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html"
45-
apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/"
45+
apiDocUrl = "https://docs.spring.io/spring-security/reference/{version}/api/java/index.html"
4646
replaceSnapshotVersionInReferenceDocUrl = true
4747
}
4848

buildSrc/src/main/groovy/io/spring/gradle/convention/DeployDocsPlugin.groovy

Lines changed: 0 additions & 82 deletions
This file was deleted.

buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public class DocsPlugin implements Plugin<Project> {
1717

1818
PluginManager pluginManager = project.getPluginManager();
1919
pluginManager.apply(BasePlugin);
20-
pluginManager.apply(DeployDocsPlugin);
2120
pluginManager.apply(JavadocApiPlugin);
2221

2322
Task docsZip = project.tasks.create('docsZip', Zip) {

buildSrc/src/test/resources/samples/showcase/Jenkinsfile

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,6 @@ ossrh: {
3030
}
3131
}
3232
},
33-
docs: {
34-
stage('Deploy Docs') {
35-
node {
36-
checkout scm
37-
withCredentials([file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')]) {
38-
sh "./gradlew deployDocs -PdeployDocsSshKeyPath=$DEPLOY_SSH_KEY -PdeployDocsSshUsername=$SPRING_DOCS_USERNAME --refresh-dependencies --no-daemon --stacktrace"
39-
}
40-
}
41-
}
42-
},
4333
schema: {
4434
stage('Deploy Schema') {
4535
node {
@@ -49,4 +39,4 @@ schema: {
4939
}
5040
}
5141
}
52-
}
42+
}

config/src/integration-test/java/org/springframework/security/config/annotation/configurers/WebAuthnWebDriverTests.java

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.junit.jupiter.api.AfterEach;
3232
import org.junit.jupiter.api.BeforeAll;
3333
import org.junit.jupiter.api.BeforeEach;
34+
import org.junit.jupiter.api.Disabled;
3435
import org.junit.jupiter.api.Test;
3536
import org.openqa.selenium.By;
3637
import org.openqa.selenium.WebDriverException;
@@ -55,6 +56,7 @@
5556
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
5657
import org.springframework.security.web.FilterChainProxy;
5758
import org.springframework.security.web.SecurityFilterChain;
59+
import org.springframework.util.StringUtils;
5860
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
5961
import org.springframework.web.filter.DelegatingFilterProxy;
6062
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -67,7 +69,7 @@
6769
*
6870
* @author Daniel Garnier-Moiroux
6971
*/
70-
@org.junit.jupiter.api.Disabled
72+
@Disabled
7173
class WebAuthnWebDriverTests {
7274

7375
private String baseUrl;
@@ -82,6 +84,8 @@ class WebAuthnWebDriverTests {
8284

8385
private static final String PASSWORD = "password";
8486

87+
private String authenticatorId = null;
88+
8589
@BeforeAll
8690
static void startChromeDriverService() throws Exception {
8791
driverService = new ChromeDriverService.Builder().usingAnyFreePort().build();
@@ -144,7 +148,7 @@ void cleanupDriver() {
144148
@Test
145149
void loginWhenNoValidAuthenticatorCredentialsThenRejects() {
146150
createVirtualAuthenticator(true);
147-
this.driver.get(this.baseUrl);
151+
this.getAndWait("/", "/login");
148152
this.driver.findElement(signinWithPasskeyButton()).click();
149153
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?error"));
150154
}
@@ -153,7 +157,7 @@ void loginWhenNoValidAuthenticatorCredentialsThenRejects() {
153157
void registerWhenNoLabelThenRejects() {
154158
login();
155159

156-
this.driver.get(this.baseUrl + "/webauthn/register");
160+
this.getAndWait("/webauthn/register");
157161

158162
this.driver.findElement(registerPasskeyButton()).click();
159163
assertHasAlertStartingWith("error", "Error: Passkey Label is required");
@@ -163,7 +167,7 @@ void registerWhenNoLabelThenRejects() {
163167
void registerWhenAuthenticatorNoUserVerificationThenRejects() {
164168
createVirtualAuthenticator(false);
165169
login();
166-
this.driver.get(this.baseUrl + "/webauthn/register");
170+
this.getAndWait("/webauthn/register");
167171
this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator");
168172
this.driver.findElement(registerPasskeyButton()).click();
169173

@@ -178,7 +182,8 @@ void registerWhenAuthenticatorNoUserVerificationThenRejects() {
178182
* <li>Step 1: Log in with username / password</li>
179183
* <li>Step 2: Register a credential from the virtual authenticator</li>
180184
* <li>Step 3: Log out</li>
181-
* <li>Step 4: Log in with the authenticator</li>
185+
* <li>Step 4: Log in with the authenticator (no allowCredentials)</li>
186+
* <li>Step 5: Log in again with the same authenticator (with allowCredentials)</li>
182187
* </ul>
183188
*/
184189
@Test
@@ -190,7 +195,7 @@ void loginWhenAuthenticatorRegisteredThenSuccess() {
190195
login();
191196

192197
// Step 2: register a credential from the virtual authenticator
193-
this.driver.get(this.baseUrl + "/webauthn/register");
198+
this.getAndWait("/webauthn/register");
194199
this.driver.findElement(passkeyLabel()).sendKeys("Virtual authenticator");
195200
this.driver.findElement(registerPasskeyButton()).click();
196201

@@ -212,9 +217,58 @@ void loginWhenAuthenticatorRegisteredThenSuccess() {
212217
logout();
213218

214219
// Step 4: log in with the virtual authenticator
215-
this.driver.get(this.baseUrl + "/webauthn/register");
220+
this.getAndWait("/webauthn/register", "/login");
216221
this.driver.findElement(signinWithPasskeyButton()).click();
217222
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?continue"));
223+
224+
// Step 5: authenticate while being already logged in
225+
// This simulates some use-cases with MFA. Since the user is already logged in,
226+
// the "allowCredentials" property is populated
227+
this.getAndWait("/login");
228+
this.driver.findElement(signinWithPasskeyButton()).click();
229+
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/"));
230+
}
231+
232+
@Test
233+
void registerWhenAuthenticatorAlreadyRegisteredThenRejects() {
234+
createVirtualAuthenticator(true);
235+
login();
236+
registerAuthenticator("Virtual authenticator");
237+
238+
// Cannot re-register the same authenticator because excludeCredentials
239+
// is not empty and contains the given authenticator
240+
this.driver.findElement(passkeyLabel()).sendKeys("Same authenticator");
241+
this.driver.findElement(registerPasskeyButton()).click();
242+
243+
await(() -> assertHasAlertStartingWith("error", "Registration failed"));
244+
}
245+
246+
@Test
247+
void registerSecondAuthenticatorThenSucceeds() {
248+
createVirtualAuthenticator(true);
249+
login();
250+
251+
registerAuthenticator("Virtual authenticator");
252+
this.getAndWait("/webauthn/register");
253+
List<WebElement> passkeyRows = this.driver.findElements(passkeyTableRows());
254+
assertThat(passkeyRows).hasSize(1)
255+
.first()
256+
.extracting((row) -> row.findElement(firstCell()))
257+
.extracting(WebElement::getText)
258+
.isEqualTo("Virtual authenticator");
259+
260+
// Create second authenticator and register
261+
removeAuthenticator();
262+
createVirtualAuthenticator(true);
263+
registerAuthenticator("Second virtual authenticator");
264+
265+
this.getAndWait("/webauthn/register");
266+
267+
passkeyRows = this.driver.findElements(passkeyTableRows());
268+
assertThat(passkeyRows).hasSize(2)
269+
.extracting((row) -> row.findElement(firstCell()))
270+
.extracting(WebElement::getText)
271+
.contains("Second virtual authenticator");
218272
}
219273

220274
/**
@@ -231,11 +285,14 @@ void loginWhenAuthenticatorRegisteredThenSuccess() {
231285
* "https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/">https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/</a>
232286
*/
233287
private void createVirtualAuthenticator(boolean userIsVerified) {
288+
if (StringUtils.hasText(this.authenticatorId)) {
289+
throw new IllegalStateException("Authenticator already exists, please remove it before re-creating one");
290+
}
234291
HasCdp cdpDriver = (HasCdp) this.driver;
235292
cdpDriver.executeCdpCommand("WebAuthn.enable", Map.of("enableUI", false));
236293
// this.driver.addVirtualAuthenticator(createVirtualAuthenticatorOptions());
237294
//@formatter:off
238-
cdpDriver.executeCdpCommand("WebAuthn.addVirtualAuthenticator",
295+
Map<String, Object> cmdResponse = cdpDriver.executeCdpCommand("WebAuthn.addVirtualAuthenticator",
239296
Map.of(
240297
"options",
241298
Map.of(
@@ -248,21 +305,38 @@ private void createVirtualAuthenticator(boolean userIsVerified) {
248305
)
249306
));
250307
//@formatter:on
308+
this.authenticatorId = cmdResponse.get("authenticatorId").toString();
309+
}
310+
311+
private void removeAuthenticator() {
312+
HasCdp cdpDriver = (HasCdp) this.driver;
313+
cdpDriver.executeCdpCommand("WebAuthn.removeVirtualAuthenticator",
314+
Map.of("authenticatorId", this.authenticatorId));
315+
this.authenticatorId = null;
251316
}
252317

253318
private void login() {
254-
this.driver.get(this.baseUrl);
319+
this.getAndWait("/", "/login");
255320
this.driver.findElement(usernameField()).sendKeys(USERNAME);
256321
this.driver.findElement(passwordField()).sendKeys(PASSWORD);
257322
this.driver.findElement(signinWithUsernamePasswordButton()).click();
323+
// Ensure login has completed
324+
await(() -> assertThat(this.driver.getCurrentUrl()).doesNotContain("/login"));
258325
}
259326

260327
private void logout() {
261-
this.driver.get(this.baseUrl + "/logout");
328+
this.getAndWait("/logout");
262329
this.driver.findElement(logoutButton()).click();
263330
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/login?logout"));
264331
}
265332

333+
private void registerAuthenticator(String passkeyName) {
334+
this.getAndWait("/webauthn/register");
335+
this.driver.findElement(passkeyLabel()).sendKeys(passkeyName);
336+
this.driver.findElement(registerPasskeyButton()).click();
337+
await(() -> assertThat(this.driver.getCurrentUrl()).endsWith("/webauthn/register?success"));
338+
}
339+
266340
private AbstractStringAssert<?> assertHasAlertStartingWith(String alertType, String alertMessage) {
267341
WebElement alert = this.driver.findElement(new By.ById(alertType));
268342
assertThat(alert.isDisplayed())
@@ -289,6 +363,15 @@ private void await(Supplier<AbstractAssert<?, ?>> assertion) {
289363
});
290364
}
291365

366+
private void getAndWait(String endpoint) {
367+
this.getAndWait(endpoint, endpoint);
368+
}
369+
370+
private void getAndWait(String endpoint, String redirectUrl) {
371+
this.driver.get(this.baseUrl + endpoint);
372+
this.await(() -> assertThat(this.driver.getCurrentUrl()).endsWith(redirectUrl));
373+
}
374+
292375
private static By.ById passkeyLabel() {
293376
return new By.ById("label");
294377
}
@@ -325,6 +408,10 @@ private static By.ByCssSelector logoutButton() {
325408
return new By.ByCssSelector("button");
326409
}
327410

411+
private static By.ByCssSelector deletePasskeyButton() {
412+
return new By.ByCssSelector("table > tbody > tr > button");
413+
}
414+
328415
/**
329416
* The configuration for WebAuthN tests. It accesses the Server's current port, so we
330417
* can configurer WebAuthnConfigurer#allowedOrigin

config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ final class MethodSecuritySelector implements ImportSelector {
4242
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
4343

4444
private static final boolean isWebPresent = ClassUtils
45-
.isPresent("org.springframework.web.servlet.DispatcherServlet", null);
45+
.isPresent("org.springframework.web.servlet.DispatcherServlet", null)
46+
&& ClassUtils.isPresent("org.springframework.security.web.util.ThrowableAnalyzer", null);
4647

4748
private static final boolean isObservabilityPresent = ClassUtils
4849
.isPresent("io.micrometer.observation.ObservationRegistry", null);

0 commit comments

Comments
 (0)