Skip to content

Commit b0e8730

Browse files
committed
Add Passkeys Support
Closes gh-13305
1 parent f280aa3 commit b0e8730

File tree

157 files changed

+19203
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

157 files changed

+19203
-5
lines changed

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ develocity {
106106
}
107107

108108
nohttp {
109-
source.exclude "buildSrc/build/**"
109+
source.exclude "buildSrc/build/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**"
110110
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
111111
}
112112

config/spring-security-config.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies {
4343
optional 'org.jetbrains.kotlin:kotlin-reflect'
4444
optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
4545
optional 'jakarta.annotation:jakarta.annotation-api'
46+
optional libs.webauthn4j.core
4647

4748
provided 'jakarta.servlet:jakarta.servlet-api'
4849

config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

+26
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer;
6868
import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer;
6969
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
70+
import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer;
7071
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
7172
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
7273
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
@@ -3674,6 +3675,31 @@ public HttpSecurity securityMatcher(String... patterns) {
36743675
return this;
36753676
}
36763677

3678+
/**
3679+
* Specifies webAuthn/passkeys based authentication.
3680+
*
3681+
* <pre>
3682+
* &#064;Bean
3683+
* SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
3684+
* http
3685+
* // ...
3686+
* .webAuthn((webAuthn) -&gt; webAuthn
3687+
* .rpName("Spring Security Relying Party")
3688+
* .rpId("example.com")
3689+
* .allowedOrigins("https://example.com")
3690+
* );
3691+
* return http.build();
3692+
* }
3693+
* </pre>
3694+
* @param webAuthn the customizer to apply
3695+
* @return the {@link HttpSecurity} for further customizations
3696+
* @throws Exception
3697+
*/
3698+
public HttpSecurity webAuthn(Customizer<WebAuthnConfigurer<HttpSecurity>> webAuthn) throws Exception {
3699+
webAuthn.customize(getOrApply(new WebAuthnConfigurer<HttpSecurity>()));
3700+
return HttpSecurity.this;
3701+
}
3702+
36773703
private List<RequestMatcher> createAntMatchers(String... patterns) {
36783704
List<RequestMatcher> matchers = new ArrayList<>(patterns.length);
36793705
for (String pattern : patterns) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web.configurers;
18+
19+
import java.lang.reflect.Constructor;
20+
import java.util.HashSet;
21+
import java.util.Map;
22+
import java.util.Optional;
23+
import java.util.Set;
24+
25+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
26+
import org.springframework.context.ApplicationContext;
27+
import org.springframework.core.io.ClassPathResource;
28+
import org.springframework.http.HttpMethod;
29+
import org.springframework.http.MediaType;
30+
import org.springframework.security.authentication.ProviderManager;
31+
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
32+
import org.springframework.security.core.userdetails.UserDetailsService;
33+
import org.springframework.security.web.access.intercept.AuthorizationFilter;
34+
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
35+
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
36+
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
37+
import org.springframework.security.web.csrf.CsrfToken;
38+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
39+
import org.springframework.security.web.util.matcher.RequestMatcher;
40+
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
41+
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
42+
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
43+
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
44+
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
45+
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
46+
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
47+
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
48+
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
49+
import org.springframework.security.web.webauthn.management.Webauthn4JRelyingPartyOperations;
50+
import org.springframework.security.web.webauthn.registration.DefaultWebAuthnRegistrationPageGeneratingFilter;
51+
import org.springframework.security.web.webauthn.registration.PublicKeyCredentialCreationOptionsFilter;
52+
import org.springframework.security.web.webauthn.registration.WebAuthnRegistrationFilter;
53+
54+
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
55+
56+
/**
57+
* Configures WebAuthn for Spring Security applications
58+
*
59+
* @param <H> the type of builder
60+
* @author Rob Winch
61+
* @since 6.4
62+
*/
63+
public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
64+
extends AbstractHttpConfigurer<WebAuthnConfigurer<H>, H> {
65+
66+
private String rpId;
67+
68+
private String rpName;
69+
70+
private Set<String> allowedOrigins = new HashSet<>();
71+
72+
/**
73+
* The Relying Party id.
74+
* @param rpId the relying party id
75+
* @return the {@link WebAuthnConfigurer} for further customization
76+
*/
77+
public WebAuthnConfigurer<H> rpId(String rpId) {
78+
this.rpId = rpId;
79+
return this;
80+
}
81+
82+
/**
83+
* Sets the relying party name
84+
* @param rpName the relying party name
85+
* @return the {@link WebAuthnConfigurer} for further customization
86+
*/
87+
public WebAuthnConfigurer<H> rpName(String rpName) {
88+
this.rpName = rpName;
89+
return this;
90+
}
91+
92+
/**
93+
* Convenience method for {@link #allowedOrigins(Set)}
94+
* @param allowedOrigins the allowed origins
95+
* @return the {@link WebAuthnConfigurer} for further customization
96+
* @see #allowedOrigins(Set)
97+
*/
98+
public WebAuthnConfigurer<H> allowedOrigins(String... allowedOrigins) {
99+
return allowedOrigins(Set.of(allowedOrigins));
100+
}
101+
102+
/**
103+
* Sets the allowed origins.
104+
* @param allowedOrigins the allowed origins
105+
* @return the {@link WebAuthnConfigurer} for further customization
106+
* @see #allowedOrigins(String...)
107+
*/
108+
public WebAuthnConfigurer<H> allowedOrigins(Set<String> allowedOrigins) {
109+
this.allowedOrigins = allowedOrigins;
110+
return this;
111+
}
112+
113+
@Override
114+
public void configure(H http) throws Exception {
115+
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
116+
throw new IllegalStateException("Missing UserDetailsService Bean");
117+
});
118+
PublicKeyCredentialUserEntityRepository userEntities = getSharedOrBean(http,
119+
PublicKeyCredentialUserEntityRepository.class)
120+
.orElse(userEntityRepository());
121+
UserCredentialRepository userCredentials = getSharedOrBean(http, UserCredentialRepository.class)
122+
.orElse(userCredentialRepository());
123+
WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials);
124+
WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
125+
webAuthnAuthnFilter.setAuthenticationManager(
126+
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
127+
http.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class);
128+
http.addFilterAfter(new WebAuthnRegistrationFilter(userCredentials, rpOperations), AuthorizationFilter.class);
129+
http.addFilterBefore(new PublicKeyCredentialCreationOptionsFilter(rpOperations), AuthorizationFilter.class);
130+
http.addFilterAfter(new DefaultWebAuthnRegistrationPageGeneratingFilter(userEntities, userCredentials),
131+
AuthorizationFilter.class);
132+
http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class);
133+
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
134+
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
135+
if (loginPageGeneratingFilter != null) {
136+
ClassPathResource webauthn = new ClassPathResource(
137+
"org/springframework/security/spring-security-webauthn.js");
138+
AntPathRequestMatcher matcher = antMatcher(HttpMethod.GET, "/login/webauthn.js");
139+
140+
Constructor<DefaultResourcesFilter> constructor = DefaultResourcesFilter.class
141+
.getDeclaredConstructor(RequestMatcher.class, ClassPathResource.class, MediaType.class);
142+
constructor.setAccessible(true);
143+
DefaultResourcesFilter resourcesFilter = constructor.newInstance(matcher, webauthn,
144+
MediaType.parseMediaType("text/javascript"));
145+
http.addFilter(resourcesFilter);
146+
DefaultLoginPageGeneratingFilter loginGeneratingFilter = http
147+
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
148+
loginGeneratingFilter.setPasskeysEnabled(true);
149+
loginGeneratingFilter.setResolveHeaders((request) -> {
150+
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
151+
return Map.of(csrfToken.getHeaderName(), csrfToken.getToken());
152+
});
153+
}
154+
}
155+
156+
private <C> Optional<C> getSharedOrBean(H http, Class<C> type) {
157+
C shared = http.getSharedObject(type);
158+
return Optional.ofNullable(shared).or(() -> getBeanOrNull(type));
159+
}
160+
161+
private <T> Optional<T> getBeanOrNull(Class<T> type) {
162+
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
163+
if (context == null) {
164+
return Optional.empty();
165+
}
166+
try {
167+
return Optional.of(context.getBean(type));
168+
}
169+
catch (NoSuchBeanDefinitionException ex) {
170+
return Optional.empty();
171+
}
172+
}
173+
174+
private MapUserCredentialRepository userCredentialRepository() {
175+
return new MapUserCredentialRepository();
176+
}
177+
178+
private PublicKeyCredentialUserEntityRepository userEntityRepository() {
179+
return new MapPublicKeyCredentialUserEntityRepository();
180+
}
181+
182+
private WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations(
183+
PublicKeyCredentialUserEntityRepository userEntities, UserCredentialRepository userCredentials) {
184+
Optional<WebAuthnRelyingPartyOperations> webauthnOperationsBean = getBeanOrNull(
185+
WebAuthnRelyingPartyOperations.class);
186+
if (webauthnOperationsBean.isPresent()) {
187+
return webauthnOperationsBean.get();
188+
}
189+
Webauthn4JRelyingPartyOperations result = new Webauthn4JRelyingPartyOperations(userEntities, userCredentials,
190+
PublicKeyCredentialRpEntity.builder().id(this.rpId).name(this.rpName).build(), this.allowedOrigins);
191+
return result;
192+
}
193+
194+
}

