简体   繁体   中英

OAuth2 client credentials flow via Spring Boot Keycloak integration

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 .

Yes, you can use OAuth 2.0 Client Credentials flow and Service Accounts.

Keycloak suggest 3 ways to secure SpringBoot REST services:

  1. with Keycloak Spring Boot Adapter
  2. with keycloak Spring Security Adapter
  3. with OAuth2 / OpenID Connect

Here is a good explanation about this with an example in the OAuth2/OIDC way:

If you follow this example, keep in mind:

Take care to configure your client as:

  • Access Type: Confidential
  • Authorization: Enabled
  • Service Account (OAuth Client Credentials Flow): Enabled

Take care to configure your target service as:

  • Access Type: Bearer-only

So, caller should be confidential and target service should be bearer-only .

Create your users, roles, mappers... and assign roles to your users.

Check that you have this dependencies in your spring project:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

Configure authentication to be used in the REST client (application.properties) eg:

security.oauth2.client.client-id=employee-service
security.oauth2.client.client-secret=68977d81-c59b-49aa-aada-58da9a43a850
security.oauth2.client.user-authorization-uri=${rest.security.issuer-uri}/protocol/openid-connect/auth
security.oauth2.client.access-token-uri=${rest.security.issuer-uri}/protocol/openid-connect/token
security.oauth2.client.scope=openid
security.oauth2.client.grant-type=client_credentials

Implement your JwtAccessTokenCustomizer and SecurityConfigurer (ResourceServerConfigurerAdapter) like Arun's sample.

And finally implement your service Controller:

@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeRestController {

  @GetMapping(path = "/username")
  @PreAuthorize("hasAnyAuthority('ROLE_USER')")
  public ResponseEntity<String> getAuthorizedUserName() {
    return ResponseEntity.ok(SecurityContextUtils.getUserName());
  }

  @GetMapping(path = "/roles")
  @PreAuthorize("hasAnyAuthority('ROLE_USER')")
  public ResponseEntity<Set<String>> getAuthorizedUserRoles() {
    return ResponseEntity.ok(SecurityContextUtils.getUserRoles());
  }
}

For a complete tutorial, please read the referred Arun's tutorial.

Hope it helps.

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.

We had similar requirement, get user e-mail by user uuid.

Create service user, make sure user has role "realm-management"->"view-users" (might be query-users as well)

Process is simple: do login to keycloak with service user (keep password and/or user name encoded in the properties file), make request to keycloak with accessToken in authorisation header to

GET http://{yourdomainadress}/auth/admin/realms/{yourrealmname}/users/{userId}

A way to login to keycloak using REST API:

POST http://{yourdomainadress}/auth/realms/{yourrealmname}/protocol/openid-connect/token

Headers:

Content-Type: application/x-www-form-urlencoded

Body x-www-form-urlencoded:

client_id: your-client

username: user-you-are-using

password: password-for-user

grant_type: password

client_secret: 11112222-3333-4444-5555-666666666666 (client secret is required if client "Access Type"="confidential")

Shortly : make sure your service user has right role assigned to do operation, do login, do query keycloak (check docs to get right query url and params, always challenging)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM