6

I am using a setup with Keycloak as Identity Provider, Spring Cloud Gateway as API Gateway and multiple Microservices. I can receive a JWT via my Gateway (redirecting to Keycloak) via http://localhost:8050/auth/realms/dev/protocol/openid-connect/token.

I can use the JWT to access a resource directly located at the Keycloak server (e.g. http://localhost:8080/auth/admin/realms/dev/users). But when I want to use the Gateway to relay me to the same resource (http://localhost:8050/auth/admin/realms/dev/users) I get the Keycloak Login form as response.

My conclusion is that there must me a misconfiguration in my Spring Cloud Gateway application.

This is the Security Configuration in the Gateway:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) {

        // Authenticate through configured OpenID Provider
        http.oauth2Login();

        // Also logout at the OpenID Connect provider
        http.logout(logout -> logout.logoutSuccessHandler(
                new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));

        //Exclude /auth from authentication
        http.authorizeExchange().pathMatchers("/auth/realms/ahearo/protocol/openid-connect/token").permitAll();

        // Require authentication for all requests
        http.authorizeExchange().anyExchange().authenticated();

        // Allow showing /home within a frame
        http.headers().frameOptions().mode(Mode.SAMEORIGIN);

        // Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
        http.csrf().disable();
        return http.build();
    }
}

This is my application.yaml in the Gateway:

spring:
  application:
    name: gw-service
  cloud:
    gateway:
      default-filters:
        - TokenRelay
      discovery:
        locator:
          lower-case-service-id: true
          enabled: true
      routes:
        - id: auth
          uri: http://localhost:8080
          predicates:
            - Path=/auth/**

  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: 'api-gw'
            client-secret: 'not-relevant-but-correct'
            authorizationGrantType: authorization_code
            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
            scope: openid,profile,email,resource.read
        provider:
          keycloak:
            issuerUri: http://localhost:8080/auth/realms/dev
            user-name-attribute: preferred_username

server:
  port: 8050
eureka:
  client:
    service-url:
      default-zone: http://localhost:8761/eureka
    register-with-eureka: true
    fetch-registry: true

How can I make the Gateway able to know that the user is authenticated (using the JWT) and not redirect me to the login page?

Felix
  • 117
  • 3
  • 11

3 Answers3

5

If you want to make requests to Spring Gateway with access token you need to make it a resource server. Add the following:

pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

application.yml

  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://.../auth/realms/...

SecurityConfiguration.java

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                        ReactiveClientRegistrationRepository clientRegistrationRepository) {
    // Authenticate through configured OpenID Provider
    http.oauth2Login();
    // Also logout at the OpenID Connect provider
    http.logout(logout -> logout.logoutSuccessHandler(
            new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
    // Require authentication for all requests
    http.authorizeExchange().anyExchange().authenticated();

    http.oauth2ResourceServer().jwt();

    // Allow showing /home within a frame
    http.headers().frameOptions().mode(Mode.SAMEORIGIN);
    // Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
    http.csrf().disable();
    return http.build();
}
Dmitri Ciornii
  • 119
  • 2
  • 4
  • Could you please guide me here - https://github.com/SaiUpadhyayula/springboot-microservices-project/issues/2? Please see very last comments – PAA Jun 02 '21 at 14:47
0

I bypassed the problem by communicating directly with Keycloak without relaying requests to it via Spring Cloud Gateway.

That's actually not a workaround but actually best practice/totally ok as far as I understand.

Felix
  • 117
  • 3
  • 11
  • No need for code, just directly address your Keycloak server. So instead of https://api.domain.com (to let the API redirect your request to Keycloak) use https://keycloak.domain.com (to directly send the request to the Keycloak instance). – Felix Jun 07 '21 at 06:23
  • Can you please share how did you do that, please? Thank you – Jonny Aug 31 '22 at 14:46
0

This code is for Client_credentials grant_type. if you use other grant type you need to add client_id and client_secret in request parameters.

public class MyFilter2 extends OncePerRequestFilter {

    private final ObjectMapper mapper = new ObjectMapper();

    @Value("${auth.server.uri}")
    private String authServerUri;

    @Value("${client_id}")
    private String clientId;
    @Value("${client_secret}")
    private String clientSecret;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
            FilterChain filterChain) throws IOException {
        try {
            String token = httpServletRequest.getHeader("Authorization");

            HttpHeaders headers = new HttpHeaders();
            headers.set("Content-Type","application/x-www-form-urlencoded");
            headers.set("Authorization",token);

            final HttpEntity finalRequest = new HttpEntity("{}", headers);
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> response = restTemplate.postForEntity(authServerUri,finalRequest,String.class);
            if (!HttpStatus.OK.equals(response.getStatusCode())) {
                Map<String, Object> errorDetails = new HashMap<>();
                errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
                errorDetails.put("message", "Invalid or empty token");

                httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
                httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

                mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
            } else {
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
            }
        }catch(HttpClientErrorException he) {
            Map<String, Object> errorDetails = new HashMap<>();
            errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
            errorDetails.put("message", "Invalid or empty token");

            httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

            mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
        }catch (Exception exception) {
    }
}
David Buck
  • 3,752
  • 35
  • 31
  • 35