Integrate Keycloak With Spring Boot Step by Step

Recently, I had to configure an API to use Keycloak to authenticate and authorize users of a web application. After spending almost two weeks, I finally got the app up and running as required. If you are on the same boat, read on since I’ll provide step by step (with pictures!) to help you save time on this setup so you can spend more time with actual development.

If you need help integrating Keycloak with Spring Boot 3, check this tutorial out.

API Requirements

There are some requirements as follow:

  • The spring boot API can create and login (return access token) to the caller
  • There are three roles (admin, moderator, member) in the app and each API endpoint can be guarded by a specific role require. There are also endpoints that don’t require authorization (publicly available)
  • Endpoints that are accessible by member are also accessible by admin and moderator. Endpoints that are accessible by moderator are accessible by admin

Code repository

If you are the impatient kind (like me), you can skip the post and go directly to the source code (available on Github) to start tinkering with the application here.

However, when you are stuck, feel free to go back to this post to find the missing pieces.

Quick Keycloak setup with docker compose

If you have a Keycloak instance up and running, you can skip this part. However, in case you haven’t, use the docker-compose file below to quickly set up Keycloak:

version: '3'

services:
  keycloak:
    container_name: keycloak
    image: jboss/keycloak:15.0.2
    restart: always
    env_file: ./keycloak.env
    depends_on:
      - keycloak_db
    volumes:
      - ./realm.json:/tmp/realm.json
    ports:
      - "18080:8080"


  keycloak_db:
    container_name: keycloak_db
    image: mariadb:10.3.26
    restart: always
    volumes:
      - keycloak_db_vol:/var/lib/mysql
    env_file:
      - ./keycloak.env
      
volumes:
  keycloak_db_vol:      

The keycloak.env contains some environment variables:

KEYCLOAK_USER=kc_dev
KEYCLOAK_PASSWORD=kc_dev1231232
KEYCLOAK_IMPORT="-Dkeycloak.profile.feature.upload_scripts=enabled"
DB_VENDOR=mariadb
DB_ADDR=keycloak_db:3306
DB_DATABASE=keycloak_1
DB_USER=root
DB_PASSWORD=root

MYSQL_ROOT_PASSWORD=root
MARIADB_DATABASE=keycloak_1

Now run docker-compose up -d then you should be able to access keycloak at http://localhost:18080 in a few minutes (docker may need to pull the images from its registry, which depends largely on the speed of your connection).

For the lazy guys, please go to this location in the repo and hit docker-compose up -d

Once, you have access to Keycloak, login with the id and password specified in the environment file (kc_dev and kc_dev1231232).

In the beginning, there should be only one realm, you need to click on Add realm to create a new realm.

Create a new realm in keycloak
Create a new realm in keycloak

Now the keycloak instance is ready. It’s time to create and configure a client.

Keycloak client configuration

The client’s configuration is very important so you need to pay close attention to this part.

At the beginning, there are some default clients:

Keycloak's default clients

You can click on the Create button on the right to create a new client like this:

Create new keycloak client

And configure the client like this:

keycloak client's configuration

Now, it’s time to create roles for the client. As you can remember, we have three roles:

  • admin
  • moderator
  • member

From the requirement at the beginning, we know that admin and moderator are composite roles while member is not a composite role.

A composite role is a role that consists of one or
more other roles

Let’s switch to the Roles tab and click on Add role

add new role in keycloak

After creating the role, you will see there is a switch to turn that role into a composite role. For the member role, we don’t need that.

However, as mentioned above, admin and moderator are composite roles, we need to turn that option on:

adding composite role

As you can see in this case, when creating moderator role, I turned the composite role on. In the Client roles select box, I select this client (spring-boot-client).

Let’s do the same for the admin role.

You don’t need to add the member role to the associated roles box since moderator includes member already.

Spring boot application configuration

Let’s configure the application.properties file.

#keycloak
keycloak.realm=datmt-test-realm
keycloak.auth-server-url=http://localhost:18080/auth/
#use external in production
keycloak.ssl-required=none

#name of the client
keycloak.resource=spring-boot-client
keycloak.credentials.secret=ee923b32-a4f1-4435-8066-9eb4541e8165
keycloak.use-resource-role-mappings=true
keycloak.bearer-only=true

All the fields in this configuration file are self-explanatory. You may wonder where to get the secret? It’s in the Credentials tab of the client:

getting keycloak's client secrets

Also, for Keycloak to guard your endpoints, you need to create a configuration file like this:

package com.datmt.keycloak.springbootauth.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .antMatchers("/member/**").hasAnyRole("member")
            .antMatchers("/moderator/**").hasAnyRole("moderator")
            .antMatchers("/admin/**").hasAnyRole("admin")
            .anyRequest()
            .permitAll();
        http.csrf().disable();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public KeycloakConfigResolver KeycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
}

Here, you can see that I’ve configured that:

  • Paths begins with /admin requrie admin role
  • Paths begin with /member require member role
  • Paths begin with /moderator require moderator role
  • Paths begin with /public is accessible by all

Let’s create a controller and test our config:

package com.datmt.keycloak.springbootauth.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.security.RolesAllowed;

@RestController
public class HelloController {

	@GetMapping("/public/hello")
	public ResponseEntity<String> helloPublic() {
		return ResponseEntity.ok("Hello public user");
	}
	
	@GetMapping("/member/hello")
	public ResponseEntity<String> helloMember() {
		return ResponseEntity.ok("Hello dear member");
	}
	
	@GetMapping("/moderator/hello")
	public ResponseEntity<String> helloModerator() {
		return ResponseEntity.ok("Hello Moderator");
	}
	
	
	@GetMapping("/admin/hello")
	public ResponseEntity<String> helloAdmin() {
		return ResponseEntity.ok("Nice day, admin");
	}

	
}

Let’s try the public endpoint:

unguarded endpoint
Public endpoint works

As you can see, it works as expected. The path begins with /public so anyone can access without any authentication.

Now let’s try the path for member:

and also admin:

You can see that since the request didn’t present any kind of credentials, the response is 401.

To make sure this works, let’s create users with specific roles and test if they can access the endpoints with his token.

Let’s first create an user with member role:

After creating the user, go to Role Mappings tab, select spring-boot-client and add member to user’s list of roles

Now, to quickly get the token for this user, go back to the details tab and click on Impersonate:

In the new tab that appears, open the network tab of the developer console, reload and copy the access token:

impersonate and get token

Now, use that token to make a request in postman:

You expected that to be a 200 response, right? But what I got was 403. That’s better than 401 since we can authenticate with the token but somehow, the user was not authorized to access the endpoint.

What’s the missing configuration?

If you decode the token using sites like jwt.io, you will see that even though the user has role member, it didn’t show in the token:

It turned out, you also need to configure client scopes so the roles appear on the token.

Then go to Scopes and add all roles of the client spring-boot-client

Now, let’s impersonate the user again. Now, her roles should be visible on the token decoding page:

Let’s make the same request to them member’s endpoint again but with the new token, sure enough, it worked:

You can try with other roles, they should work too.

Guarding API with RolesAllowed

Guarding endpoint with path prefixes is OK and sometimes that’s a clean solution. However, there are times you may need a finer tune to your access policy.

In such case, you can use RolesAllowed to specify which role can access a specific endpoint, no matter what its path prefix is.

	@RolesAllowed("member")
	@GetMapping("/other/hello")
	public ResponseEntity<String> helloCustom() {
		return ResponseEntity.ok("Nice day, my custom user");
	}

You can guess from the annotation, anyone with role member can access this endpoint.

What if the path is prefix but /public?

	@RolesAllowed("member")
	@GetMapping("/public/hello-fake-public")
	public ResponseEntity<String> helloCustom() {
		return ResponseEntity.ok("Nice day, it appears to be public but not");
	}

If you try to access the URL, even though the prefix is public, you will get a 401 without a token:

But if a user with member role, the access is granted:

overwrite path prefix with rolesallowed

Create account and login using Keycloak Admin Client

Now the API can talk to keycloak to authenticate and authorize users, let’s also integrate user login and creation to the API so we can get the token via the API.

Before doing anything with the code, let’s add some more configuration to the client so it has what it needs to manage user.

First, go to the client’s Service Account Role tab and configure as follow:

Configure role for the service account
Add manager users, query users role for the client’s service account

Now, the client is able to manage users.

Let’s add the keycloak-admin-client package to maven. You need this to enable spring boot to operate Keycloak’s admin-related tasks.

		<dependency>
			<groupId>org.keycloak</groupId>
			<artifactId>keycloak-admin-client</artifactId>
			<version>${keycloak.version}</version>
		</dependency>

Next add a KeycloakProvider class:

@Configuration
@Getter
public class KeycloakProvider {

    @Value("${keycloak.auth-server-url}")
    public String serverURL;
    @Value("${keycloak.realm}")
    public String realm;
    @Value("${keycloak.resource}")
    public String clientID;
    @Value("${keycloak.credentials.secret}")
    public String clientSecret;

    private static Keycloak keycloak = null;

    public KeycloakProvider() {
    }

    public Keycloak getInstance() {
        if (keycloak == null) {

            return KeycloakBuilder.builder()
                    .realm(realm)
                    .serverUrl(serverURL)
                    .clientId(clientID)
                    .clientSecret(clientSecret)
                    .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
                    .build();
        }
        return keycloak;
    }


    public KeycloakBuilder newKeycloakBuilderWithPasswordCredentials(String username, String password) {
        return KeycloakBuilder.builder() //
                .realm(realm) //
                .serverUrl(serverURL)//
                .clientId(clientID) //
                .clientSecret(clientSecret) //
                .username(username) //
                .password(password);
    }

    public JsonNode refreshToken(String refreshToken) throws UnirestException {
        String url = serverURL + "/realms/" + realm + "/protocol/openid-connect/token";
        return Unirest.post(url)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .field("client_id", clientID)
                .field("client_secret", clientSecret)
                .field("refresh_token", refreshToken)
                .field("grant_type", "refresh_token")
                .asJson().getBody();
    }
}

Then a service to create and log in user:

@Service
public class KeycloakAdminClientService {
    @Value("${keycloak.realm}")
    public String realm;

    private final KeycloakProvider kcProvider;


    public KeycloakAdminClientService(KeycloakProvider keycloakProvider) {
        this.kcProvider = keycloakProvider;
    }

    public Response createKeycloakUser(CreateUserRequest user) {
        UsersResource usersResource = kcProvider.getInstance().realm(realm).users();
        CredentialRepresentation credentialRepresentation = createPasswordCredentials(user.getPassword());

        UserRepresentation kcUser = new UserRepresentation();
        kcUser.setUsername(user.getEmail());
        kcUser.setCredentials(Collections.singletonList(credentialRepresentation));
        kcUser.setFirstName(user.getFirstname());
        kcUser.setLastName(user.getLastname());
        kcUser.setEmail(user.getEmail());
        kcUser.setEnabled(true);
        kcUser.setEmailVerified(false);
        return usersResource.create(kcUser);

    }

    private static CredentialRepresentation createPasswordCredentials(String password) {
        CredentialRepresentation passwordCredentials = new CredentialRepresentation();
        passwordCredentials.setTemporary(false);
        passwordCredentials.setType(CredentialRepresentation.PASSWORD);
        passwordCredentials.setValue(password);
        return passwordCredentials;
    }


}

Finally, create a controller to create and login user:

@RestController
@RequestMapping("/user")
public class UserController {
    private final KeycloakAdminClientService kcAdminClient;

    private final KeycloakProvider kcProvider;

    private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(UserController.class);


    public UserController(KeycloakAdminClientService kcAdminClient, KeycloakProvider kcProvider) {
        this.kcProvider = kcProvider;
        this.kcAdminClient = kcAdminClient;
    }
	

