Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.activeviam.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

As Kerberos is an authentication protocol rather than an implementation, this section is more of a general guide than concrete instructions. However, we outline what parts of Atoti Limits you need to configure to use Kerberos for machine-to-machine (MtM) authentication with a Key Distribution Center (KDC). This guide only assumes that you have access to a keytab file containing a principal-user, who is the service user that will be used to authenticate the MtM requests. This guide does not assume anything else about your KDC. As the MtM communication is bidirectional, Atoti Limits and your connected Atoti Server act as both a client and a server at different points in time. For this reason, we recommend placing the security configurations in a shared module that can be used by both (or potentially more) applications. This guide consists of two sections. The first section shall outline from a high level the changes that need to be made. The second section shall outline where these changes should go in Atoti Limits. We will provide code samples throughout.

Kerberos Security Configuration

In general, the configuration required to use Kerberos authentication in Spring is made up of two parts:
  • Client configuration: the configuration required to authenticate the machine sending the request.
  • Server configuration: the configuration required to authenticate the machine receiving the request.
First, get the latest version of the Spring Security Kerberos dependencies:
<dependency>
  <groupId>org.springframework.security.kerberos</groupId>
  <artifactId>spring-security-kerberos-core</artifactId>
  <version>${spring-security-kerberos.version}</version>
</dependency>

<!-- used for the server configuration -->
<dependency>
<groupId>org.springframework.security.kerberos</groupId>
<artifactId>spring-security-kerberos-web</artifactId>
<version>${spring-security-kerberos.version}</version>
</dependency>

        <!-- used for the client configuration -->
<dependency>
<groupId>org.springframework.security.kerberos</groupId>
<artifactId>spring-security-kerberos-client</artifactId>
<version>${spring-security-kerberos.version}</version>
</dependency>
Next, you can add your configuration.

Client configuration

When acting as a client, the machine sending the request needs to authenticate with the KDC. This can be done using a KerberosRestTemplate, configured with the following:
  • keytabLocation: the location of the keytab file that contains the user’s credentials
  • servicePrincipal: the principal of the “service” user making the request
@Bean
public RestTemplate kerberosTemplate() {
  // the loginOptions can be useful if you want to customise Krb5LoginModule options
    Map<String, Object> loginOptions = new HashMap<>();
    loginOptions.put("debug", "true");
    loginOptions.put("storeKey", "true");
    loginOptions.put("tryFirstPass", "true");
    loginOptions.put("useFirstPass", "true");

    // the template can also be instantiated with a username and password
    return new KerberosRestTemplate(keytabLocation, servicePrincipal, loginOptions);
  }
To use the KerberosRestTemplate, inject it into your service and use it to make requests.

Server configuration

When acting as a server, the machine receiving the request needs to authenticate the user. For this, you need to implement the following components.

Kerberos authentication providers

You need to specify the Kerberos authentication providers to give an authenticated ticket to the machine requesting authorization.
  • KerberosAuthenticationProvider: authenticates client requests.
  • KerberosServiceAuthenticationProvider: authenticates service requests.
@Bean
public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
  KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
  SunJaasKerberosClient client = new SunJaasKerberosClient();
  client.setDebug(true);
  provider.setKerberosClient(client);
  // the userDetailsService is used to fetch the user roles
  provider.setUserDetailsService(serviceUserDetailsService());
  return provider;
}
  @Bean
  public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
    KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
    provider.setTicketValidator(sunJaasKerberosTicketValidator());
  // the userDetailsService is used to fetch the user roles
    provider.setUserDetailsService(serviceUserDetailsService());
    return provider;
  }
@Bean
public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
  SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
  ticketValidator.setServicePrincipal(servicePrincipal);
  ticketValidator.setKeyTabLocation(new FileSystemResource(keytabLocation));
  ticketValidator.setDebug(true);
  return ticketValidator;
}

Authentication manager

The authentication manager used by Spring needs to use the authentication providers. The standard approach is to inject them into the manager.
@Bean
public AuthenticationManager globalAuthenticationManager(
    List<AuthenticationProvider> authenticationProviders) {
  return new ProviderManager(authenticationProviders);
}

The authentication entry point

When an unauthorized actor, in our case Atoti Limits or the connected server, makes a request, they are redirected to the authentication entry point. In many applications, this is in the form of a login page. For Kerberos, the SpnegoEntryPoint is used to redirect the user to the KDC.
You can use this entry point to redirect to a login page. We won’t cover that here, because we are focusing on machine-to-machine communication, but that is how to implement SSO using Kerberos.
@Bean
public SpnegoEntryPoint spnegoEntryPoint() {
  return new SpnegoEntryPoint();
}

The authentication filter

