Connecting Keycloak to Spring Boot Application

Hello Habr!

As you know, spring OAuth2.0.x has been in support mode for almost 2 years back and most of its functionality is now available in spring-security (matching matrixI). Spring-security refused to port Authorization service (roadmap) and suggest using free or paid analogs instead, in particular keycloak… In this post, we would like to share different options for connecting keycloak to spring-boot applications.

Content

  • A little about Keycloak

  • Launch and configure keycloak

  • We connect Keycloak using an adapter

  • Using OAuth2 Client from spring-security

  • We connect the application as a ResourceService

  • Authorizing service calls using keycloak

A little about Keycloak

It is an open source SSO (Single sign on) implementation for managing user identity and access.

The main functionality supported by Keycloak:

  • Single-Sign On and Single-Sign Out.

  • OpenID / OAuth 2.0 / SAML.

  • Identity Brokering – Authentication using external OpenID Connect or SAML.

  • Social Login – Supports Google, GitHub, Facebook, Twitter.

  • User Federation – synchronization of users from LDAP and Active Directory servers.

  • Kerberos bridge – using a Kerberos server for automatic user authentication.

  • Flexible policy management through realm.

  • Adapters for JavaScript, WildFly, JBoss EAP, Fuse, Tomcat, Jetty, Spring.

  • Expandable using plugins.

  • And many many others…

Launch and configure keycloak

It is convenient to use docker-compose to run keycloak on a development machine. In this case, we can launch our authorization service at different times for different applications, thereby saving ourselves from a bunch of problems associated with configuration for different applications. Below is one of the docker-compose configuration options for running a standalone server with a postgres database:

docker-compose.yml
version: "3.8"

services:
  postgres:
    container_name: postgres
    image: library/postgres
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
      POSTGRES_DB: keycloak_db
    ports:
      - "5432:5432"
    restart: unless-stopped

  keycloak:
    image: jboss/keycloak
    container_name: keycloak
    environment:
      DB_VENDOR: POSTGRES
      DB_ADDR: postgres
      DB_DATABASE: keycloak_db
      DB_USER: ${POSTGRES_USER:-postgres}
      DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: admin_password
    ports:
      - "8484:8080"
    depends_on:
      - postgres
    links:
      - "postgres:postgres"

After a successful launch, you need to configure the realm, clients, roles and users.

Let’s make some initial settings. Let’s create realm “my_realm”:

After that, let’s create a client "my_client", through which we will authorize users (we will leave all the default settings):

Do not forget to indicate redirect_url… In our case, it will be equal to: http: // localhost: 8080 / *

Let’s create roles for users of our system – "ADMIN", "USER":

Add users "admin" with the role "ADMIN":

And the user "user" with the role "USER"… Don’t forget to set passwords on the tab "Credentials":

The basic configuration is complete, now you can start connecting spring boot applications.

We connect Keycloak using an adapter

The official documentation for keycloak for use in applications recommends using ready-made libraries – adapters that make it possible to get rid of the boilerplate code and unnecessary configuration. There is an implementation for most popular languages ​​and frameworks (supported-platforms). We will be using the Spring Boot Adapter.

Let’s create a small demo application on spring-boot (sources can be found here) and connect the Keycloak Spring Boot adapter to it. The maven config file will look like this:

pom.xml
<?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.9.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.akazakov.keycloak</groupId>
	<artifactId>demo-keycloak-adapter</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>Demo Keycloak Adapter</name>
	<description>Demo project for Spring Boot and Keycloak</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.keycloak.bom</groupId>
				<artifactId>keycloak-adapter-bom</artifactId>
				<version>12.0.3</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	<dependencies>
		<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.keycloak</groupId>
			<artifactId>keycloak-spring-boot-starter</artifactId>
		</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>
</project>

For verification purposes, let’s add a controller that will expose methods for various user roles and information about the current user (we will use the same controller in other examples below):

@RestController
@RequestMapping("/api")
public class SampleController {

    @GetMapping("/anonymous")
    public String getAnonymousInfo() {
        return "Anonymous";
    }

    @GetMapping("/user")
    @PreAuthorize("hasRole('USER')")
    public String getUserInfo() {
        return "user info";
    }

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String getAdminInfo() {
        return "admin info";
    }

    @GetMapping("/service")
    @PreAuthorize("hasRole('SERVICE')")
    public String getServiceInfo() {
        return "service info";
    }

    @GetMapping("/me")
    public Object getMe() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication.getName();
    }
}

