close
close

spring boot – How to run a test twice for an authenticated and unauthenticated user?

spring boot – How to run a test twice for an authenticated and unauthenticated user?

First, you probably don’t want to run the same test twice (different expectations due to different preconditions should not be part of “the same test”).

To repeat the exact same test, use @RepetedTest.

To change preconditions and maintain expectations across runs, use @ParameterizedTest.

By using SecurityMockMvcRequestPostProcessors.oauth2Login()you fill the test security context with a OAuth2AuthenticationToken example. This simulates a successful login of a user using the authorization code flow on a Spring client (oauth2Login), and neither an anonymous request nor a REST request sent by an OAuth2 client to a resource server and authorized with a valid Bearer token.

Also, you will never get a 403 with the code you show:

  • Unauthorized requests will be responded to 302 (redirect to login) because of oauth2Login.
  • authorized requests will never arrive 403 forbidden because .requestMatchers("/api/**").authenticated() says that being authenticated is sufficient, which is satisfied as soon as SecurityMockMvcRequestPostProcessors.oauth2Login() is used.

Do not mix oauth2Login And oauth2ResourceServer

This is a common mistake, but here are some reasons why the two are not compatible:

  • A resource server can (and should) be stateless (no session), where a client with oauth2Login requires sessions (to run the authorization code flow and store tokens).
  • CSRF protection should be disabled only on resource servers (as it is stateless).
  • The security context is not populated with the same type of Authentication :
    • JwtAuthenticationToken by default for oauth2ResourceServer with a JWT decoder (can be modified as desired by configuring the authentication converter)
    • BearerTokenAuthentication by default for oauth2ResourceServer access token introspection (can be changed as desired by configuring a (Reactive)OpaqueTokenAuthenticationConverter)
    • OAuth2AuthenticationToken For oauth2Login (still, but with a different principal type depending on whether the authorization server is OIDC or not)
  • The user data source for building the Authentication changes (and the mapping of authorities too):
    • oauth2Login uses the ID token (if the authorization server is OIDC) or the userinfo period
    • oauth2ResourceServer uses the access token payload (if that token is a JWT and the resource server is configured with a JWT decoder) or the introspection endpoint
  • It is generally expected that a customer with oauth2Login that he returns a 302 Redirect to login to an anonymous request to protected resources and to resource servers that it returns 401 Unauthorized.

For this reason, when an application needs to expose both oauth2Login (e.g. server-side rendered applications with Thymeleaf or OAuth2 BFFs) and REST endpoints enabled with Bearer tokens, it should expose two different Security(Web)FilterChain beans (with different @Order and the one who comes first with a securityMatcher).

Fix Access Control

401 (not allowed) or 302 (redirect to login) means that the authorization request failed (no authorization information, invalid or expired token, session expired, user has not logged in yet, …). These are the statuses you can get when you use authenticated(). Whether you get one or the other depends on how the filter chain is configured. Again, by default, it is 401 with oauth2ResourceServer And 302 with oauth2Login.

403 (forbidden) means that a request is successfully authorized (valid session, Bearer token, or any other security element based on it) but is not allowed to perform the requested action. This status can be the result of hasAuthority(), hasRole()and the like.

Once you have your setup figured out, you need to know what type of Authentication you need in the security context of testing for a given request:

  • probably JwtAuthenticationToken in the case of a request processed by a filter chain with oauth2ResourceServer
  • OAuth2AuthenticationToken in the case of a user successfully authenticated with oauth2Login
  • AnonymousAuthenticationToken in case of unauthorized request (unauthorized session or missing/invalid Bearer token)

You can simulate an unauthorized request by decorating the test with:

  • @WithAnonymousUser
  • SecurityMockMvcRequestPostProcessors.anonymous()

For OAuth2AuthenticationToken And JwtAuthenticationTokenyou can use:

  • THE SecurityMockMvcRequestPostProcessors for OAuth2: oidcLogin(), oauth2Login(), opaqueToken()And jwt()
  • annotations from a library I wrote:
    • @WithMockAuthentication:simulate any type of Authentication
    • @WithJwt: simulate a JwtAuthenticationToken by providing its JSON payload
    • @WithOidcLogin Or @WithOAuth2Login depending on how you have configured the oauth2Login authorization server as OpenID provider or not

When the authentication converter returns something other than one of the default values Authentication types or when testing access control in other components than @(Rest)Controller (it is possible to use the security method on a @Service Or @Repository for example), only “my” annotations work.

“My Library” also comes with tools to run @ParameterizedTest with different Authentication instances in the security context. See the repository source code for examples (search for @ParameterizedTest in the source).