Skip to content

Commit d83aa34

Browse files
committed
Add RSocket Reference
Fixes gh-7502
1 parent b764af6 commit d83aa34

File tree

3 files changed

+214
-1
lines changed

3 files changed

+214
-1
lines changed

docs/manual/src/docs/asciidoc/_includes/about/whats-new.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Below are the highlights of the release.
4444

4545
=== Core
4646

47-
* Introducing https://github.com/spring-projects/spring-security/issues/7360[RSocket] support
47+
* Introducing <<rsoket,RSocket>> support
4848
* Introducing https://github.com/spring-projects/spring-security/issues/6019[SAML Service Provider] support
4949
* Introducing https://github.com/spring-projects/spring-security/issues/6722[AuthenticationManagerResolver]
5050
* Introducing https://github.com/spring-projects/spring-security/issues/6506[AuthenticationFilter]

docs/manual/src/docs/asciidoc/_includes/reactive/index.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ include::webclient.adoc[leveloffset=+1]
1717
include::method.adoc[leveloffset=+1]
1818

1919
include::test.adoc[leveloffset=+1]
20+
21+
include::rsocket.adoc[leveloffset=+1]
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
[[rsocket]]
2+
= RSocket Security
3+
4+
Spring Security's RSocket support relies on a `SocketAcceptorInterceptor`.
5+
The main entry point into security is found in the `PayloadSocketAcceptorInterceptor` which adapts the RSocket APIs to allow intercepting a `PayloadExchange` with `PayloadInterceptor` implementations.
6+
7+
== Minimal RSocket Security Configuration
8+
9+
You can find a minimal RSocket Security configuration below:
10+
11+
[source,java]
12+
-----
13+
@Configuration
14+
@EnableRSocketSecurity
15+
public class HelloRSocketSecurityConfig {
16+
17+
@Bean
18+
public MapReactiveUserDetailsService userDetailsService() {
19+
UserDetails user = User.withDefaultPasswordEncoder()
20+
.username("user")
21+
.password("user")
22+
.roles("USER")
23+
.build();
24+
return new MapReactiveUserDetailsService(user);
25+
}
26+
}
27+
-----
28+
29+
This configuration enables <<rsocket-authentication-basic,basic authentication>> and sets up <<authorization,rsocket-authorization>> to require an authenticated user for any request.
30+
31+
[[rsocket-authentication]]
32+
== RSocket Authentication
33+
34+
RSocket authentication is performed with `AuthenticationPayloadInterceptor` which acts as a controller to invoke a `ReactiveAuthenticationManager` instance.
35+
36+
[[rsocket-authentication-setup-vs-request]]
37+
=== Authentication at Setup vs Request Time
38+
39+
Generally, authentication can occur at setup time and/or request time.
40+
41+
Authentication at setup time makes sense in a few scenarios.
42+
A common scenarios is when a single user (i.e. mobile connection) is leveraging an RSocket connection.
43+
In this case only a single user is leveraging the connection, so authentication can be done once at connection time.
44+
45+
In a scenario where the RSocket connection is shared it makes sense to send credentials on each request.
46+
For example, a web application that connects to an RSocket server as a downstream service would make a single connection that all users leverage.
47+
In this case, if the RSocket server needs to perform authorization based on the web application's users credentials per request makes sense.
48+
49+
In some scenarios authentication at setup and per request makes sense.
50+
Consider a web application as described previously.
51+
If we need to restrict the connection to the web application itself, we can provide a credential with a `SETUP` authority at connection time.
52+
Then each user would have different authorities but not the `SETUP` authority.
53+
This means that individual users can make requests but not make additional connections.
54+
55+
[[rsocket-authentication-basic]]
56+
=== Basic Authentication
57+
58+
Spring Security has early support for https://github.com/rsocket/rsocket/issues/272[RSocket's Basic Authentication Metadata Extension].
59+
60+
The RSocket receiver can decode the credentials using `BasicAuthenticationPayloadExchangeConverter` which is automatically setup using the `basicAuthentication` portion of the DSL.
61+
An explicit configuration can be found below.
62+
63+
[source,java]
64+
----
65+
@Bean
66+
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
67+
rsocket
68+
.authorizePayload(authorize ->
69+
authorize
70+
.anyRequest().authenticated()
71+
.anyExchange().permitAll()
72+
)
73+
.basicAuthentication(Customizer.withDefaults());
74+
return rsocket.build();
75+
}
76+
----
77+
78+
The RSocket sender can send credentials using `BasicAuthenticationEncoder` which can be added to Spring's `RSocketStrategies`.
79+
80+
[source,java]
81+
----
82+
RSocketStrategies.Builder strategies = ...;
83+
strategies.encoder(new BasicAuthenticationEncoder());
84+
----
85+
86+
It can then be used to send a username and password to the receiver in the setup:
87+
88+
[source,java]
89+
----
90+
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
91+
Mono<RSocketRequester> requester = RSocketRequester.builder()
92+
.setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
93+
.rsocketStrategies(strategies.build())
94+
.connectTcp(host, port);
95+
----
96+
97+
Alternatively or additionally, a username and password can be sent in a request.
98+
99+
[source,java]
100+
----
101+
Mono<RSocketRequester> requester;
102+
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
103+
104+
public Mono<AirportLocation> findRadar(String code) {
105+
return this.requester.flatMap(req ->
106+
req.route("find.radar.{code}", code)
107+
.metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
108+
.retrieveMono(AirportLocation.class)
109+
);
110+
}
111+
----
112+
113+
[[rsocket-authentication-jwt]]
114+
=== JWT
115+
116+
Spring Security has early support for https://github.com/rsocket/rsocket/issues/272[RSocket's Bearer Token Authentication Metadata Extension].
117+
The support comes in the form of authenticating a JWT (determining the JWT is valid) and then using the JWT to make authorization decisions.
118+
119+
The RSocket receiver can decode the credentials using `BearerPayloadExchangeConverter` which is automatically setup using the `jwt` portion of the DSL.
120+
An example configuration can be found below:
121+
122+
[source,java]
123+
----
124+
@Bean
125+
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
126+
rsocket
127+
.authorizePayload(authorize ->
128+
authorize
129+
.anyRequest().authenticated()
130+
.anyExchange().permitAll()
131+
)
132+
.jwt(Customizer.withDefaults());
133+
return rsocket.build();
134+
}
135+
----
136+
137+
The configuration above relies on the existence of a `ReactiveJwtDecoder` `@Bean` being present.
138+
An example of creating one from the issuer can be found below:
139+
140+
[source,java]
141+
----
142+
@Bean
143+
ReactiveJwtDecoder jwtDecoder() {
144+
return ReactiveJwtDecoders
145+
.fromIssuerLocation("https://example.com/auth/realms/demo");
146+
}
147+
----
148+
149+
The RSocket sender does not need to do anything special to send the token because the value is just a simple String.
150+
For example, the token can be sent at setup time:
151+
152+
[source,java]
153+
----
154+
String token = ...;
155+
Mono<RSocketRequester> requester = RSocketRequester.builder()
156+
.setupMetadata(token, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
157+
.connectTcp(host, port);
158+
----
159+
160+
Alternatively or additionally, the token can be sent in a request.
161+
162+
[source,java]
163+
----
164+
Mono<RSocketRequester> requester;
165+
String token = ...;
166+
167+
public Mono<AirportLocation> findRadar(String code) {
168+
return this.requester.flatMap(req ->
169+
req.route("find.radar.{code}", code)
170+
.metadata(token, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
171+
.retrieveMono(AirportLocation.class)
172+
);
173+
}
174+
----
175+
176+
[[rsocket-authorization]]
177+
== RSocket Authorization
178+
179+
RSocket authorization is performed with `AuthorizationPayloadInterceptor` which acts as a controller to invoke a `ReactiveAuthorizationManager` instance.
180+
The DSL can be used to setup authorization rules based upon the `PayloadExchange`.
181+
An example configuration can be found below:
182+
183+
[[source,java]]
184+
----
185+
rsocket
186+
.authorizePayload(authorize ->
187+
authz
188+
.setup().hasRole("SETUP") // <1>
189+
.route("fetch.profile.me").authenticated() // <2>
190+
.matcher(payloadExchange -> isMatch(payloadExchange)) // <3>
191+
.hasRole("CUSTOM")
192+
.route("fetch.profile.{username}") // <4>
193+
.access((authentication, context) -> checkFriends(authentication, context))
194+
.anyRequest().authenticated() // <5>
195+
.anyExchange().permitAll() // <6>
196+
)
197+
----
198+
<1> Setting up a connection requires the authority `ROLE_SETUP`
199+
<2> If the route is `fetch.profile.me` authorization only requires the user be authenticated
200+
<3> In this rule we setup a custom matcher where authorization requires the user to have the authority `ROLE_CUSTOM`
201+
<4> This rule leverages custom authorization.
202+
The matcher expresses a variable with the name `username` that is made available in the `context`.
203+
A custom authorization rule is exposed in the `checkFriends` method.
204+
<5> This rule ensures that request that does not already have a rule will require the user to be authenticated.
205+
A request is where the metadata is included.
206+
It would not include additional payloads.
207+
<6> This rule ensures that any exchange that does not already have a rule is allowed for anyone.
208+
In this example, it means that payloads that have no metadata have no authorization rules.
209+
210+
It is important to understand that authorization rules are performed in order.
211+
Only the first authorization rule that matches will be invoked.

0 commit comments

Comments
 (0)