In order for our application to successfully launch and connect to keycloak, we need to add the appropriate configuration. The first thing we will do is add client settings and connection to the authorization server in application.yml:

server:
  port: ${SERVER_PORT:8080}
spring:
  application.name: ${APPLICATION_NAME:spring-security-keycloak}
keycloak:
  auth-server-url: http://localhost:8484/auth
  realm: my_realm
  resource: my_client
  public-client: true

After that, add the spring-security configuration, override KeycloakWebSecurityConfigurerAdaptersupplied with the adapter:

@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .authorizeRequests()
                .antMatchers("/api/anonymous/**").permitAll()
                .anyRequest().fullyAuthenticated();
    }
}

Now let’s check the work of our application. Let’s launch the application and try to enter the user on the corresponding url. For example: http://localhost:8080/api/admin… As a result, the browser will redirect us to the user login window:

After entering the correct username and password, the browser will redirect us to the original address. As a result, we get a page with some information available to the user:

If we go to the address for obtaining information about the current user (http://localhost:8080/api/me), we will get the user uuid in keycloak as a result:

If we need the service to only check the access token and not initialize the user authentication procedure, just enable bearer-only: true to the application config:

keycloak:
  auth-server-url: http://localhost:8484/auth
  realm: my_realm
  resource: my_client
  public-client: true
  bearer-only: true

Using OAuth2 Client from spring-security

Using the keycloak adapter saves us from writing a bunch of boilerplate code. But at the same time, our application becomes implementation dependent. In some cases, you shouldn’t be tied to any specific authorization service, this will give us more flexibility in the further operation of the system.

One of the key features of spring security 5 is support for OAuth2 and OIDC protocols. We can use the OAuth2 client from the spring-security package to integrate with the keycloak server.

So, to use the client, we connect the appropriate library depending on the project (example source code). Full text pom.xml:

pom.xml
<?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.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.akazakov.keycloak</groupId>
    <artifactId>demo-keycloak-oauth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-keycloak-oauth</name>
    <description>Demo project for Spring Boot OAuth and Keycloak</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</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.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>

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

</project>

Further in application.yaml you must specify the parameters for connecting to the authorization service:

server:
  port: ${SERVER_PORT:8080}
spring:
  application.name: ${APPLICATION_NAME:spring-security-keycloak-oauth}
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: http://localhost:8484/auth/realms/my_realm
        registration:
          keycloak:
            client-id: my_client

By default, user roles will be calculated based on the value "scope" in the access token, and added to them "ROLE_USER" for all authorized users of the system. You can leave it as it is and go to the scope model. But in our example, we will use user roles within the realm. All we need is to override oidcUserService and set your own mapping of roles for the user. The necessary roles come in the section "groups" access token, we will use it to define user roles. As a result, our configuration for spring security with an overridden oidcUserService will look like this:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests -> authorizeRequests
                        .antMatchers("/api/anonymous/**").permitAll()
                        .anyRequest().authenticated())
                .oauth2Login(oauth2Login -> oauth2Login
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
                                .oidcUserService(this.oidcUserService())
                        )
                );

    }

    @Bean
    public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        final OidcUserService delegate = new OidcUserService();

        return (userRequest) -> {
            OidcUser oidcUser = delegate.loadUser(userRequest);

            final Map<String, Object> claims = oidcUser.getClaims();
            final JSONArray groups = (JSONArray) claims.get("groups");

            final Set<GrantedAuthority> mappedAuthorities = groups.stream()
                    .map(role -> new SimpleGrantedAuthority(("ROLE_" + role)))
                    .collect(Collectors.toSet());

            return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
        };
    }
}

In this case, the application will work almost the same as using the keycloak adapter.

We connect the application as a ResourceService

Quite often, we don’t want our application to initiate user authentication. It is enough just to check the user’s authorization by the provided access token. An option to connect authorization with keycloak without using an adapter is to configure the application as a resource server. In this case, the application cannot initiate user authentication, but only authorizes the user and verifies the signature of the access token. Let’s include the corresponding libraries: spring-security-oauth2-resource-server and spring-security-oauth2-jose (source). Full file pom.xml will look like this:

pom.xml
<?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.9.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.akazakov.keycloak</groupId>
	<artifactId>demo-keycloak-resource</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo-keycloak-resource</name>
	<description>Demo project for Spring Boot and Spring security and Keycloak</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<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>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</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>

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

</project>

