OAuth2 client credentials flow via Spring Boot Keycloak integration

Steffen Harbich :

My application consists of:

  • backend/resource server
  • UI webapp
  • keycloak

The UI is talking with the backend server via RESTful API using the keycloak client with authorization code grant flow. This is working fine.

Now, I need the additional possibility to access resource of the backend using a system/service account (with usually more permissions than the user). How would you implement this requirement? I thought the client credentials flow would be useful here.

Is it possible to use the OAuth2 client credentials flow with the keycloak client for Spring Boot? I found examples that used the Spring Security OAuth2 client features to achieve a client credentials flow but that feels weird because I already use the keycloak client for the OAuth thing.

Edit: Solution

Thanks for your answers which helped me a lot. In my UI webapp, I am now able to communicate with the backend either by using the authenticated user OAuth2 token or by using the token from the client credentials flow of my UI service account. Each way has its own RestTemplate, the first is done via the keycloak integration and second is done by Spring Security OAuth2 as explained here.

Carlos Cavero :

Following @dmitri-algazin you to implement the workflow you have basically two options:

  1. If you want to cover other IdMs besides Keycloak which solves somehow the Single Responsibility principle, I would use RestTemplate. Below you can find the variables:
    //Constants
    @Value("${keycloak.url}")
    private String keycloakUrl;

    @Value("${keycloak.realm}")
    private String keycloakRealm;

    @Value("${keycloak.client_id}")
    private String keycloakClientId;

    RestTemplate restTemplate = new RestTemplate();
    private static final String BEARER = "BEARER ";

First you need to generate the access token:

    @Override
    public AccessTokenResponse login(KeycloakUser user) throws NotAuthorizedException {
        try {
            String uri = keycloakUrl + "/realms/" + keycloakRealm + 
                    "/protocol/openid-connect/token";
            String data = "grant_type=password&username="+
                    user.getUsername()+"&password="+user.getPassword()+"&client_id="+
                    keycloakClientId;

            HttpHeaders headers = new HttpHeaders();
            headers.set("Content-Type", "application/x-www-form-urlencoded");

            HttpEntity<String> entity = new HttpEntity<String>(data, headers);
            ResponseEntity<AccessTokenResponse> response = restTemplate.exchange(uri, 
                    HttpMethod.POST, entity, AccessTokenResponse.class);            

            if (response.getStatusCode().value() != HttpStatus.SC_OK) {
                log.error("Unauthorised access to protected resource", response.getStatusCode().value());
                throw new NotAuthorizedException("Unauthorised access to protected resource");
            }
            return response.getBody();
        } catch (Exception ex) {
            log.error("Unauthorised access to protected resource", ex);
            throw new NotAuthorizedException("Unauthorised access to protected resource");
        } 
    }

And then with the token you can retrieve information from the users:

    @Override
    public String user(String authToken) throws NotAuthorizedException {

        if (! authToken.toUpperCase().startsWith(BEARER)) {
            throw new NotAuthorizedException("Invalid OAuth Header. Missing Bearer prefix");
        }

        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", authToken);

        HttpEntity<String> entity = new HttpEntity<>(headers);

        ResponseEntity<AccessToken> response = restTemplate.exchange(
                keycloakUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/userinfo", 
                HttpMethod.POST, 
                entity, 
                AccessToken.class);

        if (response.getStatusCode().value() != HttpStatus.SC_OK) {
            log.error("OAuth2 Authentication failure. "
                    + "Invalid OAuth Token supplied in Authorization Header on Request. Code {}", response.getStatusCode().value());
            throw new NotAuthorizedException("OAuth2 Authentication failure. "
                    + "Invalid OAuth Token supplied in Authorization Header on Request.");
        }

        log.debug("User info: {}", response.getBody().getPreferredUsername());
        return response.getBody().getPreferredUsername();
    }

You can substitute this URL by the one provided by @dimitri-algazin to retrieve all the users information.

  1. It is possible to use the Keycloak dependencies:
        <!-- keycloak -->
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-admin-client</artifactId>
            <version>3.4.3.Final</version>
        </dependency>

        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-client</artifactId>
            <version>3.1.4.Final</version>
        </dependency>

And use the classes to generate the token:

            Keycloak keycloak = KeycloakBuilder
                    .builder()
                    .serverUrl(keycloakUrl)
                    .realm(keycloakRealm)
                    .username(user.getUsername())
                    .password(user.getPassword())
                    .clientId(keycloakClientId)
                    .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(10).build())
                    .build();

            return keycloak.tokenManager().getAccessToken();

The examples are extracted from here. We also uploaded the image to Docker Hub to facilitate the interaction with Keycloak. For this reason we started with option 2). Right now we are in the process to cover other IdMs and we went for option 1) in order to avoid including extra dependencies. Conclusion:

I would go for option 2 if you stick to Keycloak because classes include extra functionalities for Keycloak tool. I would go for option 1 for further coverage and other OAuth 2.0 tools.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=163616&siteId=1