|
| 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