Next, we need to specify the path to the JWK (JSON Web Key) set of keys with which our application will check access tokens. In keycloak, they are available at: http://${host}/auth/realms/${realm)/protocol/openid-connect/certs… Eventually application.yml will look like this:

server:
  port: ${SERVER_PORT:8080}
spring:
  application.name: ${APPLICATION_NAME:spring-security-keycloak-resource}
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${KEYCLOAK_REALM_CERT_URL:http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/certs}

As with the OAuth2 Client, we also need to override the user role converter. In this case, we can override jwtAuthenticationConverter

Full text WebSecurityConfiguration:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests -> authorizeRequests
                        .antMatchers("/api/anonymous/**").permitAll()
                        .anyRequest().authenticated())
                .oauth2ResourceServer(resourceServerConfigurer -> resourceServerConfigurer
                        .jwt(jwtConfigurer -> jwtConfigurer
                                .jwtAuthenticationConverter(jwtAuthenticationConverter()))
                );
    }

    @Bean
    public Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());
        return jwtAuthenticationConverter;
    }

    @Bean
    public Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
        JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter();

        return new Converter<>() {
            @Override
            public Collection<GrantedAuthority> convert(Jwt jwt) {
                Collection<GrantedAuthority> grantedAuthorities = delegate.convert(jwt);

                if (jwt.getClaim("realm_access") == null) {
                    return grantedAuthorities;
                }
                JSONObject realmAccess = jwt.getClaim("realm_access");
                if (realmAccess.get("roles") == null) {
                    return grantedAuthorities;
                }
                JSONArray roles = (JSONArray) realmAccess.get("roles");

                final List<SimpleGrantedAuthority> keycloakAuthorities = roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
                grantedAuthorities.addAll(keycloakAuthorities);

                return grantedAuthorities;
            }
        };
    }
}

Here we create a converter (jwtGrantedAuthoritiesConverter), which takes a token and fetches from the section "realm_access" user roles. Then we can either return them immediately, or, as in this case, expand the list that is extracted by the default converter.

Let’s check the work. We will use the built-in client in Intellij idea http, or a plugin for VSCode – Rest Client… First, we get the user’s token, make a request to keycloak using the login and password of the registered user:

###
POST <http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/token>
Content-Type: application/x-www-form-urlencoded

client_id=my_client&grant_type=password&scope=openid&username=admin&password=admin

> {% client.global.set("auth_token", response.body.access_token); %}

The answer will be something like this:

Answer
POST <http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/token>

HTTP/1.1 200 OK
...
Content-Type: application/json

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlb21qWFY2d3dNek8xVS0tYUdhVllpSHM3eURaZVM1aU96bl9JR3RlS1ZzIn0.eyJleHAiOjE2MTY2NTQzNjEsImlhdCI6MTYxNjY1NDA2MSwianRpIjoiMGQwMjg2YWUtYTlmYy00MzcxLWFmM2ItZjJlNTM5N2I4NzViIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjkzMGIxMTNmLWI0NzUtNDhkMC05NTQxLWMyYzI2MWZlYmRmZCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiQURNSU4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.dvGvYhhhfH8r6EP8k_spFwBS35ulYMTWNL4lcz9PR2e-p4FU-ehre1EQA8xpbkYzYEWRB_elzTya5IhbYR8KArrujplIDNAOlqJ9W6a4Tx-r44QCteM0DW4BNzbZAH2L0Bg7aSstRKUuULceRNYQcdCvSFjEU5DsHk26a6TM5KCrkv0ryGo11pam-pnbs2Z2jOSfSHvOAfMNL9OVJYRBjlTmsEzzgH9dHSa_pT2Q-SvgvfCcwfY0XkgUZkMPUtz85-lqchROb4XpHOiy3Cfn8MgrGNwhf-MsmN5wiAGe0DI_LW2Jxr3boZMLS4AuuNQ7agr65g-JuO9-LhlgndxN8g",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmNGEwNWQxNy0yNWU4LTRjMjEtOTMyMC0zMzcwODlhNTg5MjQifQ.eyJleHAiOjE2MTY2NTU4NjEsImlhdCI6MTYxNjY1NDA2MSwianRpIjoiMjNmNDBiZWUtNmQ3Ny00ZTIxLTg0NTItNDg1NDc2OTk1ZDUyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwic3ViIjoiOTMwYjExM2YtYjQ3NS00OGQwLTk1NDEtYzJjMjYxZmViZGZkIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIn0.r4BrjwfavKFF8dst3AyRi0LTfymbSVfDKDT9KyMpmzk",
  "token_type": "bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlb21qWFY2d3dNek8xVS0tYUdhVllpSHM3eURaZVM1aU96bl9JR3RlS1ZzIn0.eyJleHAiOjE2MTY2NTQzNjEsImlhdCI6MTYxNjY1NDA2MSwiYXV0aF90aW1lIjowLCJqdGkiOiJiN2UwNDhmZS01ZTRjLTQxMWYtYTBjMC0xNGExYzhlOGJhYWEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg0ODQvYXV0aC9yZWFsbXMvbXlfcmVhbG0iLCJhdWQiOiJteV9jbGllbnQiLCJzdWIiOiI5MzBiMTEzZi1iNDc1LTQ4ZDAtOTU0MS1jMmMyNjFmZWJkZmQiLCJ0eXAiOiJJRCIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJhdF9oYXNoIjoiRlh2VzB2Z3pwd3R6N1FabEZtTFhJdyIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.ZDeZg4Z-PPmn2fVm7opGLRutzDh6l8uRYqZzbqIX7wk0GhgtMHV1CW8RvDd51AuYw81WyoMyRAD_-T6ne58Rt9f5XNZZfS8xoXzTFV1xH6XigOVQH2jIHN-2VIM1IgJnteo7nuTz9zo4OXIFvEjaFHq4AXDkiq6jhThv0qPS3WrAA-MutyW8G37GM0fsCgANvlGKoWm1_1wKyeTZ0Gfug32Vf6gUikfxA9bmaS4oGYGc6lqFE6EHgtjIn0q9gNUfpEXaqpiL3mCBu9V6sJG5Rp_MOqp-aXrM9NbLTz2JTXevtClHI6qVUIoh8OXXXT98QmKrVr9Cyr9BRUrQyt0Zzg",
  "not-before-policy": 0,
  "session_state": "5d29d46e-b926-4d59-89f8-2436edcae4f0",
  "scope": "openid profile email"
}

