0

I have a Spring Boot microservice application generated using JHipster with Keycloak. Below are the versions for the application:

  • JHipster - 7.9.3
  • Spring Boot - 3.0.2
  • Spring Cloud - 2022.0.1
  • Keycloak - 20.0.3

I had manually updated the Spring Boot version from the one generated by JHipster.

The security configuration is as follows:

@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {

    private final JHipsterProperties jHipsterProperties;

    @Value("${spring.security.oauth2.client.provider.oidc.issuer-uri}")
    private String issuerUri;

    private final SecurityProblemSupport problemSupport;

    public SecurityConfiguration(JHipsterProperties jHipsterProperties, SecurityProblemSupport problemSupport) {
        this.problemSupport = problemSupport;
        this.jHipsterProperties = jHipsterProperties;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .csrf()
            .disable()
            .exceptionHandling()
                .authenticationEntryPoint(problemSupport)
                .accessDeniedHandler(problemSupport)
        .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeHttpRequests()
            .requestMatchers("/api/authenticate").permitAll()
            .requestMatchers("/api/auth-info").permitAll()
            .requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .requestMatchers("/api/**").authenticated()
            .requestMatchers("/management/health").permitAll()
            .requestMatchers("/management/health/**").permitAll()
            .requestMatchers("/management/info").permitAll()
            .requestMatchers("/management/prometheus").permitAll()
            .requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
        .and()
            .oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(authenticationConverter())
                .and()
            .and()
                .oauth2Client();
        return http.build();
        // @formatter:on
    }

    Converter<Jwt, AbstractAuthenticationToken> authenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthorityConverter());
        return jwtAuthenticationConverter;
    }

    @Bean
    JwtDecoder jwtDecoder() {
        NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }
}

The security related application properties are:

spring:
    security:
        oauth2:
          resource:
              filter-order: 3
          client:
            provider:
              oidc:
                issuer-uri: http://localhost:8080/realms/samplerealm
            registration:
              oidc:
                authorization-grant-type: client_credentials
                client-id: microservice-client
                client-secret: <VALID_CLIENT_SECRET>
                scope: openid, profile, email, offline_access # last one for refresh tokens

With these configurations, the application is listening on localhost:8087 for HTTP requests.

I created another client in Keycloak dev-client and using Postman to test the application API. I acquired an access token from Keycloak using this client and used the access token in Postman in the Authorization header (Bearer ----access token----). Even with this valid token, the API forwards me to localhost:8087/login with an HTML page response:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet"
        crossorigin="anonymous" />
</head>

<body>
    <div class="container">
        <h2 class="form-signin-heading">Login with OAuth 2.0</h2>
        <table class="table table-striped">
        </table>
    </div>
</body>

</html>

Here is a snapshot of Postman console (the snapshot is cropped because of the length of the access token)

enter image description here

I am not sure why are the requests being redirected/forwarded to localhost:8087/login even if I have provided a valid access token. I have tried poviding an access token which is acquired using password grant with a different client but it still gave me the same result.

Any HTTP requests to the application gets forwarded to localhost:8087/login, so far I tried GET request and it is throwing me this issue.

dur
  • 15,689
  • 25
  • 79
  • 125

1 Answers1

1

Keep the Default JwtDecoder

You are using boot. Don't override JwtDecoder for just validating issuer and audience. Define boot properties instead.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          audiences:
          - http://localhost:8087
          issuer-uri: http://localhost:8080/realms/samplerealm

There are a few good reasons for that:

  • if spring team figures out that the implementation to use should be switched or that some configuration should be added, (for performance, security or whatever reason) you won't benefit it by just bumping versions.
  • configuration will be easier to read and understand (like: "nothing special regarding JWT decoder, just the standard recommended stuff")
  • if you define a new bean you should unit-test it. Are you sure you master enough the specs of what a JWT decoder should do for that?

UI Client V.S. Resource-Server

As a reminder, clients consume resources when resource-servers serve it.

Here I make a distinction between "UI" (or BFF like spring-cloud-gateway configured as OAuth2 client) and "REST" clients. the first serve what users see and trigger login and logout on the authization-server. REST clients might be used as well on "UI" clients and resource-server to consume a resource from a resource-server. Some specific security filter-chain config is required for the first (UI) but not for the second (REST client) which usually either use the access-token already in the security context to issue a request on behalf of the user or acquires a new access-token (with client credentials) to issue a request in its own name.

Security concerns are different enough for spring to provide WIth different libs. For instance, resource-server can frequently be stateless (no session, the state is associated to the token) when UI clients generally need a session for the user.

Don't mix UI client and resource-server config in the same security filter-chain. If you need the two in the same application, create separate filter chains with order and securityMatcher for the first to intercept only the routes it should and let the second act as fallback. More details on that subject in "Use Keycloak Spring Adapter with Spring Boot 3"

Here you seem to have only REST endpoints => remove client configuration from your security filter-chain (.oauth2Client() and also be sure you don't have oauth2Login which is a client concern).

Be Lean

Unless you use a REST client autoconfigured by spring-boot (WebClient, @FeignClient, RestTemplate, ...) to fetch resources from another micro-service, also remove spring.security.oauth2.resource.client properties and spring-boot-starter-oauth2-client.

Removing useless dependencies, properties and Java configuration will make your project easier to maintain, easier to debug, have a smaller footprint on the hardware and easier to grasp for new developers.

Tutorials for Various OAuth2 Use-Cases

On this repo of mine: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials

ch4mp
  • 6,622
  • 6
  • 29
  • 49
  • Most of this answer is opinionated as in ”dont do this” but very little reasoning to why. There is no one that states that declaring this in the yaml instead of code is ”better than the orher” more than opinions – Toerktumlare Feb 08 '23 at 12:52
  • `So, why is the opinion that "less code is better" so widely spread?` read my comment, this is never mentioned. My comment says, doing it one or the other way works. Its opinionated what one prefers. My comment addresses that you say that people shouldn’t, when even spring themselves have no opinion on the subject. – Toerktumlare Feb 09 '23 at 21:07
  • And the 3 points I added are just opinions to you? – ch4mp Feb 09 '23 at 21:51
  • and your 3 points were added after the comment, the second bullet point is still an opinion. I can give you a negative reason for having an issuer in a setting. If i gain access to your server, i can change the issuer and restart the service. – Toerktumlare Feb 09 '23 at 22:55
  • Not your second one – ch4mp Feb 09 '23 at 22:56
  • 1
    im not going to have a discussion in the comment section (and this is the reason why people dont explain downvotes, because people want to have discussions) – Toerktumlare Feb 09 '23 at 22:59
  • So you are seriously suggesting to hardcode the issuer and the audience in Java code? Because if it's in the properties, being boot auto-config or explicit configuration does not change a thing to the attack vector you mention. Plus if one gets enough accès to set config and restart services, he might be able to do about anything. – ch4mp Feb 09 '23 at 23:19