The SpnegoAuthenticationProcessingFilter is responsible for processing the authentication request. It authenticates the user once validated by the KDC and sets the Spring authentication for the rest of the request.
@Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
    AuthenticationManager authenticationManager) {
  SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
  filter.setAuthenticationManager(authenticationManager);
  return filter;
}

The filter chain

This is where all of the above is put together. It is your responsibility to define your filter chains. This is because the filter chain contains endpoints and user roles related to your organization, including potentially other systems that are not part of Atoti Limits. The following considerations must be made for defining the Kerberos filter chain:
  • Redirect unauthorized requests to the SpnegoEntryPoint.
  • Add the SpnegoAuthenticationProcessingFilter to the filter chain to set the Authorization Negotiate xxx header.
  • Make sure the request matchers are loose enough to allow any access to vital assets to happen, such as a login page, but strict enough that private assets require authentication, for example, sensitive endpoints.
A simple filter chain for Kerberos looks like this:
@Bean
public SecurityFilterChain filterChain(final HttpSecurity http, SpnegoEntryPoint spnegoEntryPoint,
        SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter)
        throws Exception {
  return http
          .exceptionHandling(exception -> exception.authenticationEntryPoint(spnegoEntryPoint))
          .authorizeHttpRequests(auth -> auth
                  // we require that all endpoints are authentication
                  .anyRequest().authenticated())
          .addFilterBefore(spnegoAuthenticationProcessingFilter, BasicAuthenticationFilter.class)
          .build();
}

The UserDetailsService

The UserDetailsService, which usually fetches our user roles, is used only for the service user here. Note that if you want to impose user roles for your service user, this is the place to do it. As an example only, to replicate the default admin user that is shipped with Atoti Limits you can use the following:
public UserDetailsService serviceUserDetailsService() {
  return new UserDetailsService() {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      return new User(
              username,
              "notUsed",
              true,
              true,
              true,
              true,
              AuthorityUtils.createAuthorityList(
                      "ROLE_ADMIN",
                      "ROLE_CS_ROOT",
                      "ROLE_USERS",
                      "ROLE_MANAGERS",
                      "ROLE_USER",
                      "ROLE_ACTIVITI_USER",
                      "ROLE_ACTIVITI_ADMIN",
                      "ROLE_LIMITS"));
    }
  };
}
This serviceUserDetailsService is not exposed as a Spring Bean, to avoid conflicts with other UserDetailsService beans that may be present in your application.
As mentioned above, we recommend placing all of the above in a shared module that can be used by both Atoti Limits and your connected Atoti Server.

Kerberos in Atoti Limits

Once you have prepared your security configuration, you can use it in Atoti Limits and your connected server.

Import the security configuration

Import your security configuration created in the previous section into both Atoti Limits and your connected server.
It is up to you to determine if your security can be imported alongside your current security configuration, or if it should replace any existing configurations. This is because security configurations are architecture-specific, and so they differ from client to client.

Implement the required REST clients

As discussed in Adding Custom Machine-to-Machine (MtM) Authentication, when customizing authentication, users need to implement the following interfaces:
  1. ILimitsRestTemplateBuilder in the connected server for Atoti Server version 6.0.x or
  2. ILimitsRestClientBuilder in the connected server, for Atoti Server version 6.1.x or 6.0.x-sb3, and in Atoti Limits

1. ILimitsRestTemplateBuilder

In this case you only need to expose the KerberosRestTemplate that you created in the previous section.

2. ILimitsRestClientBuilder in your connected server

There is no spring-security-kerberos implementation at this time of a “Kerberos” RestClient like there is for KerberosRestTemplate. You can instantiate a RestClient with:
   RestTemplate template = new KerberosRestTemplate(...);
   RestClient restClient = RestClient.create(template);
However, the Kerberos authentication fails, because the RestTemplate itself does not manage the execution of the request. This means you will need to implement your own version of ILimitsConnector to send the requests, which should be exposed in your connected server.
The API of ILimitsRestClientBuilder provides a RestClient rather than a RestTemplate as it is the most modern Spring REST client. We expect the spring-security-kerberos project to eventually implement a KerberosRestClient.
A sample of the changes required for your ILimitsConnector is provided below.
public class KerberosLimitsConnector extends LimitsConnector {
  ...
  protected final RestTemplate kerberosTemplate;

  ...

  protected boolean limitsServerIsStarted() {
    String limitsRestUrl = autoconfigurationProperties.getLimitsRestUrl();
    if (limitsRestUrl != null) {
      try {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(APPLICATION_JSON);

        ResponseEntity<String> response =
                kerberosTemplate.exchange(
                        URI.create(autoconfigurationProperties.getLimitsPingUrl()).toString(),
                        HttpMethod.GET,
                        new HttpEntity<>(headers),
                        String.class);
        return response.getStatusCode().value() == 200 && response.getBody().equals("pong");
      } catch (ResourceAccessException ex) {
        log.warn("Could not ping Limits. Most likely the server is not started...");
        log.debug(ex.getMessage(), ex);
      }
    } else {
      log.warn("Limits URL  not provided but required! limits.rest-url = {}.", limitsRestUrl);
    }
    isConnected = false;
    return false;
  }