Response code: 200 (OK); Time: 114ms; Content length: 2987 bytes

Now let’s check that the methods are available to a user with the appropriate rights:

GET <http://localhost:8080/api/admin>
Authorization: Bearer {{auth_token}}
Content-Type: application/json

In response, we get:

GET <http://localhost:8080/api/admin>

HTTP/1.1 200 
...

admin info

Response code: 200; Time: 34ms; Content length: 10 bytes

Authorizing service calls using keycloak

When working with a microservice architecture, sometimes there are requirements for authorized calls between services. In cases where the initiator of the interaction is some internal process or service, we need to take an access token somewhere. As a solution to this issue, we can use Client Credentials Flow to get a token from keycloak (the source code of the example is available at link).

First, let’s create a new client, under which our services will be authorized:

To be able to authorize the service, we need to change the type of access ("Access Type") on the "confidential" and enable the flag "Service accounts Enabled"… The rest of the configuration does not differ from the default configuration:

If we need the services authorized under this client to have their own role, add it to the role:

Next, this role must be added to the client. In the tab "Service Account Roles" choose the required role – in our case, the role "SERVICE":

We save the client_id and client_secret for further use in services for authorization:

For demonstration, we will create a small application that will receive information available at http://localhost:8080/api/service from previous examples.

First, let’s create a component that will authorize our service in keycloak:

@Component
public class KeycloakAuthClient {
    private static final Logger log = LoggerFactory
            .getLogger(KeycloakAuthClient.class);

    private static final String TOKEN_PATH = "/token";
    private static final String GRANT_TYPE = "grant_type";
    private static final String CLIENT_ID = "client_id";
    private static final String CLIENT_SECRET = "client_secret";
    public static final String CLIENT_CREDENTIALS = "client_credentials";

    @Value("${app.keycloak.auth-url:http://localhost:8484/auth/realms/my_realm/protocol/openid-connect}")
    private String authUrl;

    @Value("${app.keycloak.client-id:service_client}")
    private String clientId;

    @Value("${app.keycloak.client-secret:acb719cf-4afd-42d3-91f2-93a60b3f2023}")
    private String clientSecret;

    private final RestTemplate restTemplate;

