Monday, November 11, 2024

Spring Boot Microservice + API Gateway + OAuth2 + Keycloak Server

In this tutorial we'll see how to secure your Spring Boot microservices using Keycloak as Identity and access management server, OUTH2 for authorization and OpenID Connect which verifies the identity of users based on the authentication performed by an authorization server. Rather than securing individual microservices it is better to perform security at the API gateway level, making gateway as a single implementation point to provide cross cutting concerns to all the microservices such as security.

Using Keycloak

By using Keycloak you can have a separate authorization server for authentication and authorization, which means keeping your security logic separate from business logic and delegating all the security related tasks to the Keycloak server which acts as an IAM.

Install keycloak

Convenient way to use keycloak is to run it in a Docker container by using the following command. You can read more about it here.

docker run -d -p 7080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.0.5 start-dev

Exposed port is kept as 7080 as 8080 is usually already in use if you are using Spring Boot REST API with services deployed in Tomcat.

Username and password both are given as admin.

You can also download and install it in your local system. Get more information about it here.

Start admin console

Once started you can access Keycloak using the URL- http://localhost:7080. It takes you to the login page where you can login using the credentials as admin. Once logged in you are directed to the admin console.

Here you need to configure Keycloak to perform authentication and authorization. Note that with this setup the configuration done is temporary (kept in memory) which will be lost once you logout. To persist your configuration, you can use a DB server to store it.

Create realm

First thing is to create a realm, on the left-hand side you should see a dropdown with master as the current realm. Click the dropdown and then select "Create realm".

keycloak realm

Realm helps in grouping applications and users with in a realm and keeping it isolated. Keycloak suggests not to create clients and users with in Master realm. Use Master realm only for managing Keycloak not any other application. So, create a new realm and give a Realm name, I have given name as microservice-realm.

Once realm is created that becomes the default realm.

Create client

In order for an application or service to utilize Keycloak it has to register a client in Keycloak. Click on "Clients" option from the menu and select client type as "Open ID Connect".

For Client ID give a name for your client application. Same way enter name and description.

keycloak client

Click next.

Ensure client authentication is turned on. In the authentication flow select "Standard Flow" which is Authorization Code Grant type. Uncheck any other authentication flow option.

keycloak client flow

Click next.

The Valid redirect URIs, is a valid URI, browser can redirect to after a successful login. Since we are going to use Postman and there is no UI application as such so for now you can use * to mean any URI. For testing purpose you can use "*" as value for POst Logout Redirect URI and Web Origins.

keycloak client URI

Click save to save the new client.

If you go to the credentials tab you should be able to see how client is authenticated and what is client secret. We'll need the client's name and secret when trying to access the REST endpoint.

keycloak client credentials

Create user with password

Select Users from the left-hand side section and then click on create new user. User is needed to authenticate against the Keycloak server.

With in the general section give value for Username and email. I have given as test_user and testuser@test.com. Then give values for First name and Last name. Also set the Email Verified to ON signifying that the user is already verified. Click on Create.

keycloak user

Select Credentials to set password for the created User. Enter value for password and set temporary to off so that you are not asked to change password later.

keycloak user password

Same way create another user test_admin with its own password.

Create realm roles

Select Realm roles from the left-hand side section and enter values for the role. I have created two roles user and admin.

Mapping role with user

Go back to users and select test_user. Click on "Role Mapping" tab and then click "Assign role" button.

keycloak user role

In the filter select filter by realm roles. Select "user" role for test_user.

Same way select "admin" role for test_admin user.

With that our Keycloak configuration is complete. Just to reiterate what we have done is-

  1. Created a realm.
  2. Created a client with client secret.
  3. Created a user.
  4. Created roles which are then mapped with the user.

Keycloak Authorization Code Grant Type flow

Oauth 2.0 specification defines several grant types which refers to the way IDToken/Access Token is granted. Some of the grant types are-

  1. Implicit Grant
  2. Authorization Code Grant
  3. Client Credentials Grant
  4. Device Authorization Grant

In this example we are using the Authorization Code Grant type which is optimized for confidential clients (Web Applications) and it is a redirection-based flow. In authorization code grant type first an authorization code is issued by authorization server using which an access token is requested from the authorization server.

OAuth roles in this scenario

OAuth specification defines the following roles-

  1. Resource server- It is the server that hosts the protected resources. Resource server accepts and responds to protected resource requests using access tokens.
  2. Client- It is an application which makes protected resource requests on behalf of the resource owner and with its authorization.
  3. Authorization server- The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.
  4. Resource owner- Resource owner (typically an end-user) owns the resource and capable of granting access to a protected resource.

In our example gateway server acts as both client and resource server, Keycloak is the Authorization server.

Auth code flow

Code changes

Refer this post Spring Boot Microservice + API Gateway + Resilience4J for getting the code to configure API Gateway in Microservices using Spring Cloud Gateway and the other microservices. We'll be adding security layer on top of that.

Adding dependencies

Need to add the following 3 Spring Boot starters for security in the gateway service.

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

Changes in configuration

The Spring OAuth2 Resource Server need to verify incoming JWT tokens for that we need to configure the JSON Web Key Set (JWKS) endpoint. With in the gateway-service open the application.yml and add the following configuration.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: "http://localhost:7080/realms/microservice-realm/protocol/openid-connect/certs"  

