123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- [[rsocket]]
- = RSocket Security
- Spring Security's RSocket support relies on a `SocketAcceptorInterceptor`.
- The main entry point into security is in `PayloadSocketAcceptorInterceptor`, which adapts the RSocket APIs to allow intercepting a `PayloadExchange` with `PayloadInterceptor` implementations.
- The following example shows a minimal RSocket Security configuration:
- * Hello RSocket {gh-samples-url}/reactive/rsocket/hello-security[hellorsocket]
- * https://github.com/rwinch/spring-flights/tree/security[Spring Flights]
- == Minimal RSocket Security Configuration
- You can find a minimal RSocket Security configuration below:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Configuration
- @EnableRSocketSecurity
- public class HelloRSocketSecurityConfig {
- @Bean
- public MapReactiveUserDetailsService userDetailsService() {
- UserDetails user = User.withDefaultPasswordEncoder()
- .username("user")
- .password("user")
- .roles("USER")
- .build();
- return new MapReactiveUserDetailsService(user);
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Configuration
- @EnableRSocketSecurity
- open class HelloRSocketSecurityConfig {
- @Bean
- open fun userDetailsService(): MapReactiveUserDetailsService {
- val user = User.withDefaultPasswordEncoder()
- .username("user")
- .password("user")
- .roles("USER")
- .build()
- return MapReactiveUserDetailsService(user)
- }
- }
- ----
- ======
- This configuration enables <<rsocket-authentication-simple,simple authentication>> and sets up <<rsocket-authorization,rsocket-authorization>> to require an authenticated user for any request.
- == Adding SecuritySocketAcceptorInterceptor
- For Spring Security to work, we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
- Doing so connects our `PayloadSocketAcceptorInterceptor` with the RSocket infrastructure.
- In a Spring Boot application, you can do this automatically by using `RSocketSecurityAutoConfiguration` with the following code:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) {
- return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor));
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor): RSocketServerCustomizer {
- return RSocketServerCustomizer { server ->
- server.interceptors { registry ->
- registry.forSocketAcceptor(interceptor)
- }
- }
- }
- ----
- ======
- [[rsocket-authentication]]
- == RSocket Authentication
- RSocket authentication is performed with `AuthenticationPayloadInterceptor`, which acts as a controller to invoke a `ReactiveAuthenticationManager` instance.
- [[rsocket-authentication-setup-vs-request]]
- === Authentication at Setup versus Request Time
- Generally, authentication can occur at setup time or at request time or both.
- Authentication at setup time makes sense in a few scenarios.
- A common scenarios is when a single user (such as a mobile connection) uses an RSocket connection.
- In this case, only a single user uses the connection, so authentication can be done once at connection time.
- In a scenario where the RSocket connection is shared, it makes sense to send credentials on each request.
- For example, a web application that connects to an RSocket server as a downstream service would make a single connection that all users use.
- In this case, if the RSocket server needs to perform authorization based on the web application's users credentials, authentication for each request makes sense.
- In some scenarios, authentication at both setup and for each request makes sense.
- Consider a web application, as described previously.
- If we need to restrict the connection to the web application itself, we can provide a credential with a `SETUP` authority at connection time.
- Then each user can have different authorities but not the `SETUP` authority.
- This means that individual users can make requests but not make additional connections.
- [[rsocket-authentication-simple]]
- === Simple Authentication
- Spring Security has support for the https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md[Simple Authentication Metadata Extension].
- [NOTE]
- ====
- Basic Authentication 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 by using `AuthenticationPayloadExchangeConverter`, which is automatically setup by using the `simpleAuthentication` portion of the DSL.
- The following example shows an explicit configuration:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
- rsocket
- .authorizePayload(authorize ->
- authorize
- .anyRequest().authenticated()
- .anyExchange().permitAll()
- )
- .simpleAuthentication(Customizer.withDefaults());
- return rsocket.build();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- open fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
- rsocket
- .authorizePayload { authorize -> authorize
- .anyRequest().authenticated()
- .anyExchange().permitAll()
- }
- .simpleAuthentication(withDefaults())
- return rsocket.build()
- }
- ----
- ======
- The RSocket sender can send credentials by using `SimpleAuthenticationEncoder`, which you can add to Spring's `RSocketStrategies`.
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- RSocketStrategies.Builder strategies = ...;
- strategies.encoder(new SimpleAuthenticationEncoder());
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- var strategies: RSocketStrategies.Builder = ...
- strategies.encoder(SimpleAuthenticationEncoder())
- ----
- ======
- You can then use it to send a username and password to the receiver in the setup:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- MimeType authenticationMimeType =
- MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
- UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
- Mono<RSocketRequester> requester = RSocketRequester.builder()
- .setupMetadata(credentials, authenticationMimeType)
- .rsocketStrategies(strategies.build())
- .connectTcp(host, port);
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- val authenticationMimeType: MimeType =
- MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
- val credentials = UsernamePasswordMetadata("user", "password")
- val requester: Mono<RSocketRequester> = RSocketRequester.builder()
- .setupMetadata(credentials, authenticationMimeType)
- .rsocketStrategies(strategies.build())
- .connectTcp(host, port)
- ----
- ======
- Alternatively or additionally, a username and password can be sent in a request.
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- Mono<RSocketRequester> requester;
- UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
- public Mono<AirportLocation> findRadar(String code) {
- return this.requester.flatMap(req ->
- req.route("find.radar.{code}", code)
- .metadata(credentials, authenticationMimeType)
- .retrieveMono(AirportLocation.class)
- );
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- import org.springframework.messaging.rsocket.retrieveMono
- // ...
- var requester: Mono<RSocketRequester>? = null
- var credentials = UsernamePasswordMetadata("user", "password")
- open fun findRadar(code: String): Mono<AirportLocation> {
- return requester!!.flatMap { req ->
- req.route("find.radar.{code}", code)
- .metadata(credentials, authenticationMimeType)
- .retrieveMono<AirportLocation>()
- }
- }
- ----
- ======
- [[rsocket-authentication-jwt]]
- === JWT
- Spring Security has support for the 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 that the JWT is valid) and then using the JWT to make authorization decisions.
- The RSocket receiver can decode the credentials by using `BearerPayloadExchangeConverter`, which is automatically setup by using the `jwt` portion of the DSL.
- The following listing shows an example configuration:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
- rsocket
- .authorizePayload(authorize ->
- authorize
- .anyRequest().authenticated()
- .anyExchange().permitAll()
- )
- .jwt(Customizer.withDefaults());
- return rsocket.build();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
- rsocket
- .authorizePayload { authorize -> authorize
- .anyRequest().authenticated()
- .anyExchange().permitAll()
- }
- .jwt(withDefaults())
- return rsocket.build()
- }
- ----
- ======
- The configuration above relies on the existence of a `ReactiveJwtDecoder` `@Bean` being present.
- An example of creating one from the issuer can be found below:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- ReactiveJwtDecoder jwtDecoder() {
- return ReactiveJwtDecoders
- .fromIssuerLocation("https://example.com/auth/realms/demo");
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun jwtDecoder(): ReactiveJwtDecoder {
- return ReactiveJwtDecoders
- .fromIssuerLocation("https://example.com/auth/realms/demo")
- }
- ----
- ======
- The RSocket sender does not need to do anything special to send the token, because the value is a simple `String`.
- The following example sends the token at setup time:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- MimeType authenticationMimeType =
- MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
- BearerTokenMetadata token = ...;
- Mono<RSocketRequester> requester = RSocketRequester.builder()
- .setupMetadata(token, authenticationMimeType)
- .connectTcp(host, port);
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- val authenticationMimeType: MimeType =
- MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
- val token: BearerTokenMetadata = ...
- val requester = RSocketRequester.builder()
- .setupMetadata(token, authenticationMimeType)
- .connectTcp(host, port)
- ----
- ======
- Alternatively or additionally, you can send the token in a request:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- MimeType authenticationMimeType =
- MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
- Mono<RSocketRequester> requester;
- BearerTokenMetadata token = ...;
- public Mono<AirportLocation> findRadar(String code) {
- return this.requester.flatMap(req ->
- req.route("find.radar.{code}", code)
- .metadata(token, authenticationMimeType)
- .retrieveMono(AirportLocation.class)
- );
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- val authenticationMimeType: MimeType =
- MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
- var requester: Mono<RSocketRequester>? = null
- val token: BearerTokenMetadata = ...
- open fun findRadar(code: String): Mono<AirportLocation> {
- return this.requester!!.flatMap { req ->
- req.route("find.radar.{code}", code)
- .metadata(token, authenticationMimeType)
- .retrieveMono<AirportLocation>()
- }
- }
- ----
- ======
- [[rsocket-authorization]]
- == RSocket Authorization
- RSocket authorization is performed with `AuthorizationPayloadInterceptor`, which acts as a controller to invoke a `ReactiveAuthorizationManager` instance.
- You can use the DSL to set up authorization rules based upon the `PayloadExchange`.
- The following listing shows an example configuration:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- rsocket
- .authorizePayload(authz ->
- authz
- .setup().hasRole("SETUP") // <1>
- .route("fetch.profile.me").authenticated() // <2>
- .matcher(payloadExchange -> isMatch(payloadExchange)) // <3>
- .hasRole("CUSTOM")
- .route("fetch.profile.{username}") // <4>
- .access((authentication, context) -> checkFriends(authentication, context))
- .anyRequest().authenticated() // <5>
- .anyExchange().permitAll() // <6>
- );
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- rsocket
- .authorizePayload { authz ->
- authz
- .setup().hasRole("SETUP") // <1>
- .route("fetch.profile.me").authenticated() // <2>
- .matcher { payloadExchange -> isMatch(payloadExchange) } // <3>
- .hasRole("CUSTOM")
- .route("fetch.profile.{username}") // <4>
- .access { authentication, context -> checkFriends(authentication, context) }
- .anyRequest().authenticated() // <5>
- .anyExchange().permitAll()
- } // <6>
- ----
- ======
- <1> Setting up a connection requires the `ROLE_SETUP` authority.
- <2> If the route is `fetch.profile.me`, authorization only requires the user to be authenticated.
- <3> In this rule, we set up a custom matcher, where authorization requires the user to have the `ROLE_CUSTOM` authority.
- <4> This rule uses custom authorization.
- The matcher expresses a variable with a name of `username` that is made available in the `context`.
- A custom authorization rule is exposed in the `checkFriends` method.
- <5> This rule ensures that a request that does not already have a rule requires the user to be authenticated.
- A request is where the metadata is included.
- It would not include additional payloads.
- <6> This rule ensures that any exchange that does not already have a rule is allowed for anyone.
- In this example, it means that payloads that have no metadata also have no authorization rules.
- Note that authorization rules are performed in order.
- Only the first authorization rule that matches is invoked.
|