diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
index f2f5d2bf04..bc6b5c7feb 100644
--- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
+++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
@@ -56,6 +56,7 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager;
@@ -91,6 +92,8 @@ import org.springframework.security.oauth2.server.resource.web.access.server.Bea
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.PortMapper;
+import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
@@ -99,6 +102,7 @@ import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
+import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
@@ -108,6 +112,7 @@ import org.springframework.security.web.server.authentication.ServerAuthenticati
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerHttpBasicAuthenticationConverter;
+import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
@@ -241,6 +246,8 @@ public class ServerHttpSecurity {
private HttpBasicSpec httpBasic;
+ private X509Spec x509;
+
private final RequestCacheSpec requestCache = new RequestCacheSpec();
private FormLoginSpec formLogin;
@@ -578,6 +585,93 @@ public class ServerHttpSecurity {
return this.formLogin;
}
+ /**
+ * Configures x509 authentication using a certificate provided by a client.
+ *
+ *
+ * @Bean
+ * public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+ * http
+ * .x509()
+ * .authenticationManager(authenticationManager)
+ * .principalExtractor(principalExtractor);
+ * return http.build();
+ * }
+ *
+ *
+ * Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor} will be used.
+ * If authenticationManager is not specified, {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
+ *
+ * @return the {@link X509Spec} to customize
+ * @author Alexey Nesterov
+ * @since 5.2
+ */
+ public X509Spec x509() {
+ if (this.x509 == null) {
+ this.x509 = new X509Spec();
+ }
+
+ return this.x509;
+ }
+
+ /**
+ * Configures X509 authentication
+ *
+ * @author Alexey Nesterov
+ * @since 5.2
+ * @see #x509()
+ */
+ public class X509Spec {
+
+ private X509PrincipalExtractor principalExtractor;
+ private ReactiveAuthenticationManager authenticationManager;
+
+ public X509Spec principalExtractor(X509PrincipalExtractor principalExtractor) {
+ this.principalExtractor = principalExtractor;
+ return this;
+ }
+
+ public X509Spec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
+ this.authenticationManager = authenticationManager;
+ return this;
+ }
+
+ public ServerHttpSecurity and() {
+ return ServerHttpSecurity.this;
+ }
+
+ protected void configure(ServerHttpSecurity http) {
+ ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
+ X509PrincipalExtractor principalExtractor = getPrincipalExtractor();
+
+ AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager);
+ filter.setServerAuthenticationConverter(new ServerX509AuthenticationConverter(principalExtractor));
+ http.addFilterAt(filter, SecurityWebFiltersOrder.AUTHENTICATION);
+ }
+
+ private X509PrincipalExtractor getPrincipalExtractor() {
+ if (this.principalExtractor != null) {
+ return this.principalExtractor;
+ }
+
+ return new SubjectDnX509PrincipalExtractor();
+ }
+
+ private ReactiveAuthenticationManager getAuthenticationManager() {
+ if (this.authenticationManager != null) {
+ return this.authenticationManager;
+ }
+
+ ReactiveUserDetailsService userDetailsService = getBean(ReactiveUserDetailsService.class);
+ ReactivePreAuthenticatedAuthenticationManager authenticationManager = new ReactivePreAuthenticatedAuthenticationManager(userDetailsService);
+
+ return authenticationManager;
+ }
+
+ private X509Spec() {
+ }
+ }
+
public OAuth2LoginSpec oauth2Login() {
if (this.oauth2Login == null) {
this.oauth2Login = new OAuth2LoginSpec();
@@ -1508,6 +1602,9 @@ public class ServerHttpSecurity {
if (this.httpsRedirectSpec != null) {
this.httpsRedirectSpec.configure(this);
}
+ if (this.x509 != null) {
+ this.x509.configure(this);
+ }
if (this.csrf != null) {
this.csrf.configure(this);
}
diff --git a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java
index fc22c8c471..9e45b0c92f 100644
--- a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java
+++ b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java
@@ -19,6 +19,7 @@ package org.springframework.security.config.web.server;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Arrays;
@@ -34,6 +35,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
import reactor.core.publisher.Mono;
import reactor.test.publisher.TestPublisher;
@@ -279,6 +282,43 @@ public class ServerHttpSecurityTests {
assertThat(result.getResponseCookies().getFirst("SESSION")).isNull();
}
+ @Test
+ @SuppressWarnings("unchecked")
+ public void addsX509FilterWhenX509AuthenticationIsConfigured() {
+ X509PrincipalExtractor mockExtractor = mock(X509PrincipalExtractor.class);
+ ReactiveAuthenticationManager mockAuthenticationManager = mock(ReactiveAuthenticationManager.class);
+
+ this.http.x509()
+ .principalExtractor(mockExtractor)
+ .authenticationManager(mockAuthenticationManager)
+ .and();
+
+ SecurityWebFilterChain securityWebFilterChain = this.http.build();
+ WebFilter x509WebFilter = securityWebFilterChain.getWebFilters().filter(this::isX509Filter).blockFirst();
+
+ assertThat(x509WebFilter).isNotNull();
+ }
+
+ @Test
+ public void addsX509FilterWhenX509AuthenticationIsConfiguredWithDefaults() {
+ this.http.x509();
+
+ SecurityWebFilterChain securityWebFilterChain = this.http.build();
+ WebFilter x509WebFilter = securityWebFilterChain.getWebFilters().filter(this::isX509Filter).blockFirst();
+
+ assertThat(x509WebFilter).isNotNull();
+ }
+
+ private boolean isX509Filter(WebFilter filter) {
+ try {
+ Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter");
+ return converter.getClass().isAssignableFrom(ServerX509AuthenticationConverter.class);
+ } catch (IllegalArgumentException e) {
+ // field doesn't exist
+ return false;
+ }
+ }
+
private Optional getWebFilter(SecurityWebFilterChain filterChain, Class filterClass) {
return (Optional) filterChain.getWebFilters()
.filter(Objects::nonNull)
diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ReactivePreAuthenticatedAuthenticationManager.java b/web/src/main/java/org/springframework/security/web/server/authentication/ReactivePreAuthenticatedAuthenticationManager.java
new file mode 100644
index 0000000000..5081f3f1d5
--- /dev/null
+++ b/web/src/main/java/org/springframework/security/web/server/authentication/ReactivePreAuthenticatedAuthenticationManager.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.web.server.authentication;
+
+import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.UserDetailsChecker;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+import reactor.core.publisher.Mono;
+
+/**
+ * Reactive version of {@link org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider}
+ *
+ * This manager receives a {@link PreAuthenticatedAuthenticationToken}, checks that associated account is not disabled,
+ * expired, or blocked, and returns new authenticated {@link PreAuthenticatedAuthenticationToken}.
+ *
+ * If no {@link UserDetailsChecker} is provided, a default {@link AccountStatusUserDetailsChecker} will be
+ * created.
+ *
+ * @author Alexey Nesterov
+ * @since 5.2
+ */
+public class ReactivePreAuthenticatedAuthenticationManager
+ implements ReactiveAuthenticationManager {
+
+ private final ReactiveUserDetailsService userDetailsService;
+ private final UserDetailsChecker userDetailsChecker;
+
+ public ReactivePreAuthenticatedAuthenticationManager(ReactiveUserDetailsService userDetailsService) {
+ this(userDetailsService, new AccountStatusUserDetailsChecker());
+ }
+
+ public ReactivePreAuthenticatedAuthenticationManager(
+ ReactiveUserDetailsService userDetailsService,
+ UserDetailsChecker userDetailsChecker) {
+ this.userDetailsService = userDetailsService;
+ this.userDetailsChecker = userDetailsChecker;
+ }
+
+ @Override
+ public Mono authenticate(Authentication authentication) {
+ return Mono.just(authentication)
+ .filter(this::supports)
+ .map(Authentication::getName)
+ .flatMap(userDetailsService::findByUsername)
+ .switchIfEmpty(Mono.error(() -> new UsernameNotFoundException("User not found")))
+ .doOnNext(userDetailsChecker::check)
+ .map(ud -> {
+ PreAuthenticatedAuthenticationToken result = new PreAuthenticatedAuthenticationToken(
+ ud, authentication.getCredentials(), ud.getAuthorities());
+ result.setDetails(authentication.getDetails());
+
+ return result;
+ });
+ }
+
+ private boolean supports(Authentication authentication) {
+ return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication.getClass());
+ }
+}
diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ServerX509AuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/authentication/ServerX509AuthenticationConverter.java
new file mode 100644
index 0000000000..9757150c1d
--- /dev/null
+++ b/web/src/main/java/org/springframework/security/web/server/authentication/ServerX509AuthenticationConverter.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.http.server.reactive.SslInfo;
+import org.springframework.lang.NonNull;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Converts from a {@link SslInfo} provided by a request to an {@link PreAuthenticatedAuthenticationToken} that can be authenticated.
+ *
+ * @author Alexey Nesterov
+ * @since 5.2
+ */
+public class ServerX509AuthenticationConverter implements ServerAuthenticationConverter {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+ private final X509PrincipalExtractor principalExtractor;
+
+ public ServerX509AuthenticationConverter(@NonNull X509PrincipalExtractor principalExtractor) {
+ this.principalExtractor = principalExtractor;
+ }
+
+ @Override
+ public Mono convert(ServerWebExchange exchange) {
+ SslInfo sslInfo = exchange.getRequest().getSslInfo();
+ if (sslInfo == null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("No SslInfo provided with a request, skipping x509 authentication");
+ }
+
+ return Mono.empty();
+ }
+
+ if (sslInfo.getPeerCertificates() == null || sslInfo.getPeerCertificates().length == 0) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("No peer certificates found in SslInfo, skipping x509 authentication");
+ }
+
+ return Mono.empty();
+ }
+
+ X509Certificate clientCertificate = sslInfo.getPeerCertificates()[0];
+ Object principal = this.principalExtractor.extractPrincipal(clientCertificate);
+
+ PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(
+ principal, clientCertificate);
+
+ return Mono.just(authRequest);
+ }
+}
diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/ReactivePreAuthenticatedAuthenticationManagerTest.java b/web/src/test/java/org/springframework/security/web/server/authentication/ReactivePreAuthenticatedAuthenticationManagerTest.java
new file mode 100644
index 0000000000..16925d7b9e
--- /dev/null
+++ b/web/src/test/java/org/springframework/security/web/server/authentication/ReactivePreAuthenticatedAuthenticationManagerTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication;
+
+import org.junit.Test;
+import org.springframework.security.authentication.AccountExpiredException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Alexey Nesterov
+ * @since 5.2
+ */
+public class ReactivePreAuthenticatedAuthenticationManagerTest {
+
+ private ReactiveUserDetailsService mockUserDetailsService
+ = mock(ReactiveUserDetailsService.class);
+
+ private ReactivePreAuthenticatedAuthenticationManager manager
+ = new ReactivePreAuthenticatedAuthenticationManager(mockUserDetailsService);
+
+ private final User validAccount = new User("valid", "", Collections.emptySet());
+ private final User nonExistingAccount = new User("non existing", "", Collections.emptySet());
+ private final User disabledAccount = new User("disabled", "", false, true, true, true, Collections.emptySet());
+ private final User expiredAccount = new User("expired", "", true, false, true, true, Collections.emptySet());
+ private final User accountWithExpiredCredentials = new User("credentials expired", "", true, true, false, true, Collections.emptySet());
+ private final User lockedAccount = new User("locked", "", true, true, true, false, Collections.emptySet());
+
+ @Test
+ public void returnsAuthenticatedTokenForValidAccount() {
+ when(mockUserDetailsService.findByUsername(anyString())).thenReturn(Mono.just(validAccount));
+
+ Authentication authentication = manager.authenticate(tokenForUser(validAccount.getUsername())).block();
+ assertThat(authentication.isAuthenticated()).isEqualTo(true);
+ }
+
+ @Test(expected = UsernameNotFoundException.class)
+ public void returnsNullForNonExistingAccount() {
+ when(mockUserDetailsService.findByUsername(anyString())).thenReturn(Mono.empty());
+
+ manager.authenticate(tokenForUser(nonExistingAccount.getUsername())).block();
+ }
+
+ @Test(expected = LockedException.class)
+ public void throwsExceptionForLockedAccount() {
+ when(mockUserDetailsService.findByUsername(anyString())).thenReturn(Mono.just(lockedAccount));
+
+ manager.authenticate(tokenForUser(lockedAccount.getUsername())).block();
+ }
+
+ @Test(expected = DisabledException.class)
+ public void throwsExceptionForDisabledAccount() {
+ when(mockUserDetailsService.findByUsername(anyString())).thenReturn(Mono.just(disabledAccount));
+
+ manager.authenticate(tokenForUser(disabledAccount.getUsername())).block();
+ }
+
+ @Test(expected = AccountExpiredException.class)
+ public void throwsExceptionForExpiredAccount() {
+ when(mockUserDetailsService.findByUsername(anyString())).thenReturn(Mono.just(expiredAccount));
+
+ manager.authenticate(tokenForUser(expiredAccount.getUsername())).block();
+ }
+
+
+ @Test(expected = CredentialsExpiredException.class)
+ public void throwsExceptionForAccountWithExpiredCredentials() {
+ when(mockUserDetailsService.findByUsername(anyString())).thenReturn(Mono.just(accountWithExpiredCredentials));
+
+ manager.authenticate(tokenForUser(accountWithExpiredCredentials.getUsername())).block();
+ }
+
+ private Authentication tokenForUser(String username) {
+ return new PreAuthenticatedAuthenticationToken(username, null);
+ }
+}
diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/ServerX509AuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/ServerX509AuthenticationConverterTests.java
new file mode 100644
index 0000000000..33b24df8d8
--- /dev/null
+++ b/web/src/test/java/org/springframework/security/web/server/authentication/ServerX509AuthenticationConverterTests.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.http.server.reactive.SslInfo;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
+
+import java.security.cert.X509Certificate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ServerX509AuthenticationConverterTests {
+
+ @Mock
+ private X509PrincipalExtractor principalExtractor;
+
+ @InjectMocks
+ private ServerX509AuthenticationConverter converter;
+
+ private X509Certificate certificate;
+
+ private MockServerHttpRequest.BaseBuilder> request;
+
+ @Before
+ public void setUp() throws Exception {
+ request = MockServerHttpRequest.get("/");
+
+ certificate = X509TestUtils.buildTestCertificate();
+ when(principalExtractor.extractPrincipal(any())).thenReturn("Luke Taylor");
+ }
+
+ @Test
+ public void shouldReturnNullForInvalidCertificate() {
+ Authentication authentication = converter.convert(MockServerWebExchange.from(request.build())).block();
+
+ assertThat(authentication).isNull();
+ }
+
+ @Test
+ public void shouldReturnAuthenticationForValidCertificate() {
+ request.sslInfo(new MockSslInfo(certificate));
+
+ Authentication authentication = converter.convert(MockServerWebExchange.from(request.build())).block();
+
+ assertThat(authentication.getName()).isEqualTo("Luke Taylor");
+ assertThat(authentication.getCredentials()).isEqualTo(certificate);
+ }
+
+ class MockSslInfo implements SslInfo {
+
+ private final X509Certificate[] peerCertificates;
+
+ MockSslInfo(X509Certificate... peerCertificates) {
+ this.peerCertificates = peerCertificates;
+ }
+
+ @Override
+ public String getSessionId() {
+ return "mock-session-id";
+ }
+
+ @Override
+ public X509Certificate[] getPeerCertificates() {
+ return this.peerCertificates;
+ }
+ }
+}