You can get jwk-set-uri by going to Keycloak admin console, there go to "Realm Settings" and all the way down in the displayed screen. There click on OpenID Endpoint Configuration. There take the value for the jwks_uri property.

Adding Security configuration class

Create a Configuration class annotated with @Configuration because we need to declare @Bean methods. Also use @EnableWebFluxSecurity to add Spring Security WebFlux support. WebFlux because Spring Cloud Gateway is built using the reactive version of the Spring web module.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;

import reactor.core.publisher.Mono;

import org.springframework.core.convert.converter.Converter;

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
  @Bean
  SecurityWebFilterChain filterChain(ServerHttpSecurity httpSecurity) throws Exception {
    httpSecurity.authorizeExchange((exchanges) -> exchanges
                .pathMatchers(HttpMethod.POST, "/customer").hasRole("admin")
                .pathMatchers("/account/**").authenticated()
        .pathMatchers("/customer/**").authenticated())
        .oauth2ResourceServer(oAuth2 -> oAuth2
                        .jwt(jwtSpec -> jwtSpec.jwtAuthenticationConverter(grantedAuthoritiesExtractor())));
    httpSecurity.csrf(ServerHttpSecurity.CsrfSpec::disable);
    
    return httpSecurity.build();
    
  }
  
    private Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new AuthServerRoleConverter());
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }
}

Note that as per configuration, only the user having the role as admin can access POST request going to /customer. For other requests , only authentication is required there is no authorization.

oauth2ResourceServer() method verifies an access token before forwarding request to services kept behind the API gateway. Since we have also configured some roles for the users so that information is also passed to the resource server in the form of SimpleGrantedAuthority objects.

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AuthServerRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>>{

  @Override
  public Collection<GrantedAuthority> convert(Jwt jwt) {
    Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        if (realmAccess == null || realmAccess.isEmpty()) {
            return new ArrayList<>();
        }
        List<String> roles = (List<String>) realmAccess.get("roles");
        Collection<GrantedAuthority> grantedAuthorities = roles.stream()
            .map(roleName -> "ROLE_" + roleName)
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        //System.out.println(grantedAuthorities);
        return grantedAuthorities;
  }
}

Test using Postman

Create a GET request for URL- localhost:8181/customer/2 (Please make the request as per the ID you have, I have a Customer with ID 2 in DB).

Try sending the request to backend without setting any type of authorization configuration. You should get "401 Unauthorized" as response.

Postman GET request_unauthorized

Authorization settings in Postman

Select "Authorization" tab in Postman and below that select type as Oauth 2.0 and "Add authorization data to" as Request Header.

In the "Configure new token" section provide the following configuration options.

Token Name- accesstoken

Grant Type- Select Authorization code

Callback URL- Check Authorize using browser (So that login page is opened in a browser)

To get values for these two properties "Auth URL" and "Access Token URL " go to "Realm Settings" and all the way down in the displayed screen. There click on OpenID Endpoint Configuration

Keycloak Accesse end points

Select value of authorization_endpoint and enter it in Auth URL, select value of token_endpoint and enter it in Access Token URL

Client ID- Name selected for the client (customer-client). Get it from Keycloak admin console.

Client Secret- Client secret displayed for the client. Get it from Keycloak admin console.

Scope- openid email profile (scope for access is email and profile of the user)

State- ab12-cd34-ef56 (Give an alphanumeric value that is used for preventing cross-site request forgery.

Client Authentication- select "send client credentials in body"

Then click "Get New Access Token"

Postman authorization config

Ensure that you are not already logged in to the Keycloak server in your default browser as admin, otherwise it will show you already logged in as admin. What we need is to log in as either test_user or test_admin.

Once you click on "Get New Access Token" in Postman it should open the Keycloak login page. Lets login with test_user and its password.

keycloak login

Once you enter the credentials and click sign in browser should redirect you to Postman with the access token. If that doesn't happen check that browser is allowing popups for this URL.

Postman with access_token

Click on use token so that the sent token is used as the bearer token with the request. Click Send to send the GET request to the backend. This time you should get the Customer details.

Postman GET request_authorized

POST mapping

Create a Post request for URL- localhost:8181/customer with body as

{
    "name": "Suresh",
    "age": 35,
    "city": "Bangalore"
}

Authorization setting remains same and user logged in is "test_user"

Click on use token so that the sent token is used as the bearer token with the request. Click Send to send the Post request to the backend.

Since "user" role doesn't have authority to make a POST request so 403 Forbidden is sent as response.

Click again on "Get New Access Token" and login as "test_admin" user. Now POST request should work.

Postman POST request

That's all for this topic Spring Boot Microservice + API Gateway + OAuth2 + Keycloak. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Spring Tutorial Page


Related Topics

  1. Spring Boot Observability - Distributed Tracing, Metrics
  2. Spring Boot Event Driven Microservice With Kafka
  3. Spring Boot Microservice - Externalized Configuration With Spring Cloud Config
  4. Spring Boot Microservice Circuit Breaker Using Resilience4j
  5. Spring Boot Microservice Example Using WebClient

You may also like-

  1. Spring MVC Checkbox And Checkboxes Form Tag Example
  2. Spring NamedParameterJdbcTemplate Insert, Update And Delete Example
  3. Injecting Inner Bean in Spring
  4. Bean Definition Inheritance in Spring
  5. Java LinkedBlockingDeque With Examples
  6. Abstraction in Java
  7. Angular Event Binding With Examples
  8. JavaScript Rest Parameter

No comments:

Post a Comment