Secure Your Spring Boot API with Keycloak: A Comprehensive Guide(2024)
Tags: springboot, springsecurity, keycloak, microservices
Published: 2024-02-10 16:20:19

This comprehensive guide walks you through integrating Keycloak, a powerful open-source identity and access management (IAM) solution, with your Spring Boot API for robust security. Keycloak simplifies adding authentication and authorization, granting authorized users access to your protected API endpoints.

## Prerequisites

- Java Development Kit (JDK): Installed on your machine https://www.oracle.com/java/technologies/downloads/.

- Maven (Optional): Recommended for dependency management https://maven.apache.org/download.cgi.

- Keycloak Server: Set up locally or use a hosted instancehttps://www.keycloak.org/downloads. You can use Docker with this command:

docker run -p 8182:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:23.0.6 start-dev
 
## Setting Up Keycloak

- Download and Install Keycloak: Visit the Keycloak website to download and install Keycloak on your machine or use a hosted instance Or visit the port where your keycloak docker image is running. Like for our case it is on http://localhost:8182

- Create a Realm: Log in to the Keycloak admin console and create a new realm. For this tutorial, let's name it spring-boot-microservices-realm.

- Create a Client: Within the realm, create a new client for your Spring Boot application. Set the client protocol to openid-connect, and specify the valid redirect URIs.

- Define Roles: Define roles for your application users. For example, let's create admin_role and client_role.

## Integrating Keycloak with Spring Boot

- Add Keycloak Dependencies: In your Spring Boot project's pom.xml, add the necessary dependencies for Keycloak integration.

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

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
```

- Configure application.yml: Configure Keycloak settings in your application.yml file, specifying the issuer URI and JWK set URI.

```
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
            issuer-uri: "http://localhost:8182/realms/spring-boot-microservices-realm"
            jwk-set-uri: " ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs"

server:
  port: 8041

jwt:
  auth:
    converter:
      resource-id: "spring-rest-api"
      principle-attribute: "preferred_username"
```


## Secure Your API Endpoints(Inside SecurityConf class)

- Create Security Configuration: Define security configurations in your Spring Boot application to secure API endpoints.

```
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConf { 

  private final JwtAuthConverter jwtAuthConverter;

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
      .csrf(AbstractHttpConfigurer::disable)
      .cors(AbstractHttpConfigurer::disable)
      .authorizeHttpRequests(req -> req.requestMatchers("/api/v1/")
        .permitAll()
        .anyRequest()
        .authenticated())
      .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .oauth2ResourceServer(oauth2 -> oauth2
        .jwt(jwt -> jwt
          .jwtAuthenticationConverter(jwtAuthConverter)
        )).build();
  }

}

```

## Extracting claims from the JWT token(inside JWTAuthConverter class)

```
@Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {

  private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
    new JwtGrantedAuthoritiesConverter();

  @Value("${jwt.auth.converter.principle-attribute}")
  private String principleAttribute;
  @Value("${jwt.auth.converter.resource-id}")
  private String resourceId;

  @Override
  public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
    Collection<GrantedAuthority> authorities =
      Stream.concat(
          jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
          extractResourceRoles(jwt).stream()
        )
        .collect(Collectors.toSet());
    return new JwtAuthenticationToken(
      jwt,
      authorities,
      getPrincipleClaimName(jwt)
    );
  }

  private String getPrincipleClaimName(Jwt jwt) {
    String claimName = JwtClaimNames.SUB;
    if (principleAttribute != null) {
      claimName = principleAttribute;
    }
    return jwt.getClaim(claimName);
  }

  private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
    Map<String, Object> resourceAccess;
    Map<String, Object> resources;
    Collection<String> resourceRoles;

    if (jwt.getClaim("resource_access") == null) {
      return Set.of();
    }
    resourceAccess = jwt.getClaim("resource_access");

    if (resourceAccess.get(resourceId) == null) {
      return Set.of();
    }

    resources = (Map<String, Object>) resourceAccess.get(resourceId);
    resourceRoles = (Collection<String>) resources.get("roles");

    return resourceRoles.stream()
      .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
      .collect(Collectors.toSet());
  }

  @Override
  public <U> Converter<Jwt, U> andThen(Converter<? super AbstractAuthenticationToken, ? extends U> after) {
    return Converter.super.andThen(after);
  }
}

```

## Demo controller for testing our apis(inside DemoController class)

```

@RestController
@RequestMapping("/api/v1/demo")
public class DemoController {

  @GetMapping
  @PreAuthorize("client_role")
  public String hello(){
    return "Hello from spring boot and keyclaok";
  }
  @PreAuthorize("admin_role")

  @GetMapping("/hello2")
  public String hello2(){
    return "authorized hello";
  }

}

```

## Conclusion

In this tutorial, we've learned how to integrate Keycloak with a Spring Boot application to secure API endpoints using JWT tokens. By following the steps outlined above, you can ensure that your Spring Boot APIs are protected and accessible only to authorized users based on their assigned roles in Keycloak. This approach provides a robust security mechanism for your applications, enabling you to focus on building features rather than worrying about authentication and authorization concerns.

Feel free to experiment further with additional Keycloak features such as user federation, fine-grained access control, and social login integration to tailor authentication and authorization according to your application's requirements.