    @PostMapping(value = "/create")
    public ResponseEntity<Response> createUser(@RequestBody CreateUserRequest user) {
        Response createdResponse = kcAdminClient.createKeycloakUser(user);
        return ResponseEntity.ok(createdResponse);

    }

    @PostMapping("/login")
    public ResponseEntity<AccessTokenResponse> login(@NotNull @RequestBody LoginRequest loginRequest) {
        Keycloak keycloak = kcProvider.newKeycloakBuilderWithPasswordCredentials(loginRequest.getUsername(), loginRequest.getPassword()).build();

        AccessTokenResponse accessTokenResponse = null;
        try {
            accessTokenResponse = keycloak.tokenManager().getAccessToken();
            return ResponseEntity.status(HttpStatus.OK).body(accessTokenResponse);
        } catch (BadRequestException ex) {
            LOG.warn("invalid account. User probably hasn't verified email.", ex);
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(accessTokenResponse);
        }

    }

}

Too much code? No worries, they are all available on Github.

Now, open postman and call these endpoints. First, create a user:

If you login with that user now, you will get an error since the user hasn’t got email verified:

Simply switch email verified on then y ou can get the tokens:

Notice that you need to put email in the place of user name because, by default, email is used as username:

Obviously, you can turn this off to log in with the username.

Conclusion

In this super long post, I created a project that help you to quickly configure keycloak with your spring boot app. You can now use this project as a base and start creating your app’s business logic.

Code repo is here: https://github.com/datmt/Keycloak-Spring-Boot-Login-Create-User

21 thoughts on “Integrate Keycloak With Spring Boot Step by Step”

  1. Great Job bro!!! Some screens may have been changed in the current version but it’s really helpful for me. Thank you very much.

    Reply
  2. Great job on your article. I have a problem with setting up the keycloak provider to save a user on the version 19.0.2
    find below the error am facing i may be missing a step or i did not properly set up my service class
    DEBUG [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-1) Error response 401: javax.ws.rs.NotAuthorizedException: HTTP 401 Unauthorized
    at org.keycloak.services.resources.admin.AdminRoot.authenticateRealmAdminRequest(AdminRoot.java:161)
    at org.keycloak.services.resources.admin.AdminRoot.getRealmsAdmin(AdminRoot.java:205)
    at jdk.internal.reflect.GeneratedMethodAccessor126.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)

    Reply
  3. Thank you for the tutorial. It has solved my problem. But I would like to ask how can I add a role to the user when creating a user through Spring.

    Reply
    • Add this code to execution after 201
      log.info(“User creation successfull “);
      String userId = CreatedResponseUtil.getCreatedId(response);
      log.info(“Created user id = “+userId);

      // Define password credential
      CredentialRepresentation passwordCred = new CredentialRepresentation();
      passwordCred.setTemporary(false);
      passwordCred.setType(CredentialRepresentation.PASSWORD);
      passwordCred.setValue(user.getPassword());

      UserResource userResource = usersResource.get(userId);

      // Set password credential
      userResource.resetPassword(passwordCred);

      // Get client
      ClientRepresentation app1Client = realmResource.clients() //
      .findByClientId(“your-client-name”).get(0); // Get this from configuration

      // Get client level role (requires view-clients role)
      RoleRepresentation userClientRole = realmResource.clients().get(app1Client.getId()) //
      .roles().get(“member”).toRepresentation(); // role name in this case is member

      // Assign client level role to user
      userResource.roles().clientLevel(app1Client.getId()).add(Arrays.asList(userClientRole));

      Reply
  4. Inside client -> Authentication Flow Overrides properties -> Keep the value of Direct Grant Flow as Direct Grant Flow. Where in the screenshot you mentioned, it’s selected as Browser for both the fields.

    Reply
  5. Thanks a lot for this amazing article, I just want to receive the email but nothing happened
    Notice: I configure my email setting like SMTP host and port…

    Reply
  6. Great description of the role based authorization. I followed step by step but I am getting 403 forbidden error while creating user from postman

    Reply

Leave a Comment