  ...

  protected boolean sendPropertiesToLimits() {
    String limitsRestUrl = autoconfigurationProperties.getLimitsRestUrl();
    if (limitsRestUrl != null) {
      // Limits might not be up, so let's check
      log.debug("Connecting to Limits...");
      try {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(APPLICATION_JSON);

        HttpEntity<LimitsConnectionProperties> body =
                new HttpEntity<>(autoconfigurationProperties, headers);
        ResponseEntity<String> response =
                kerberosTemplate.exchange(
                        URI.create(
                                        limitsRestUrl + LIMITS_AUTO_CONFIG_REST_SERVICE_ADDRESS
                                                + CONNECT_ENDPOINT)
                                .toString(),
                        HttpMethod.PUT,
                        body,
                        String.class);

        if (response.getStatusCode().is2xxSuccessful() && response.getBody().equals("true")) {
          log.debug("...properties now sent to Limits!");
          log.info("...connected to Limits");
          return true;
        } else {
          log.warn(
                  "Could not connect to Limits. HTTP Status : {}",
                  response.getStatusCode().value());
        }
        log.debug(response.getBody());
      } catch (RestClientException e) {
        log.warn("Could not connect to Limits. Perhaps the server is not up.", e);
      }
    } else {
      log.warn(
              "Limits Rest URL and Auth have not been specified in properties. This Active Pivot application will not try to connect to Limits.");
    }
    return false;
  }

  ...

}

3. ILimitsRestClientBuilder in Atoti Limits

As in the previous section, there is no implementation at this time of a “Kerberos” RestClient.Builder. In this case, it means you have to provide an implementation of IWebClientService in Atoti Limits. We have provided KerberosWebClientService as an example.
@Service
@RequiredArgsConstructor
public class KerberosWebClientService implements IWebClientService {

  @Qualifier("kerberosTemplate")
  protected final RestTemplate kerberosTemplate;

  @Override
  public String post(String serverName, String path, String jsonBody, String errorMessage) {
    return post(serverName, path, jsonBody, errorMessage, false);
  }

  @Override
  public String post(
      String serverName,
      String path,
      String jsonBody,
      String errorMessage,
      boolean extractResponseBody) {
    try {
      // Note that the `serverName` is not used because the KDC is now responsible for authentication rather than the target server
      HttpHeaders headers = new HttpHeaders();
      headers.set("Content-Type", "application/json");
      HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
      ResponseEntity<String> response =
          kerberosTemplate.postForEntity(path, entity, String.class);
      return response.getBody();
    } catch (RestClientException e) {
      throw new LimitsWebClientServiceException(errorMessage, e);
    }
  }

  @Override
  public String get(String url, String server, String errorMessage) {
    return get(url, server, errorMessage, false);
  }

  @Override
  public String get(String url, String server, String errorMessage, boolean extractResponseBody) {
    try {
      // Note that the `server` is not used because the KDC is now responsible for authentication rather than the target server
      ResponseEntity<String> response = kerberosTemplate.getForEntity(url, String.class);
      return response.getBody();
    } catch (RestClientException e) {
      throw new LimitsWebClientServiceException(errorMessage, e);
    }
  }

  @Override
  public String getWithAuth(String url, String authorization, String errorMessage) {
    return getWithAuth(url, authorization, errorMessage, false);
  }

  @Override
  public String getWithAuth(
      String url, String authorization, String errorMessage, boolean extractResponseBody) {
    try {
      HttpHeaders headers = new HttpHeaders();
      headers.set("Content-Type", "application/json");
      HttpEntity<String> entity = new HttpEntity<>(headers);
      ResponseEntity<String> response =
          kerberosTemplate.exchange(url, HttpMethod.GET, entity, String.class);
      return response.getBody();
    } catch (RestClientException e) {
      throw new LimitsWebClientServiceException(errorMessage, e);
    }
  }

  @Override
  public <T> ResponseEntity<T> put(
      String serverName, String path, Object body, Class<T> responseType) {
    try {
      // Note that the `serverName` is not used because the KDC is now responsible for authentication rather than the target server
      HttpHeaders headers = new HttpHeaders();
      headers.set("Content-Type", "application/json");
      HttpEntity<Object> entity = new HttpEntity<>(body, headers);
      return kerberosTemplate.exchange(path, HttpMethod.PUT, entity, responseType);
    } catch (RestClientException e) {
      throw new LimitsWebClientServiceException("Error during PUT request", e);
    }
  }
}

Further resources

Please see the following articles for further resources on integrating Kerberos with Spring: