diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java index a05e90edcc..8ab04005e8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java +++ b/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 + * Simple Authentication + * @param simple a customizer + * @return RSocketSecurity for additional configuration + * @since 5.3 + */ + public RSocketSecurity simpleAuthentication(Customizer 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 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 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()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/SecuritySocketAcceptorInterceptorConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/SecuritySocketAcceptorInterceptorConfiguration.java index a469b29939..cdd007d61e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/SecuritySocketAcceptorInterceptorConfiguration.java +++ b/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() diff --git a/config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java b/config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java index 23b2b8a53f..6b08f11ffd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java +++ b/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()); diff --git a/config/src/test/java/org/springframework/security/config/annotation/rsocket/SimpleAuthenticationITests.java b/config/src/test/java/org/springframework/security/config/annotation/rsocket/SimpleAuthenticationITests.java new file mode 100644 index 0000000000..9c9f4607c3 --- /dev/null +++ b/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 payloads = new ArrayList<>(); + + @MessageMapping("**") + String retrieveMono(String payload) { + add(payload); + return "Hi " + payload; + } + + private void add(String p) { + this.payloads.add(p); + } + } + +} diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc index 0bbf66f735..0b942f6198 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc @@ -32,7 +32,7 @@ public class HelloRSocketSecurityConfig { } ----- -This configuration enables <> and sets up <> to require an authenticated user for any request. +This configuration enables <> and sets up <> 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 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 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 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 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 requester; -String token = ...; +BearerTokenMetadata token = ...; public Mono 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) ); } diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadExchangeConverter.java b/rsocket/src/main/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadExchangeConverter.java new file mode 100644 index 0000000000..8c38b1e315 --- /dev/null +++ b/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 + * Authentication Extension. + * For + * Simple + * a {@link UsernamePasswordAuthenticationToken} is returned. For + * Bearer + * 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 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 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; + } +} diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoder.java b/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoder.java index 5085e5a833..6b8a45fe41 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoder.java +++ b/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 { public BasicAuthenticationDecoder() { super(UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE); diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationEncoder.java b/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationEncoder.java index 9d088f5a2a..75e3f909ac 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BasicAuthenticationEncoder.java +++ b/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 { diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BearerTokenAuthenticationEncoder.java b/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BearerTokenAuthenticationEncoder.java new file mode 100644 index 0000000000..3a513c4dd5 --- /dev/null +++ b/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 Bearer Authentication. + * + * @author Rob Winch + * @since 5.3 + */ +public class BearerTokenAuthenticationEncoder extends + AbstractEncoder { + + 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 encode( + Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, + MimeType mimeType, Map 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 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; + } +} diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BearerTokenMetadata.java b/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BearerTokenMetadata.java index e252fa21f3..5998b07cdd 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/BearerTokenMetadata.java +++ b/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 rsocket/rsocket#272 + * @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; diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/SimpleAuthenticationEncoder.java b/rsocket/src/main/java/org/springframework/security/rsocket/metadata/SimpleAuthenticationEncoder.java new file mode 100644 index 0000000000..64e4dc2e2b --- /dev/null +++ b/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 + * Simple + * Authentication. + * + * @author Rob Winch + * @since 5.3 + */ +public class SimpleAuthenticationEncoder extends + AbstractEncoder { + + 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 encode( + Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, + MimeType mimeType, Map 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 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; + } +} diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/UsernamePasswordMetadata.java b/rsocket/src/main/java/org/springframework/security/rsocket/metadata/UsernamePasswordMetadata.java index e99e23aa40..dab4ad6ea5 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/metadata/UsernamePasswordMetadata.java +++ b/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 rsocket/rsocket#272 + * @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; diff --git a/samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle b/samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle index f2b788a0e2..b2fdafe9fd 100644 --- a/samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle +++ b/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')