From efddb9d0d5e7a52a7f62f4538bdf1bc6150c9f30 Mon Sep 17 00:00:00 2001 From: DingHao Date: Tue, 27 Feb 2024 14:08:42 +0800 Subject: [PATCH] Add DelegatingServerAuthenticationConverter Closes gh-14644 --- ...legatingServerAuthenticationConverter.java | 72 +++++++++ ...ingServerAuthenticationConverterTests.java | 137 ++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverter.java create mode 100644 web/src/test/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverterTests.java diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverter.java new file mode 100644 index 00000000000..ef8f88b0365 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2024 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 java.util.List; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ServerAuthenticationConverter} that delegates to other + * {@link ServerAuthenticationConverter} instances. + * + * @author DingHao + * @since 6.3 + */ +public final class DelegatingServerAuthenticationConverter implements ServerAuthenticationConverter { + + private final List delegates; + + private boolean continueOnError = false; + + private final Log logger = LogFactory.getLog(getClass()); + + public DelegatingServerAuthenticationConverter(ServerAuthenticationConverter... converters) { + this(List.of(converters)); + } + + public DelegatingServerAuthenticationConverter(List converters) { + Assert.notEmpty(converters, "converters cannot be null"); + this.delegates = converters; + } + + @Override + public Mono convert(ServerWebExchange exchange) { + Flux result = Flux.fromIterable(this.delegates); + Function> logging = ( + converter) -> converter.convert(exchange).doOnError(this.logger::debug); + return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next(); + } + + /** + * Continue iterating when a delegate errors, defaults to {@code true} + * @param continueOnError whether to continue when a delegate errors + * @since 6.3 + */ + public void setContinueOnError(boolean continueOnError) { + this.continueOnError = continueOnError; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverterTests.java new file mode 100644 index 00000000000..6840106870c --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/DelegatingServerAuthenticationConverterTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2024 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.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author DingHao + * @since 6.3 + */ +@ExtendWith(MockitoExtension.class) +public class DelegatingServerAuthenticationConverterTests { + + DelegatingServerAuthenticationConverter converter = new DelegatingServerAuthenticationConverter( + new ApkServerAuthenticationConverter(), new ServerHttpBasicAuthenticationConverter()); + + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest.get("/"); + + @Test + public void applyServerHttpBasicAuthenticationConverter() { + Mono result = this.converter.convert(MockServerWebExchange + .from(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==").build())); + UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class) + .block(); + assertThat(authentication.getPrincipal()).isEqualTo("user"); + assertThat(authentication.getCredentials()).isEqualTo("password"); + } + + @Test + public void applyApkServerAuthenticationConverter() { + String apk = "123e4567e89b12d3a456426655440000"; + Mono result = this.converter + .convert(MockServerWebExchange.from(this.request.header("APK", apk).build())); + ApkTokenAuthenticationToken authentication = result.cast(ApkTokenAuthenticationToken.class).block(); + assertThat(authentication.getApk()).isEqualTo(apk); + } + + @Test + public void applyServerHttpBasicAuthenticationConverterWhenFirstAuthenticationConverterException() { + this.converter.setContinueOnError(true); + Mono result = this.converter.convert(MockServerWebExchange.from(this.request.header("APK", "") + .header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==") + .build())); + UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class) + .block(); + assertThat(authentication.getPrincipal()).isEqualTo("user"); + assertThat(authentication.getCredentials()).isEqualTo("password"); + } + + @Test + public void applyApkServerAuthenticationConverterThenDelegate2NotInvokedAndError() { + Mono result = this.converter.convert(MockServerWebExchange.from(this.request.header("APK", "") + .header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==") + .build())); + StepVerifier.create(result.cast(ApkTokenAuthenticationToken.class)) + .expectError(ApkAuthenticationException.class) + .verify(); + } + + public static class ApkServerAuthenticationConverter implements ServerAuthenticationConverter { + + @Override + public Mono convert(ServerWebExchange exchange) { + return Mono.fromCallable(() -> exchange.getRequest().getHeaders().getFirst("APK")).map((apk) -> { + if (apk.isEmpty()) { + throw new ApkAuthenticationException("apk invalid"); + } + return new ApkTokenAuthenticationToken(apk); + }); + } + + } + + public static class ApkTokenAuthenticationToken extends AbstractAuthenticationToken { + + private final String apk; + + public ApkTokenAuthenticationToken(String apk) { + super(AuthorityUtils.NO_AUTHORITIES); + this.apk = apk; + } + + public String getApk() { + return this.apk; + } + + @Override + public Object getCredentials() { + return this.getApk(); + } + + @Override + public Object getPrincipal() { + return this.getApk(); + } + + } + + public static class ApkAuthenticationException extends AuthenticationException { + + public ApkAuthenticationException(String msg) { + super(msg); + } + + } + +}