431 lines
14 KiB
Plaintext
431 lines
14 KiB
Plaintext
[[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:
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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`.
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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.
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.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.
|