Browse Source

Add RSocket Authentication Extension Support

Fixes gh-7935
Rob Winch 5 năm trước cách đây
mục cha
commit
1d7208f8ef

+ 70 - 6
config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java

@@ -30,6 +30,7 @@ import org.springframework.security.config.Customizer;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
 import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
 import org.springframework.security.rsocket.api.PayloadInterceptor;
+import org.springframework.security.rsocket.authentication.AuthenticationPayloadExchangeConverter;
 import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;
 import org.springframework.security.rsocket.authentication.AnonymousPayloadInterceptor;
 import org.springframework.security.rsocket.authentication.AuthenticationPayloadInterceptor;
@@ -44,6 +45,7 @@ import org.springframework.security.rsocket.util.matcher.RoutePayloadExchangeMat
 import reactor.core.publisher.Mono;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -116,6 +118,8 @@ public class RSocketSecurity {
 
 	private BasicAuthenticationSpec basicAuthSpec;
 
+	private SimpleAuthenticationSpec simpleAuthSpec;
+
 	private JwtSpec jwtSpec;
 
 	private AuthorizePayloadsSpec authorizePayload;
@@ -145,6 +149,58 @@ public class RSocketSecurity {
 		return this;
 	}
 
+	/**
+	 * Adds support for validating a username and password using
+	 * <a href="https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md">Simple Authentication</a>
+	 * @param simple a customizer
+	 * @return RSocketSecurity for additional configuration
+	 * @since 5.3
+	 */
+	public RSocketSecurity simpleAuthentication(Customizer<SimpleAuthenticationSpec> simple) {
+		if (this.simpleAuthSpec == null) {
+			this.simpleAuthSpec = new SimpleAuthenticationSpec();
+		}
+		simple.customize(this.simpleAuthSpec);
+		return this;
+	}
+
+	/**
+	 * @since 5.3
+	 */
+	public class SimpleAuthenticationSpec {
+		private ReactiveAuthenticationManager authenticationManager;
+
+		public SimpleAuthenticationSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
+			this.authenticationManager = authenticationManager;
+			return this;
+		}
+
+		private ReactiveAuthenticationManager getAuthenticationManager() {
+			if (this.authenticationManager == null) {
+				return RSocketSecurity.this.authenticationManager;
+			}
+			return this.authenticationManager;
+		}
+
+		protected AuthenticationPayloadInterceptor build() {
+			ReactiveAuthenticationManager manager = getAuthenticationManager();
+			AuthenticationPayloadInterceptor result = new AuthenticationPayloadInterceptor(manager);
+			result.setAuthenticationConverter(new AuthenticationPayloadExchangeConverter());
+			result.setOrder(PayloadInterceptorOrder.AUTHENTICATION.getOrder());
+			return result;
+		}
+
+		private SimpleAuthenticationSpec() {}
+	}
+
+	/**
+	 * Adds authentication with BasicAuthenticationPayloadExchangeConverter.
+	 *
+	 * @param basic
+	 * @return
+	 * @deprecated Use {@link #simpleAuthentication(Customizer)}
+	 */
+	@Deprecated
 	public RSocketSecurity basicAuthentication(Customizer<BasicAuthenticationSpec> basic) {
 		if (this.basicAuthSpec == null) {
 			this.basicAuthSpec = new BasicAuthenticationSpec();
@@ -206,12 +262,17 @@ public class RSocketSecurity {
 			return RSocketSecurity.this.authenticationManager;
 		}
 
-		protected AuthenticationPayloadInterceptor build() {
+		protected List<AuthenticationPayloadInterceptor> build() {
 			ReactiveAuthenticationManager manager = getAuthenticationManager();
-			AuthenticationPayloadInterceptor result = new AuthenticationPayloadInterceptor(manager);
-			result.setAuthenticationConverter(new BearerPayloadExchangeConverter());
-			result.setOrder(PayloadInterceptorOrder.AUTHENTICATION.getOrder());
-			return result;
+			AuthenticationPayloadInterceptor legacy = new AuthenticationPayloadInterceptor(manager);
+			legacy.setAuthenticationConverter(new BearerPayloadExchangeConverter());
+			legacy.setOrder(PayloadInterceptorOrder.AUTHENTICATION.getOrder());
+
+			AuthenticationPayloadInterceptor standard = new AuthenticationPayloadInterceptor(manager);
+			standard.setAuthenticationConverter(new AuthenticationPayloadExchangeConverter());
+			standard.setOrder(PayloadInterceptorOrder.AUTHENTICATION.getOrder());
+
+			return Arrays.asList(standard, legacy);
 		}
 
 		private JwtSpec() {}
@@ -240,8 +301,11 @@ public class RSocketSecurity {
 		if (this.basicAuthSpec != null) {
 			result.add(this.basicAuthSpec.build());
 		}
+		if (this.simpleAuthSpec != null) {
+			result.add(this.simpleAuthSpec.build());
+		}
 		if (this.jwtSpec != null) {
-			result.add(this.jwtSpec.build());
+			result.addAll(this.jwtSpec.build());
 		}
 		result.add(anonymous());
 

+ 1 - 0
config/src/main/java/org/springframework/security/config/annotation/rsocket/SecuritySocketAcceptorInterceptorConfiguration.java

@@ -47,6 +47,7 @@ class SecuritySocketAcceptorInterceptorConfiguration {
 		}
 		rsocket
 			.basicAuthentication(Customizer.withDefaults())
+			.simpleAuthentication(Customizer.withDefaults())
 			.authorizePayload(authz ->
 				authz
 					.setup().authenticated()

+ 32 - 10
config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java

@@ -15,10 +15,6 @@
  */
 package org.springframework.security.config.annotation.rsocket;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
 import io.rsocket.RSocketFactory;
 import io.rsocket.frame.decoder.PayloadDecoder;
 import io.rsocket.transport.netty.server.CloseableChannel;
@@ -27,8 +23,6 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import reactor.core.publisher.Mono;
-
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -43,12 +37,20 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
 import org.springframework.security.oauth2.jwt.TestJwts;
 import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;
 import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor;
-import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder;
+import org.springframework.security.rsocket.metadata.BearerTokenAuthenticationEncoder;
 import org.springframework.security.rsocket.metadata.BearerTokenMetadata;
 import org.springframework.stereotype.Controller;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils;
+import reactor.core.publisher.Mono;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static io.rsocket.metadata.WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
@@ -95,7 +97,7 @@ public class JwtITests {
 	}
 
 	@Test
-	public void routeWhenAuthorized() {
+	public void routeWhenBearerThenAuthorized() {
 		BearerTokenMetadata credentials =
 				new BearerTokenMetadata("token");
 		when(this.decoder.decode(any())).thenReturn(Mono.just(jwt()));
@@ -112,6 +114,26 @@ public class JwtITests {
 		assertThat(hiRob).isEqualTo("Hi rob");
 	}
 
+	@Test
+	public void routeWhenAuthenticationBearerThenAuthorized() {
+		MimeType authenticationMimeType = MimeTypeUtils.parseMimeType(MESSAGE_RSOCKET_AUTHENTICATION.getString());
+
+		BearerTokenMetadata credentials =
+				new BearerTokenMetadata("token");
+		when(this.decoder.decode(any())).thenReturn(Mono.just(jwt()));
+		this.requester = requester()
+				.setupMetadata(credentials, authenticationMimeType)
+				.connectTcp(this.server.address().getHostName(), this.server.address().getPort())
+				.block();
+
+		String hiRob = this.requester.route("secure.retrieve-mono")
+				.data("rob")
+				.retrieveMono(String.class)
+				.block();
+
+		assertThat(hiRob).isEqualTo("Hi rob");
+	}
+
 	private Jwt jwt() {
 		return TestJwts.jwt()
 				.claim(IdTokenClaimNames.ISS, "https://issuer.example.com")
@@ -145,7 +167,7 @@ public class JwtITests {
 		@Bean
 		public RSocketStrategies rsocketStrategies() {
 			return RSocketStrategies.builder()
-					.encoder(new BasicAuthenticationEncoder())
+					.encoder(new BearerTokenAuthenticationEncoder())
 					.build();
 		}
 
@@ -154,7 +176,7 @@ public class JwtITests {
 			rsocket
 				.authorizePayload(authorize ->
 					authorize
-						.route("secure.admin.*").authenticated()
+						.anyRequest().authenticated()
 						.anyExchange().permitAll()
 				)
 				.jwt(Customizer.withDefaults());

+ 192 - 0
config/src/test/java/org/springframework/security/config/annotation/rsocket/SimpleAuthenticationITests.java

@@ -0,0 +1,192 @@
+/*
+ * Copyright 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.config.annotation.rsocket;
+
+import io.rsocket.RSocketFactory;
+import io.rsocket.exceptions.ApplicationErrorException;
+import io.rsocket.frame.decoder.PayloadDecoder;
+import io.rsocket.transport.netty.server.CloseableChannel;
+import io.rsocket.transport.netty.server.TcpServerTransport;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.rsocket.RSocketRequester;
+import org.springframework.messaging.rsocket.RSocketStrategies;
+import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;
+import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor;
+import org.springframework.security.rsocket.metadata.SimpleAuthenticationEncoder;
+import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata;
+import org.springframework.stereotype.Controller;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.rsocket.metadata.WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * @author Rob Winch
+ */
+@ContextConfiguration
+@RunWith(SpringRunner.class)
+public class SimpleAuthenticationITests {
+	@Autowired
+	RSocketMessageHandler handler;
+
+	@Autowired
+	SecuritySocketAcceptorInterceptor interceptor;
+
+	@Autowired
+	ServerController controller;
+
+	private CloseableChannel server;
+
+	private RSocketRequester requester;
+
+	@Before
+	public void setup() {
+		this.server = RSocketFactory.receive()
+				.frameDecoder(PayloadDecoder.ZERO_COPY)
+				.addSocketAcceptorPlugin(this.interceptor)
+				.acceptor(this.handler.responder())
+				.transport(TcpServerTransport.create("localhost", 0))
+				.start()
+				.block();
+	}
+
+	@After
+	public void dispose() {
+		this.requester.rsocket().dispose();
+		this.server.dispose();
+		this.controller.payloads.clear();
+	}
+
+	@Test
+	public void retrieveMonoWhenSecureThenDenied() throws Exception {
+		this.requester = RSocketRequester.builder()
+				.rsocketStrategies(this.handler.getRSocketStrategies())
+				.connectTcp("localhost", this.server.address().getPort())
+				.block();
+
+		String data = "rob";
+		assertThatCode(() -> this.requester.route("secure.retrieve-mono")
+				.data(data)
+				.retrieveMono(String.class)
+				.block()
+			)
+			.isInstanceOf(ApplicationErrorException.class);
+		assertThat(this.controller.payloads).isEmpty();
+	}
+
+	@Test
+	public void retrieveMonoWhenAuthorizedThenGranted() {
+		MimeType authenticationMimeType = MimeTypeUtils.parseMimeType(MESSAGE_RSOCKET_AUTHENTICATION.getString());
+
+		UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("rob", "password");
+		this.requester = RSocketRequester.builder()
+				.setupMetadata(credentials, authenticationMimeType)
+				.rsocketStrategies(this.handler.getRSocketStrategies())
+				.connectTcp("localhost", this.server.address().getPort())
+				.block();
+		String data = "rob";
+		String hiRob = this.requester.route("secure.retrieve-mono")
+				.metadata(credentials, authenticationMimeType)
+				.data(data)
+				.retrieveMono(String.class)
+				.block();
+
+		assertThat(hiRob).isEqualTo("Hi rob");
+		assertThat(this.controller.payloads).containsOnly(data);
+	}
+
+	@Configuration
+	@EnableRSocketSecurity
+	static class Config {
+
+		@Bean
+		public ServerController controller() {
+			return new ServerController();
+		}
+
+		@Bean
+		public RSocketMessageHandler messageHandler() {
+			RSocketMessageHandler handler = new RSocketMessageHandler();
+			handler.setRSocketStrategies(rsocketStrategies());
+			return handler;
+		}
+
+		@Bean
+		public RSocketStrategies rsocketStrategies() {
+			return RSocketStrategies.builder()
+					.encoder(new SimpleAuthenticationEncoder())
+					.build();
+		}
+
+		@Bean
+		PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
+			rsocket
+					.authorizePayload(authorize ->
+							authorize
+									.anyRequest().authenticated()
+									.anyExchange().permitAll()
+					)
+					.simpleAuthentication(Customizer.withDefaults());
+			return rsocket.build();
+		}
+
+		@Bean
+		MapReactiveUserDetailsService uds() {
+			UserDetails rob = User.withDefaultPasswordEncoder()
+					.username("rob")
+					.password("password")
+					.roles("USER", "ADMIN")
+					.build();
+			return new MapReactiveUserDetailsService(rob);
+		}
+	}
+
+	@Controller
+	static class ServerController {
+		private List<String> payloads = new ArrayList<>();
+
+		@MessageMapping("**")
+		String retrieveMono(String payload) {
+			add(payload);
+			return "Hi " + payload;
+		}
+
+		private void add(String p) {
+			this.payloads.add(p);
+		}
+	}
+
+}

+ 27 - 15
docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc

@@ -32,7 +32,7 @@ public class HelloRSocketSecurityConfig {
 }
 -----
 
-This configuration enables <<rsocket-authentication-basic,basic authentication>> and sets up <<authorization,rsocket-authorization>> to require an authenticated user for any request.
+This configuration enables <<rsocket-authentication-simple,simple authentication>> and sets up <<authorization,rsocket-authorization>> to require an authenticated user for any request.
 
 == Adding SecuritySocketAcceptorInterceptor
 
@@ -73,12 +73,18 @@ If we need to restrict the connection to the web application itself, we can prov
 Then each user would have different authorities but not the `SETUP` authority.
 This means that individual users can make requests but not make additional connections.
 
-[[rsocket-authentication-basic]]
-=== Basic Authentication
+[[rsocket-authentication-simple]]
+=== Simple Authentication
 
-Spring Security has early support for https://github.com/rsocket/rsocket/issues/272[RSocket's Basic Authentication Metadata Extension].
+Spring Security has support for https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md[Simple Authentication Metadata Extension].
 
-The RSocket receiver can decode the credentials using `BasicAuthenticationPayloadExchangeConverter` which is automatically setup using the `basicAuthentication` portion of the DSL.
+[NOTE]
+====
+Basic Authentication drafts evolved into Simple Authentication and is only supported for backward compatibility.
+See `RSocketSecurity.basicAuthentication(Customizer)` for setting it up.
+====
+
+The RSocket receiver can decode the credentials using `AuthenticationPayloadExchangeConverter` which is automatically setup using the `simpleAuthentication` portion of the DSL.
 An explicit configuration can be found below.
 
 [source,java]
@@ -91,26 +97,28 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
 					.anyRequest().authenticated()
 					.anyExchange().permitAll()
 		)
-		.basicAuthentication(Customizer.withDefaults());
+		.simpleAuthentication(Customizer.withDefaults());
 	return rsocket.build();
 }
 ----
 
-The RSocket sender can send credentials using `BasicAuthenticationEncoder` which can be added to Spring's `RSocketStrategies`.
+The RSocket sender can send credentials using `SimpleAuthenticationEncoder` which can be added to Spring's `RSocketStrategies`.
 
 [source,java]
 ----
 RSocketStrategies.Builder strategies = ...;
-strategies.encoder(new BasicAuthenticationEncoder());
+strategies.encoder(new SimpleAuthenticationEncoder());
 ----
 
 It can then be used to send a username and password to the receiver in the setup:
 
 [source,java]
 ----
+MimeType authenticationMimeType =
+	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
 UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
 Mono<RSocketRequester> requester = RSocketRequester.builder()
-	.setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
+	.setupMetadata(credentials, authenticationMimeType)
 	.rsocketStrategies(strategies.build())
 	.connectTcp(host, port);
 ----
@@ -125,7 +133,7 @@ UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "pas
 public Mono<AirportLocation> findRadar(String code) {
 	return this.requester.flatMap(req ->
 		req.route("find.radar.{code}", code)
-			.metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
+			.metadata(credentials, authenticationMimeType)
 			.retrieveMono(AirportLocation.class)
 	);
 }
@@ -134,7 +142,7 @@ public Mono<AirportLocation> findRadar(String code) {
 [[rsocket-authentication-jwt]]
 === JWT
 
-Spring Security has early support for https://github.com/rsocket/rsocket/issues/272[RSocket's Bearer Token Authentication Metadata Extension].
+Spring Security has support for https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Bearer.md[Bearer Token Authentication Metadata Extension].
 The support comes in the form of authenticating a JWT (determining the JWT is valid) and then using the JWT to make authorization decisions.
 
 The RSocket receiver can decode the credentials using `BearerPayloadExchangeConverter` which is automatically setup using the `jwt` portion of the DSL.
@@ -172,9 +180,11 @@ For example, the token can be sent at setup time:
 
 [source,java]
 ----
-String token = ...;
+MimeType authenticationMimeType =
+	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
+BearerTokenMetadata token = ...;
 Mono<RSocketRequester> requester = RSocketRequester.builder()
-	.setupMetadata(token, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
+	.setupMetadata(token, authenticationMimeType)
 	.connectTcp(host, port);
 ----
 
@@ -182,13 +192,15 @@ Alternatively or additionally, the token can be sent in a request.
 
 [source,java]
 ----
+MimeType authenticationMimeType =
+	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
 Mono<RSocketRequester> requester;
-String token = ...;
+BearerTokenMetadata token = ...;
 
 public Mono<AirportLocation> findRadar(String code) {
 	return this.requester.flatMap(req ->
 		req.route("find.radar.{code}", code)
-	        .metadata(token, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
+	        .metadata(token, authenticationMimeType)
 			.retrieveMono(AirportLocation.class)
 	);
 }

+ 102 - 0
rsocket/src/main/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadExchangeConverter.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright 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.rsocket.authentication;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.rsocket.metadata.WellKnownMimeType;
+import io.rsocket.metadata.security.AuthMetadataFlyweight;
+import io.rsocket.metadata.security.WellKnownAuthType;
+import org.springframework.core.codec.ByteArrayDecoder;
+import org.springframework.messaging.rsocket.DefaultMetadataExtractor;
+import org.springframework.messaging.rsocket.MetadataExtractor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
+import org.springframework.security.rsocket.api.PayloadExchange;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * Converts from the {@link PayloadExchange} for
+ * <a href="https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Authentication.md">Authentication Extension</a>.
+ * For
+ * <a href="https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md">Simple</a>
+ * a {@link UsernamePasswordAuthenticationToken} is returned. For
+ * <a href="https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Bearer.md">Bearer</a>
+ * a {@link BearerTokenAuthenticationToken} is returned.
+ *
+ * @author Rob Winch
+ * @since 5.3
+ */
+public class AuthenticationPayloadExchangeConverter implements PayloadExchangeAuthenticationConverter {
+	private static final MimeType COMPOSITE_METADATA_MIME_TYPE = MimeTypeUtils.parseMimeType(
+			WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
+
+	private static final MimeType AUTHENTICATION_MIME_TYPE = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
+
+	private MetadataExtractor metadataExtractor = createDefaultExtractor();
+
+	@Override
+	public Mono<Authentication> convert(PayloadExchange exchange) {
+		return Mono.fromCallable(() -> this.metadataExtractor
+				.extract(exchange.getPayload(), this.COMPOSITE_METADATA_MIME_TYPE))
+				.flatMap(metadata -> Mono.justOrEmpty(authentication(metadata)));
+	}
+
+	private Authentication authentication(Map<String, Object> metadata) {
+		byte[] authenticationMetadata = (byte[]) metadata.get("authentication");
+		if (authenticationMetadata == null) {
+			return null;
+		}
+		ByteBuf rawAuthentication = ByteBufAllocator.DEFAULT.buffer().writeBytes(authenticationMetadata);
+		if (!AuthMetadataFlyweight.isWellKnownAuthType(rawAuthentication)) {
+			return null;
+		}
+		WellKnownAuthType wellKnownAuthType = AuthMetadataFlyweight.decodeWellKnownAuthType(rawAuthentication);
+		if (WellKnownAuthType.SIMPLE.equals(wellKnownAuthType)) {
+			return simple(rawAuthentication);
+		} else if (WellKnownAuthType.BEARER.equals(wellKnownAuthType)) {
+			return bearer(rawAuthentication);
+		}
+		throw new IllegalArgumentException("Unknown Mime Type " + wellKnownAuthType);
+	}
+
+	private Authentication simple(ByteBuf rawAuthentication) {
+		ByteBuf rawUsername = AuthMetadataFlyweight.decodeUsername(rawAuthentication);
+		String username = rawUsername.toString(StandardCharsets.UTF_8);
+		ByteBuf rawPassword = AuthMetadataFlyweight.decodePassword(rawAuthentication);
+		String password = rawPassword.toString(StandardCharsets.UTF_8);
+		return new UsernamePasswordAuthenticationToken(username, password);
+	}
+
+	private Authentication bearer(ByteBuf rawAuthentication) {
+		char[] rawToken = AuthMetadataFlyweight.decodeBearerTokenAsCharArray(rawAuthentication);
+		String token = new String(rawToken);
+		return new BearerTokenAuthenticationToken(token);
+	}
+
+	private static MetadataExtractor createDefaultExtractor() {
+		DefaultMetadataExtractor result = new DefaultMetadataExtractor(new ByteArrayDecoder());
+		result.metadataToExtract(AUTHENTICATION_MIME_TYPE, byte[].class, "authentication");
+		return result;
+	}
+}

+ 2 - 0
rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoder.java

@@ -31,7 +31,9 @@ import java.util.Map;
  *
  * @author Rob Winch
  * @since 5.2
+ * @deprecated Basic Authentication did not evolve into a standard. Use Simple Authentication instead.
  */
+@Deprecated
 public class BasicAuthenticationDecoder extends AbstractDecoder<UsernamePasswordMetadata> {
 	public BasicAuthenticationDecoder() {
 		super(UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE);

+ 2 - 0
rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationEncoder.java

@@ -34,7 +34,9 @@ import java.util.Map;
  *
  * @author Rob Winch
  * @since 5.2
+ * @deprecated Basic Authentication did not evolve into a standard. use {@link SimpleAuthenticationEncoder}
  */
+@Deprecated
 public class BasicAuthenticationEncoder extends
 		AbstractEncoder<UsernamePasswordMetadata> {
 

+ 78 - 0
rsocket/src/main/java/org/springframework/security/rsocket/metadata/BearerTokenAuthenticationEncoder.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright 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.rsocket.metadata;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.rsocket.metadata.security.AuthMetadataFlyweight;
+import org.reactivestreams.Publisher;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.codec.AbstractEncoder;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.NettyDataBufferFactory;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils;
+import reactor.core.publisher.Flux;
+
+import java.util.Map;
+
+/**
+ * Encodes <a href="https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Bearer.md">Bearer Authentication</a>.
+ *
+ * @author Rob Winch
+ * @since 5.3
+ */
+public class BearerTokenAuthenticationEncoder extends
+		AbstractEncoder<BearerTokenMetadata> {
+
+	private static final MimeType AUTHENTICATION_MIME_TYPE = MimeTypeUtils.parseMimeType("message/x.rsocket.authentication.v0");
+
+	private NettyDataBufferFactory defaultBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
+
+	public BearerTokenAuthenticationEncoder() {
+		super(AUTHENTICATION_MIME_TYPE);
+	}
+
+	@Override
+	public Flux<DataBuffer> encode(
+			Publisher<? extends BearerTokenMetadata> inputStream,
+			DataBufferFactory bufferFactory, ResolvableType elementType,
+			MimeType mimeType, Map<String, Object> hints) {
+		return Flux.from(inputStream).map(credentials ->
+				encodeValue(credentials, bufferFactory, elementType, mimeType, hints));
+	}
+
+	@Override
+	public DataBuffer encodeValue(BearerTokenMetadata credentials,
+			DataBufferFactory bufferFactory, ResolvableType valueType, MimeType mimeType,
+			Map<String, Object> hints) {
+		String token = credentials.getToken();
+		NettyDataBufferFactory factory = nettyFactory(bufferFactory);
+		ByteBufAllocator allocator = factory.getByteBufAllocator();
+		ByteBuf simpleAuthentication = AuthMetadataFlyweight
+				.encodeBearerMetadata(allocator, token.toCharArray());
+		return factory.wrap(simpleAuthentication);
+	}
+
+	private NettyDataBufferFactory nettyFactory(DataBufferFactory bufferFactory) {
+		if (bufferFactory instanceof NettyDataBufferFactory) {
+			return (NettyDataBufferFactory) bufferFactory;
+		}
+		return this.defaultBufferFactory;
+	}
+}

+ 2 - 0
rsocket/src/main/java/org/springframework/security/rsocket/metadata/BearerTokenMetadata.java

@@ -32,7 +32,9 @@ public class BearerTokenMetadata {
 	 * Represents a bearer token which is encoded as a String.
 	 *
 	 * See <a href="https://github.com/rsocket/rsocket/issues/272">rsocket/rsocket#272</a>
+	 * @deprecated Basic did not evolve into the standard. Instead use Simple Authentication MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString())
 	 */
+	@Deprecated
 	public static final MimeType BEARER_AUTHENTICATION_MIME_TYPE = new MediaType("message", "x.rsocket.authentication.bearer.v0");
 
 	private final String token;

+ 81 - 0
rsocket/src/main/java/org/springframework/security/rsocket/metadata/SimpleAuthenticationEncoder.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright 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.rsocket.metadata;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.rsocket.metadata.security.AuthMetadataFlyweight;
+import org.reactivestreams.Publisher;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.codec.AbstractEncoder;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.NettyDataBufferFactory;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils;
+import reactor.core.publisher.Flux;
+
+import java.util.Map;
+
+/**
+ * Encodes
+ * <a href="https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md">Simple</a>
+ * Authentication.
+ *
+ * @author Rob Winch
+ * @since 5.3
+ */
+public class SimpleAuthenticationEncoder extends
+		AbstractEncoder<UsernamePasswordMetadata> {
+
+	private static final MimeType AUTHENTICATION_MIME_TYPE = MimeTypeUtils.parseMimeType("message/x.rsocket.authentication.v0");
+
+	private NettyDataBufferFactory defaultBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
+
+	public SimpleAuthenticationEncoder() {
+		super(AUTHENTICATION_MIME_TYPE);
+	}
+
+	@Override
+	public Flux<DataBuffer> encode(
+			Publisher<? extends UsernamePasswordMetadata> inputStream,
+			DataBufferFactory bufferFactory, ResolvableType elementType,
+			MimeType mimeType, Map<String, Object> hints) {
+		return Flux.from(inputStream).map(credentials ->
+				encodeValue(credentials, bufferFactory, elementType, mimeType, hints));
+	}
+
+	@Override
+	public DataBuffer encodeValue(UsernamePasswordMetadata credentials,
+			DataBufferFactory bufferFactory, ResolvableType valueType, MimeType mimeType,
+			Map<String, Object> hints) {
+		String username = credentials.getUsername();
+		String password = credentials.getPassword();
+		NettyDataBufferFactory factory = nettyFactory(bufferFactory);
+		ByteBufAllocator allocator = factory.getByteBufAllocator();
+		ByteBuf simpleAuthentication = AuthMetadataFlyweight
+				.encodeSimpleMetadata(allocator, username.toCharArray(), password.toCharArray());
+		return factory.wrap(simpleAuthentication);
+	}
+
+	private NettyDataBufferFactory nettyFactory(DataBufferFactory bufferFactory) {
+		if (bufferFactory instanceof NettyDataBufferFactory) {
+			return (NettyDataBufferFactory) bufferFactory;
+		}
+		return this.defaultBufferFactory;
+	}
+}

+ 2 - 0
rsocket/src/main/java/org/springframework/security/rsocket/metadata/UsernamePasswordMetadata.java

@@ -33,7 +33,9 @@ public final class UsernamePasswordMetadata {
 	 * {@code ${username-bytes-length}${username-bytes}${password-bytes}}.
 	 *
 	 * See <a href="https://github.com/rsocket/rsocket/issues/272">rsocket/rsocket#272</a>
+	 * @deprecated Basic did not evolve into the standard. Instead use Simple Authentication MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString())
 	 */
+	@Deprecated
 	public static final MimeType BASIC_AUTHENTICATION_MIME_TYPE = new MediaType("message", "x.rsocket.authentication.basic.v0");
 
 	private final String username;

+ 2 - 0
samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle

@@ -1,5 +1,7 @@
 apply plugin: 'io.spring.convention.spring-sample-boot'
 
+ext['rsocket.version'] = '1.0.0-RC6'
+
 dependencies {
 	compile project(':spring-security-core')
 	compile project(':spring-security-config')