diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc index 8b15c3e19a..832bd5c025 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc @@ -10,7 +10,9 @@ The easiest way to ensure that CORS is handled first is to use the `CorsWebFilte Users can integrate the `CorsWebFilter` with Spring Security by providing a `CorsConfigurationSource`. For example, the following will integrate CORS support within Spring Security: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean CorsConfigurationSource corsConfigurationSource() { @@ -23,9 +25,26 @@ CorsConfigurationSource corsConfigurationSource() { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("https://example.com") + configuration.allowedMethods = listOf("GET", "POST") + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source +} +---- +==== + The following will disable the CORS integration within Spring Security: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -35,3 +54,18 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http.build(); } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + cors { + disable() + } + } +} +---- +==== diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc index 542415651b..eed51eb691 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/rsocket.adoc @@ -14,7 +14,9 @@ You can find a few sample applications that demonstrate the code below: You can find a minimal RSocket Security configuration below: -[source,java] +==== +.Java +[source,java,role="primary"] ----- @Configuration @EnableRSocketSecurity @@ -32,6 +34,25 @@ public class HelloRSocketSecurityConfig { } ----- +.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 <> and sets up <> to require an authenticated user for any request. == Adding SecuritySocketAcceptorInterceptor @@ -86,7 +107,9 @@ 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] +==== +.Java +[source,java,role="primary"] ---- @Bean PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { @@ -101,17 +124,45 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { } ---- +.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 using `SimpleAuthenticationEncoder` which can be added to Spring's `RSocketStrategies`. -[source,java] +==== +.Java +[source,java,role="primary"] ---- RSocketStrategies.Builder strategies = ...; strategies.encoder(new SimpleAuthenticationEncoder()); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +var strategies: RSocketStrategies.Builder = ... +strategies.encoder(SimpleAuthenticationEncoder()) +---- +==== + It can then be used to send a username and password to the receiver in the setup: -[source,java] +==== +.Java +[source,java,role="primary"] ---- MimeType authenticationMimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); @@ -122,9 +173,24 @@ Mono requester = RSocketRequester.builder() .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.builder() + .setupMetadata(credentials, authenticationMimeType) + .rsocketStrategies(strategies.build()) + .connectTcp(host, port) +---- +==== + Alternatively or additionally, a username and password can be sent in a request. -[source,java] +==== +.Java +[source,java,role="primary"] ---- Mono requester; UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password"); @@ -138,6 +204,26 @@ public Mono findRadar(String code) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import org.springframework.messaging.rsocket.retrieveMono + +// ... + +var requester: Mono? = null +var credentials = UsernamePasswordMetadata("user", "password") + +open fun findRadar(code: String): Mono { + return requester!!.flatMap { req -> + req.route("find.radar.{code}", code) + .metadata(credentials, authenticationMimeType) + .retrieveMono() + } +} +---- +==== + [[rsocket-authentication-jwt]] === JWT @@ -147,7 +233,9 @@ The support comes in the form of authenticating a JWT (determining the JWT is va The RSocket receiver can decode the credentials using `BearerPayloadExchangeConverter` which is automatically setup using the `jwt` portion of the DSL. An example configuration can be found below: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { @@ -162,10 +250,28 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { } ---- +.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: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean ReactiveJwtDecoder jwtDecoder() { @@ -174,10 +280,23 @@ ReactiveJwtDecoder jwtDecoder() { } ---- +.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 just a simple String. For example, the token can be sent at setup time: -[source,java] +==== +.Java +[source,java,role="primary"] ---- MimeType authenticationMimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); @@ -187,9 +306,24 @@ Mono requester = RSocketRequester.builder() .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, the token can be sent in a request. -[source,java] +==== +.Java +[source,java,role="primary"] ---- MimeType authenticationMimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); @@ -205,6 +339,24 @@ public Mono findRadar(String code) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val authenticationMimeType: MimeType = + MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string) +var requester: Mono? = null +val token: BearerTokenMetadata = ... + +open fun findRadar(code: String): Mono { + return this.requester!!.flatMap { req -> + req.route("find.radar.{code}", code) + .metadata(token, authenticationMimeType) + .retrieveMono() + } +} +---- +==== + [[rsocket-authorization]] == RSocket Authorization @@ -212,7 +364,9 @@ RSocket authorization is performed with `AuthorizationPayloadInterceptor` which The DSL can be used to setup authorization rules based upon the `PayloadExchange`. An example configuration can be found below: -[[source,java]] +==== +.Java +[source,java,role="primary"] ---- rsocket .authorizePayload(authz -> @@ -227,6 +381,23 @@ rsocket .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 authority `ROLE_SETUP` <2> If the route is `fetch.profile.me` authorization only requires the user be authenticated <3> In this rule we setup a custom matcher where authorization requires the user to have the authority `ROLE_CUSTOM` diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc index 001d192dae..5849e2b624 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc @@ -7,7 +7,9 @@ For example, we can test our example from <> using the same setup and annotations we did in <>. Here is a minimal sample of what we can do: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @RunWith(SpringRunner.class) @ContextConfiguration(classes = HelloWebfluxMethodApplication.class) @@ -40,6 +42,41 @@ public class HelloWorldMessageServiceTests { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@RunWith(SpringRunner::class) +@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class]) +class HelloWorldMessageServiceTests { + @Autowired + lateinit var messages: HelloWorldMessageService + + @Test + fun messagesWhenNotAuthenticatedThenDenied() { + StepVerifier.create(messages.findMessage()) + .expectError(AccessDeniedException::class.java) + .verify() + } + + @Test + @WithMockUser + fun messagesWhenUserThenDenied() { + StepVerifier.create(messages.findMessage()) + .expectError(AccessDeniedException::class.java) + .verify() + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun messagesWhenAdminThenOk() { + StepVerifier.create(messages.findMessage()) + .expectNext("Hello World!") + .verifyComplete() + } +} +---- +==== + [[test-webtestclient]] == WebTestClientSupport @@ -75,7 +112,9 @@ public class HelloWebfluxMethodApplicationTests { After applying the Spring Security support to `WebTestClient` we can use either annotations or `mutateWith` support. For example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Test public void messageWhenNotAuthenticated() throws Exception { @@ -133,13 +172,63 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import org.springframework.test.web.reactive.server.expectBody + +//... + +@Test +@WithMockUser +fun messageWhenWithMockUserThenForbidden() { + this.rest.get().uri("/message") + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) +} + +@Test +@WithMockUser(roles = ["ADMIN"]) +fun messageWhenWithMockAdminThenOk() { + this.rest.get().uri("/message") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("Hello World!") + +} + +// --- mutateWith mockUser --- + +@Test +fun messageWhenMutateWithMockUserThenForbidden() { + this.rest + .mutateWith(mockUser()) + .get().uri("/message") + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) +} + +@Test +fun messageWhenMutateWithMockAdminThenOk() { + this.rest + .mutateWith(mockUser().roles("ADMIN")) + .get().uri("/message") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("Hello World!") +} +---- +==== + === CSRF Support Spring Security also provides support for CSRF testing with `WebTestClient`. For example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- this.rest // provide a valid CSRF token @@ -149,6 +238,18 @@ this.rest ... ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +this.rest + // provide a valid CSRF token + .mutateWith(csrf()) + .post() + .uri("/login") + ... +---- +==== + [[webflux-testing-oauth2]] === Testing OAuth 2.0 @@ -156,7 +257,9 @@ When it comes to OAuth 2.0, the same principles covered earlier still apply: Ult For example, for a controller that looks like this: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(Principal user) { @@ -164,11 +267,23 @@ public Mono foo(Principal user) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/endpoint") +fun foo(user: Principal): Mono { + return Mono.just(user.name) +} +---- +==== + There's nothing OAuth2-specific about it, so you will likely be able to simply <> and be fine. But, in cases where your controllers are bound to some aspect of Spring Security's OAuth 2.0 support, like the following: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(@AuthenticationPrincipal OidcUser user) { @@ -176,6 +291,16 @@ public Mono foo(@AuthenticationPrincipal OidcUser user) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal user: OidcUser): Mono { + return Mono.just(user.idToken.subject) +} +---- +==== + then Spring Security's test support can come in handy. [[webflux-testing-oidc-login]] @@ -186,36 +311,76 @@ Certainly this would be a daunting task, which is why Spring Security ships with For example, we can tell Spring Security to include a default `OidcUser` using the `SecurityMockServerConfigurers#oidcLogin` method, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOidcLogin()).get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOidcLogin()) + .get().uri("/endpoint") + .exchange() +---- +==== + What this will do is configure the associated `MockServerRequest` with an `OidcUser` that includes a simple `OidcIdToken`, `OidcUserInfo`, and `Collection` of granted authorities. Specifically, it will include an `OidcIdToken` with a `sub` claim set to `user`: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(user.getIdToken().getClaim("sub")).isEqualTo("user"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(user.idToken.getClaim("sub")).isEqualTo("user") +---- +==== + an `OidcUserInfo` with no claims set: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(user.getUserInfo().getClaims()).isEmpty(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(user.userInfo.claims).isEmpty() +---- +==== + and a `Collection` of authorities with just one authority, `SCOPE_read`: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(user.getAuthorities()).hasSize(1); assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read")); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(user.authorities).hasSize(1) +assertThat(user.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read")) +---- +==== + Spring Security does the necessary work to make sure that the `OidcUser` instance is available for <>. Further, it also links that `OidcUser` to a simple instance of `OAuth2AuthorizedClient` that it deposits into a mock `ServerOAuth2AuthorizedClientRepository`. @@ -228,7 +393,9 @@ In many circumstances, your method is protected by filter or method security and In this case, you can supply what granted authorities you need using the `authorities()` method: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOidcLogin() @@ -237,6 +404,17 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOidcLogin() + .authorities(SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-oidc-login-claims]] ==== Configuring Claims @@ -245,7 +423,9 @@ And while granted authorities are quite common across all of Spring Security, we Let's say, for example, that you've got a `user_id` claim that indicates the user's id in your system. You might access it like so in a controller: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(@AuthenticationPrincipal OidcUser oidcUser) { @@ -254,9 +434,22 @@ public Mono foo(@AuthenticationPrincipal OidcUser oidcUser) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal oidcUser: OidcUser): Mono { + val userId = oidcUser.idToken.getClaim("user_id") + // ... +} +---- +==== + In that case, you'd want to specify that claim with the `idToken()` method: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOidcLogin() @@ -265,6 +458,17 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOidcLogin() + .idToken { token -> token.claim("user_id", "1234") } + ) + .get().uri("/endpoint").exchange() +---- +==== + since `OidcUser` collects its claims from `OidcIdToken`. [[webflux-testing-oidc-login-user]] @@ -283,7 +487,9 @@ That last one is handy if you: For example, let's say that your authorization server sends the principal name in the `user_name` claim instead of the `sub` claim. In that case, you can configure an `OidcUser` by hand: -[source,java] +==== +.Java +[source,java,role="primary"] ---- OidcUser oidcUser = new DefaultOidcUser( AuthorityUtils.createAuthorityList("SCOPE_message:read"), @@ -295,6 +501,21 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val oidcUser: OidcUser = DefaultOidcUser( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + OidcIdToken.withTokenValue("id-token").claim("user_name", "foo_user").build(), + "user_name" +) + +client + .mutateWith(mockOidcLogin().oidcUser(oidcUser)) + .get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-oauth2-login]] === Testing OAuth 2.0 Login @@ -303,7 +524,9 @@ And because of that, Spring Security also has test support for non-OIDC use case Let's say that we've got a controller that gets the logged-in user as an `OAuth2User`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(@AuthenticationPrincipal OAuth2User oauth2User) { @@ -311,32 +534,72 @@ public Mono foo(@AuthenticationPrincipal OAuth2User oauth2User) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): Mono { + return Mono.just(oauth2User.getAttribute("sub")) +} +---- +==== + In that case, we can tell Spring Security to include a default `OAuth2User` using the `SecurityMockServerConfigurers#oauth2User` method, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOAuth2Login()) .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOAuth2Login()) + .get().uri("/endpoint").exchange() +---- +==== + What this will do is configure the associated `MockServerRequest` with an `OAuth2User` that includes a simple `Map` of attributes and `Collection` of granted authorities. Specifically, it will include a `Map` with a key/value pair of `sub`/`user`: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat((String) user.getAttribute("sub")).isEqualTo("user"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(user.getAttribute("sub")).isEqualTo("user") +---- +==== + and a `Collection` of authorities with just one authority, `SCOPE_read`: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(user.getAuthorities()).hasSize(1); assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read")); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(user.authorities).hasSize(1) +assertThat(user.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read")) +---- +==== + Spring Security does the necessary work to make sure that the `OAuth2User` instance is available for <>. Further, it also links that `OAuth2User` to a simple instance of `OAuth2AuthorizedClient` that it deposits in a mock `ServerOAuth2AuthorizedClientRepository`. @@ -349,7 +612,9 @@ In many circumstances, your method is protected by filter or method security and In this case, you can supply what granted authorities you need using the `authorities()` method: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOAuth2Login() @@ -358,6 +623,17 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOAuth2Login() + .authorities(SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-oauth2-login-claims]] ==== Configuring Claims @@ -366,7 +642,9 @@ And while granted authorities are quite common across all of Spring Security, we Let's say, for example, that you've got a `user_id` attribute that indicates the user's id in your system. You might access it like so in a controller: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(@AuthenticationPrincipal OAuth2User oauth2User) { @@ -375,9 +653,22 @@ public Mono foo(@AuthenticationPrincipal OAuth2User oauth2User) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): Mono { + val userId = oauth2User.getAttribute("user_id") + // ... +} +---- +==== + In that case, you'd want to specify that attribute with the `attributes()` method: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOAuth2Login() @@ -386,6 +677,17 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOAuth2Login() + .attributes { attrs -> attrs["user_id"] = "1234" } + ) + .get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-oauth2-login-user]] ==== Additional Configurations @@ -401,7 +703,9 @@ That last one is handy if you: For example, let's say that your authorization server sends the principal name in the `user_name` claim instead of the `sub` claim. In that case, you can configure an `OAuth2User` by hand: -[source,java] +==== +.Java +[source,java,role="primary"] ---- OAuth2User oauth2User = new DefaultOAuth2User( AuthorityUtils.createAuthorityList("SCOPE_message:read"), @@ -413,13 +717,30 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val oauth2User: OAuth2User = DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + mapOf(Pair("user_name", "foo_user")), + "user_name" +) + +client + .mutateWith(mockOAuth2Login().oauth2User(oauth2User)) + .get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-oauth2-client]] === Testing OAuth 2.0 Clients Independent of how your user authenticates, you may have other tokens and client registrations that are in play for the request you are testing. For example, your controller may be relying on the client credentials grant to get a token that isn't associated with the user at all: -[source,json] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) { @@ -430,41 +751,98 @@ public Mono foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2Author } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import org.springframework.web.reactive.function.client.bodyToMono + +// ... + +@GetMapping("/endpoint") +fun foo(@RegisteredOAuth2AuthorizedClient("my-app") authorizedClient: OAuth2AuthorizedClient?): Mono { + return this.webClient.get() + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono() +} +---- +==== + Simulating this handshake with the authorization server could be cumbersome. Instead, you can use `SecurityMockServerConfigurers#oauth2Client` to add a `OAuth2AuthorizedClient` into a mock `ServerOAuth2AuthorizedClientRepository`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOAuth2Client("my-app")) .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOAuth2Client("my-app")) + .get().uri("/endpoint").exchange() +---- +==== + What this will do is create an `OAuth2AuthorizedClient` that has a simple `ClientRegistration`, `OAuth2AccessToken`, and resource owner name. Specifically, it will include a `ClientRegistration` with a client id of "test-client" and client secret of "test-secret": -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(authorizedClient.getClientRegistration().getClientId()).isEqualTo("test-client"); assertThat(authorizedClient.getClientRegistration().getClientSecret()).isEqualTo("test-secret"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(authorizedClient.clientRegistration.clientId).isEqualTo("test-client") +assertThat(authorizedClient.clientRegistration.clientSecret).isEqualTo("test-secret") +---- +==== + a resource owner name of "user": -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(authorizedClient.getPrincipalName()).isEqualTo("user"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(authorizedClient.principalName).isEqualTo("user") +---- +==== + and an `OAuth2AccessToken` with just one scope, `read`: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(authorizedClient.getAccessToken().getScopes()).hasSize(1); assertThat(authorizedClient.getAccessToken().getScopes()).containsExactly("read"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(authorizedClient.accessToken.scopes).hasSize(1) +assertThat(authorizedClient.accessToken.scopes).containsExactly("read") +---- +==== + The client can then be retrieved as normal using `@RegisteredOAuth2AuthorizedClient` in a controller method. [[webflux-testing-oauth2-client-scopes]] @@ -473,7 +851,9 @@ The client can then be retrieved as normal using `@RegisteredOAuth2AuthorizedCli In many circumstances, the OAuth 2.0 access token comes with a set of scopes. If your controller inspects these, say like so: -[source,json] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) { @@ -488,9 +868,32 @@ public Mono foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2Author } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import org.springframework.web.reactive.function.client.bodyToMono + +// ... + +@GetMapping("/endpoint") +fun foo(@RegisteredOAuth2AuthorizedClient("my-app") authorizedClient: OAuth2AuthorizedClient): Mono { + val scopes = authorizedClient.accessToken.scopes + if (scopes.contains("message:read")) { + return webClient.get() + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono() + } + // ... +} +---- +==== + then you can configure the scope using the `accessToken()` method: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOAuth2Client("my-app") @@ -499,6 +902,17 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOAuth2Client("my-app") + .accessToken(OAuth2AccessToken(BEARER, "token", null, null, setOf("message:read"))) +) +.get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-oauth2-client-registration]] ==== Additional Configurations @@ -514,7 +928,9 @@ For example, let's say that you are wanting to use one of your app's `ClientRegi In that case, your test can autowire the `ReactiveClientRegistrationRepository` and look up the one your test needs: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Autowired ReactiveClientRegistrationRepository clientRegistrationRepository; @@ -528,6 +944,22 @@ client .get().uri("/exchange").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Autowired +lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + +// ... + +client + .mutateWith(mockOAuth2Client() + .clientRegistration(this.clientRegistrationRepository.findByRegistrationId("facebook").block()) + ) + .get().uri("/exchange").exchange() +---- +==== + [[webflux-testing-jwt]] === Testing JWT Authentication @@ -543,12 +975,22 @@ We'll look at two of them now: The first way is via a `WebTestClientConfigurer`. The simplest of these would look something like this: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockJwt()).get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockJwt()).get().uri("/endpoint").exchange() +---- +==== + What this will do is create a mock `Jwt`, passing it correctly through any authentication APIs so that it's available for your authorization mechanisms to verify. By default, the `JWT` that it creates has the following characteristics: @@ -566,18 +1008,31 @@ By default, the `JWT` that it creates has the following characteristics: And the resulting `Jwt`, were it tested, would pass in the following way: -[source,java] +==== +.Java +[source,java,role="primary"] ---- assertThat(jwt.getTokenValue()).isEqualTo("token"); assertThat(jwt.getHeaders().get("alg")).isEqualTo("none"); assertThat(jwt.getSubject()).isEqualTo("sub"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(jwt.tokenValue).isEqualTo("token") +assertThat(jwt.headers["alg"]).isEqualTo("none") +assertThat(jwt.subject).isEqualTo("sub") +---- +==== + These values can, of course be configured. Any headers or claims can be configured with their corresponding methods: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockJwt().jwt(jwt -> jwt.header("kid", "one") @@ -585,35 +1040,83 @@ client .get().uri("/endpoint").exchange(); ---- -[source,java] +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockJwt().jwt { jwt -> jwt.header("kid", "one") + .claim("iss", "https://idp.example.org") + }) + .get().uri("/endpoint").exchange() +---- +==== + +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockJwt().jwt(jwt -> jwt.claims(claims -> claims.remove("scope")))) .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockJwt().jwt { jwt -> + jwt.claims { claims -> claims.remove("scope") } + }) + .get().uri("/endpoint").exchange() +---- +==== + The `scope` and `scp` claims are processed the same way here as they are in a normal bearer token request. However, this can be overridden simply by providing the list of `GrantedAuthority` instances that you need for your test: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_messages"))) .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockJwt().authorities(SimpleGrantedAuthority("SCOPE_messages"))) + .get().uri("/endpoint").exchange() +---- +==== + Or, if you have a custom `Jwt` to `Collection` converter, you can also use that to derive the authorities: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockJwt().authorities(new MyConverter())) .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockJwt().authorities(MyConverter())) + .get().uri("/endpoint").exchange() +---- +==== + You can also specify a complete `Jwt`, for which `{security-api-url}org/springframework/security/oauth2/jwt/Jwt.Builder.html[Jwt.Builder]` comes quite handy: -[source,java] +==== +.Java +[source,java,role="primary"] ---- Jwt jwt = Jwt.withTokenValue("token") .header("alg", "none") @@ -626,12 +1129,29 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwt: Jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", "user") + .claim("scope", "read") + .build() + +client + .mutateWith(mockJwt().jwt(jwt)) + .get().uri("/endpoint").exchange() +---- +==== + ==== `authentication()` `WebTestClientConfigurer` The second way is by using the `authentication()` `Mutator`. Essentially, you can instantiate your own `JwtAuthenticationToken` and provide it in your test, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- Jwt jwt = Jwt.withTokenValue("token") .header("alg", "none") @@ -645,6 +1165,22 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", "user") + .build() +val authorities: Collection = AuthorityUtils.createAuthorityList("SCOPE_read") +val token = JwtAuthenticationToken(jwt, authorities) + +client + .mutateWith(mockAuthentication(token)) + .get().uri("/endpoint").exchange() +---- +==== + Note that as an alternative to these, you can also mock the `ReactiveJwtDecoder` bean itself with a `@MockBean` annotation. [[webflux-testing-opaque-token]] @@ -655,7 +1191,9 @@ To help with that, Spring Security has test support for opaque tokens. Let's say that we've got a controller that retrieves the authentication as a `BearerTokenAuthentication`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(BearerTokenAuthentication authentication) { @@ -663,32 +1201,72 @@ public Mono foo(BearerTokenAuthentication authentication) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/endpoint") +fun foo(authentication: BearerTokenAuthentication): Mono { + return Mono.just(authentication.tokenAttributes["sub"] as String?) +} +---- +==== + In that case, we can tell Spring Security to include a default `BearerTokenAuthentication` using the `SecurityMockServerConfigurers#opaqueToken` method, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOpaqueToken()) .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOpaqueToken()) + .get().uri("/endpoint").exchange() +---- +==== + What this will do is configure the associated `MockHttpServletRequest` with a `BearerTokenAuthentication` that includes a simple `OAuth2AuthenticatedPrincipal`, `Map` of attributes, and `Collection` of granted authorities. Specifically, it will include a `Map` with a key/value pair of `sub`/`user`: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat((String) token.getTokenAttributes().get("sub")).isEqualTo("user"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(token.tokenAttributes["sub"] as String?).isEqualTo("user") +---- +==== + and a `Collection` of authorities with just one authority, `SCOPE_read`: -[source,json] +==== +.Java +[source,java,role="primary"] ---- assertThat(token.getAuthorities()).hasSize(1); assertThat(token.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read")); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +assertThat(token.authorities).hasSize(1) +assertThat(token.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read")) +---- +==== + Spring Security does the necessary work to make sure that the `BearerTokenAuthentication` instance is available for your controller methods. [[webflux-testing-opaque-token-authorities]] @@ -698,7 +1276,9 @@ In many circumstances, your method is protected by filter or method security and In this case, you can supply what granted authorities you need using the `authorities()` method: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOpaqueToken() @@ -707,6 +1287,17 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOpaqueToken() + .authorities(SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-opaque-token-attributes]] ==== Configuring Claims @@ -715,7 +1306,9 @@ And while granted authorities are quite common across all of Spring Security, we Let's say, for example, that you've got a `user_id` attribute that indicates the user's id in your system. You might access it like so in a controller: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/endpoint") public Mono foo(BearerTokenAuthentication authentication) { @@ -724,9 +1317,22 @@ public Mono foo(BearerTokenAuthentication authentication) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/endpoint") +fun foo(authentication: BearerTokenAuthentication): Mono { + val userId = authentication.tokenAttributes["user_id"] as String? + // ... +} +---- +==== + In that case, you'd want to specify that attribute with the `attributes()` method: -[source,java] +==== +.Java +[source,java,role="primary"] ---- client .mutateWith(mockOpaqueToken() @@ -735,6 +1341,17 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +client + .mutateWith(mockOpaqueToken() + .attributes { attrs -> attrs["user_id"] = "1234" } + ) + .get().uri("/endpoint").exchange() +---- +==== + [[webflux-testing-opaque-token-principal]] ==== Additional Configurations @@ -749,7 +1366,9 @@ It's handy if you: For example, let's say that your authorization server sends the principal name in the `user_name` attribute instead of the `sub` attribute. In that case, you can configure an `OAuth2AuthenticatedPrincipal` by hand: -[source,java] +==== +.Java +[source,java,role="primary"] ---- Map attributes = Collections.singletonMap("user_name", "foo_user"); OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal( @@ -762,4 +1381,20 @@ client .get().uri("/endpoint").exchange(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val attributes: Map = mapOf(Pair("user_name", "foo_user")) +val principal: OAuth2AuthenticatedPrincipal = DefaultOAuth2AuthenticatedPrincipal( + attributes["user_name"] as String?, + attributes, + AuthorityUtils.createAuthorityList("SCOPE_message:read") +) + +client + .mutateWith(mockOpaqueToken().principal(principal)) + .get().uri("/endpoint").exchange() +---- +==== + Note that as an alternative to using `mockOpaqueToken()` test support, you can also mock the `OpaqueTokenIntrospector` bean itself with a `@MockBean` annotation. diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc index 104644e406..e3fee04d43 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc @@ -134,7 +134,9 @@ You can configure multiple `SecurityWebFilterChain` instances to separate config For example, you can isolate configuration for URLs that start with `/api`, like so: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Configuration @EnableWebFluxSecurity @@ -171,6 +173,46 @@ static class MultiSecurityHttpConfig { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableWebFluxSecurity +open class MultiSecurityHttpConfig { + @Order(Ordered.HIGHEST_PRECEDENCE) <1> + @Bean + open fun apiHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) <2> + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { } <3> + } + } + } + + @Bean + open fun webHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { <4> + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + httpBasic { } <5> + } + } + + @Bean + open fun userDetailsService(): ReactiveUserDetailsService { + return MapReactiveUserDetailsService( + PasswordEncodedUser.user(), PasswordEncodedUser.admin() + ) + } +} +---- +==== + <1> Configure a `SecurityWebFilterChain` with an `@Order` to specify which `SecurityWebFilterChain` Spring Security should consider first <2> Use `PathPatternParserServerWebExchangeMatcher` to state that this `SecurityWebFilterChain` will only apply to URL paths that start with `/api/` <3> Specify the authentication mechanisms that will be used for `/api/**` endpoints diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc index 694b905093..cb1c4e0a47 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc @@ -4,7 +4,9 @@ Similar to <>, reactive x509 authentication filter allows extracting an authentication token from a certificate provided by a client. Below is an example of a reactive x509 security configuration: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { @@ -17,11 +19,28 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + x509 { } + authorizeExchange { + authorize(anyExchange, authenticated) + } + } +} +---- +==== + In the configuration above, when neither `principalExtractor` nor `authenticationManager` is provided defaults will be used. The default principal extractor is `SubjectDnX509PrincipalExtractor` which extracts the CN (common name) field from a certificate provided by a client. The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager` which performs user account validation, checking that user account with a name extracted by `principalExtractor` exists and it is not locked, disabled, or expired. The next example demonstrates how these defaults can be overridden. -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { @@ -47,6 +66,30 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? { + val customPrincipalExtractor = SubjectDnX509PrincipalExtractor() + customPrincipalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)") + val customAuthenticationManager = ReactiveAuthenticationManager { authentication: Authentication -> + authentication.isAuthenticated = "Trusted Org Unit" == authentication.name + Mono.just(authentication) + } + return http { + x509 { + principalExtractor = customPrincipalExtractor + authenticationManager = customAuthenticationManager + } + authorizeExchange { + authorize(anyExchange, authenticated) + } + } +} +---- +==== + In this example, a username is extracted from the OU field of a client certificate instead of CN, and account lookup using `ReactiveUserDetailsService` is not performed at all. Instead, if the provided certificate issued to an OU named "Trusted Org Unit", a request will be authenticated. For an example of configuring Netty and `WebClient` or `curl` command-line tool to use mutual TLS and enable X.509 authentication, please refer to https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/anonymous.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/anonymous.adoc index b63edecba9..8cb386a153 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/anonymous.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/anonymous.adoc @@ -108,7 +108,9 @@ https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc This means that a construct like this one: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/") public String method(Authentication authentication) { @@ -120,6 +122,20 @@ public String method(Authentication authentication) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun method(authentication: Authentication?): String { + return if (authentication is AnonymousAuthenticationToken) { + "anonymous" + } else { + "not anonymous" + } +} +---- +==== + will always return "not anonymous", even for anonymous requests. The reason is that Spring MVC resolves the parameter using `HttpServletRequest#getPrincipal`, which is `null` when the request is anonymous. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc index d89093e824..0b5f1ccc72 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc @@ -820,12 +820,22 @@ class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { Or similarly with method security: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @PreAuthorize("hasAuthority('SCOPE_messages')") public List getMessages(...) {} ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasAuthority('SCOPE_messages')") +fun getMessages(): List { } +---- +==== + [[oauth2resourceserver-jwt-authorization-extraction]] ==== Extracting Authorities Manually diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/test/method.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/test/method.adoc index c5b94df84f..82a2f9ceb9 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/test/method.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/test/method.adoc @@ -4,7 +4,9 @@ This section demonstrates how to use Spring Security's Test support to test method based security. We first introduce a `MessageService` that requires the user to be authenticated in order to access it. -[source,java] +==== +.Java +[source,java,role="primary"] ---- public class HelloMessageService implements MessageService { @@ -17,6 +19,19 @@ public class HelloMessageService implements MessageService { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class HelloMessageService : MessageService { + @PreAuthorize("authenticated") + fun getMessage(): String { + val authentication: Authentication = SecurityContextHolder.getContext().authentication + return "Hello $authentication" + } +} +---- +==== + The result of `getMessage` is a String saying "Hello" to the current Spring Security `Authentication`. An example of the output is displayed below.