    public KeycloakAuthClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public KeycloakAuthResponse authenticate() {
        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
        paramMap.add(CLIENT_ID, clientId);
        paramMap.add(CLIENT_SECRET, clientSecret);
        paramMap.add(GRANT_TYPE, CLIENT_CREDENTIALS);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        String url = authUrl + TOKEN_PATH;

        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(paramMap, headers);

        log.info("Try to authenticate");

        ResponseEntity<KeycloakAuthResponse> response =
                restTemplate.exchange(url,
                        HttpMethod.POST,
                        entity,
                        KeycloakAuthResponse.class);

        if (!response.getStatusCode().is2xxSuccessful()) {
            log.error("Failed to authenticate");
            throw new RuntimeException("Failed to authenticate");
        }

        log.info("Authentication success");

        return response.getBody();
    }
}

Method authenticate makes a call to keycloak and, if successful, returns an object KeycloakAuthResponse:

public class KeycloakAuthResponse {
    @JsonProperty("access_token")
    private String accessToken;

    @JsonProperty("expires_in")
    private Integer expiresIn;

    @JsonProperty("refresh_expires_in")
    private Integer refreshExpiresIn;

    @JsonProperty("refresh_token")
    private String refreshToken;

    @JsonProperty("token_type")
    private String tokenType;

    @JsonProperty("id_token")
    private String idToken;

    @JsonProperty("session_state")
    private String sessionState;

    @JsonProperty("scope")
    private String scope;

    // Getters and setters or lombok ...
}

Next we take access_token from the answer for use in further calls to protected methods. Below is an example of a call to a protected method:

@SpringBootApplication
public class DemoServiceAuthApplication implements CommandLineRunner {
    private static final String BEARER = "Bearer ";
    private static final String SERVICE_INFO_URL = "http://localhost:8080/api/service";

    private final KeycloakAuthClient keycloakAuthClient;

    private final RestTemplate restTemplate;

    private static final Logger log = LoggerFactory
            .getLogger(DemoServiceAuthApplication.class);

    public DemoServiceAuthApplication(KeycloakAuthClient keycloakAuthClient, RestTemplate restTemplate) {
        this.keycloakAuthClient = keycloakAuthClient;
        this.restTemplate = restTemplate;
    }


    public static void main(String[] args) {
        SpringApplication.run(DemoServiceAuthApplication.class, args);
    }

    @Override
    public void run(String... args) {
        final KeycloakAuthResponse authenticate = keycloakAuthClient.authenticate();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(authenticate.getAccessToken());

        log.info("Make request to resource server");

        final ResponseEntity<String> responseEntity = restTemplate.exchange(SERVICE_INFO_URL, HttpMethod.GET, new HttpEntity(headers), String.class);

        if (!responseEntity.getStatusCode().is2xxSuccessful()) {
            log.error("Failed to request");
            throw new RuntimeException("Failed to request");
        }

        log.info("Response data: {}", responseEntity.getBody());
    }
}

First, we authorize our service through keycloak, then we make a request to the protected resource by adding the parameter to the HTTP Headers Authorization: Bearer ...

As a result of executing the program, we get the contents of the protected method:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.4)

2021-04-13 16:04:36.672  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Starting DemoServiceAuthApplication using Java 14.0.1 on MacBook-Pro.local with PID 19240 (/Users/akazakov/Projects/spring-boot-keycloak/demo-service-auth/target/classes started by akazakov in /Users/akazakov/Projects/spring-boot-keycloak)
2021-04-13 16:04:36.674  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : No active profile set, falling back to default profiles: default
2021-04-13 16:04:37.199  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Started DemoServiceAuthApplication in 0.814 seconds (JVM running for 6.425)
2021-04-13 16:04:37.203  INFO 19240 --- [           main] o.akazakov.keycloak.KeycloakAuthClient   : Try to authenticate
2021-04-13 16:04:53.697  INFO 19240 --- [           main] o.akazakov.keycloak.KeycloakAuthClient   : Authentication success
2021-04-13 16:04:53.697  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Make request to resource server
2021-04-13 16:04:54.088  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Response data: service info
Disconnected from the target VM, address: '127.0.0.1:57479', transport: 'socket'

Process finished with exit code 0

Of course, presented in the example above is strictly for informational purposes only. KeycloakAuthClient cannot be used in a production environment, at least you need to add support for saving the access token for a while, or even better, support for the mechanism for updating the access token when it expires.

conclusions

Connecting keycloak using the supplied adapter, of course, saves us from writing a lot of code and configuration. But then our application will be tied to a specific implementation of the authorization service. Connecting using only the capabilities of the spring framework gives us more flexibility in customization and more choice in implementations. But at the same time it forces us to write more code and configuration, although, in my opinion, not that much. In any case, when choosing how to connect the authorization service to our application, we must proceed from many parameters, the main of which is common sense.

Thanks for attention!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *