Thursday, May 13, 2021

Zuul + Eureka + Oauth2 security + Keycloak + Spring boot

 Here is a way to secure your Zuul Gateway with Oauth2 security. 
I had a hard time finding documentation on how to do that. As usual, there are bits and pieces all over the web but never a concrete solution:

Here is the pom.xml for the Zuul microservice:
Zuul is now deprecated in favor of the Spring cloud-gateway. I believe those are the most recent versions you can use safely.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.10.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nes</groupId>
    <artifactId>my-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>my-gateway</name>
    <description>Zuul proxy</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR11</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${build.directory}/classes/static/</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>../angular-client/dist</directory>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

The security config class:

package com.nes.gateway.security; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${keycloak.uri}") protected String keycloakUri; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests(authorizeRequests -> authorizeRequests .anyRequest().authenticated() // Any requests other than the one we ignore should have a valid Keycloak token. ).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/auth/realms/IzoaKeycloak/protocol/openid-connect/token"); } @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withJwkSetUri(keycloakUri).build();

The zuul application.yml
server:
  port : 8085
  ssl:
      enabled: true
      #clientAuth: need
      key-store: classpath:ssl/zuul-truststore.p12
      key-store-password: zuulpass
      #keyAlias: zuul-proxy
      keyStoreType: PKCS12  
      trust-store: classpath:ssl/zuul-truststore.p12
      trust-store-password: zuulpass
      trust-store-type: PKCS12 

spring:
    application:
      name: gateway 
      
keycloak:
     uri: http://keycloak:8080/auth/realms/IzoaKeycloak/protocol/openid-connect/certs      

eureka:
    client:
        fetchRegistry: true
        registerWithEureka: true
        serviceUrl:
            defaultZone: http://discovery:8761/eureka
    instance:
        preferIpAddress: true  

zuul:
  sslHostnameValidationEnabled: true 
  #okhttp:
    #enabled: true
  host:
    connect-timeout-millis: 90000
    socket-timeout-millis: 90000
    
  routes: 
    myService:
      serviceId: myService
      path: /myService/**
      sensitiveHeaders: Cookie,Set-Cookie
      strip-prefix: false
 
    oauth:
      path: /auth/**
      url: http://keycloak:8080/auth/

logging:
  level:
    ROOT: INFO
    org.springframework.web: INFO
    com.netflix: DEBUG

    
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 160000000           

ribbon:
  ReadTimeout: 5000000
  ConnectTimeout: 5000000
      

        
    
It uses the serviceId provided by Eureka (no port needed as the Spring framework knows where the route is located).

Now the pom.xml for the Discovery Service:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nes</groupId>
    <artifactId>discovery-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>discovery-server</name>
    <description>Eureka discovery server</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.1</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

and the application.yml file:

server:
  port: 8761
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
The Keycloak server setup is another story, plenty of good tutorials out there.
That should be enough to get you started and get your stack running.

No comments:

Post a Comment