config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

+31
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,37 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
10311031
this.http.rememberMe(rememberMeCustomizer)
10321032
}
10331033

1034+
/**
1035+
* Enable WebAuthn configuration.
1036+
*
1037+
* Example:
1038+
*
1039+
* ```
1040+
* @Configuration
1041+
* @EnableWebSecurity
1042+
* class SecurityConfig {
1043+
*
1044+
* @Bean
1045+
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
1046+
* http {
1047+
* webAuthn {
1048+
* loginPage = "/log-in"
1049+
* }
1050+
* }
1051+
* return http.build()
1052+
* }
1053+
* }
1054+
* ```
1055+
*
1056+
* @param webAuthnConfiguration custom configurations to be applied
1057+
* to the WebAuthn authentication
1058+
* @see [WebAuthnDsl]
1059+
*/
1060+
fun webAuthn(webAuthnConfiguration: WebAuthnDsl.() -> Unit) {
1061+
val webAuthnCustomizer = WebAuthnDsl().apply(webAuthnConfiguration).get()
1062+
this.http.webAuthn(webAuthnCustomizer)
1063+
}
1064+
10341065
/**
10351066
* Adds the [Filter] at the location of the specified [Filter] class.
10361067
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web
18+
19+
import org.springframework.security.config.annotation.web.builders.HttpSecurity
20+
import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer
21+
22+
/**
23+
* A Kotlin DSL to configure [HttpSecurity] webauthn using idiomatic Kotlin code.
24+
* @property rpName the relying party name
25+
* @property rpId the relying party id
26+
* @property the allowed origins
27+
* @since 6.4
28+
* @author Rob Winch
29+
*/
30+
@SecurityMarker
31+
class WebAuthnDsl {
32+
var rpName: String? = null
33+
var rpId: String? = null
34+
var allowedOrigins: Set<String>? = null
35+
36+
internal fun get(): (WebAuthnConfigurer<HttpSecurity>) -> Unit {
37+
return { webAuthn -> webAuthn
38+
.rpId(rpId)
39+
.rpName(rpName)
40+
.allowedOrigins(allowedOrigins);
41+
}
42+
}
43+
}

config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class X509Dsl {
5353
var authenticationUserDetailsService: AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>? = null
5454
var subjectPrincipalRegex: String? = null
5555

56+
5657
internal fun get(): (X509Configurer<HttpSecurity>) -> Unit {
5758
return { x509 ->
5859
x509AuthenticationFilter?.also { x509.x509AuthenticationFilter(x509AuthenticationFilter) }

0 commit comments

Comments
 (0)