> ## 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.

# Add Kerberos machine-to-machine authentication

> How to configure Kerberos machine-to-machine authentication in Atoti Limits, covering client and server security configuration, KerberosRestTemplate, authentication providers, SpnegoEntryPoint, and the filter chain

## Overview

As [Kerberos](https://web.mit.edu/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:

```xml theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
<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

<Accordion title="KerberosRestTemplate">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  @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);
    }
  ```
</Accordion>

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.

<Accordion title="KerberosAuthenticationProvider">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  @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;
  }
  ```
</Accordion>

<Accordion title="KerberosServiceAuthenticationProvider">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
    @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;
    }
  ```

  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  @Bean
  public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
    SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
    ticketValidator.setServicePrincipal(servicePrincipal);
    ticketValidator.setKeyTabLocation(new FileSystemResource(keytabLocation));
    ticketValidator.setDebug(true);
    return ticketValidator;
  }
  ```
</Accordion>

#### Authentication manager

The authentication manager used by Spring needs to use the authentication providers. The standard approach
is to inject them into the manager.

<Accordion title="AuthenticationManager">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  @Bean
  public AuthenticationManager globalAuthenticationManager(
      List<AuthenticationProvider> authenticationProviders) {
    return new ProviderManager(authenticationProviders);
  }
  ```
</Accordion>

#### 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.

<Note>
  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.
</Note>

<Accordion title="SpnegoEntryPoint">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  @Bean
  public SpnegoEntryPoint spnegoEntryPoint() {
    return new SpnegoEntryPoint();
  }
  ```
</Accordion>

#### 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.

<Accordion title="SpnegoAuthenticationProcessingFilter">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  @Bean
  public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
      AuthenticationManager authenticationManager) {
    SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
    filter.setAuthenticationManager(authenticationManager);
    return filter;
  }
  ```
</Accordion>

#### 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:

```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
@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:

<Accordion title="UserDetailsService">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  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"));
      }
    };
  }
  ```
</Accordion>

<Note>
  This `serviceUserDetailsService` is not exposed as a Spring Bean, to avoid conflicts
  with other `UserDetailsService` beans that may be present in your application.
</Note>

<Tip>
  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.
</Tip>

## 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**.

<Warning>
  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.
</Warning>

### Implement the required REST clients

As discussed in [Adding Custom Machine-to-Machine (MtM) Authentication](.#how-to-add-custom-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:

```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
   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**.

<Note>
  The API of `ILimitsRestClientBuilder` provides a `RestClient` rather than a `RestTemplate` as it
  is the most modern [Spring REST client](https://docs.spring.io/spring-framework/reference/integration/rest-clients.).
  We expect the `spring-security-kerberos` project to eventually implement a `KerberosRestClient`.
</Note>

A sample of the changes required for your `ILimitsConnector` is provided below.

<Accordion title="KerberosLimitsConnector">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  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;
    }

    ...

  }
  ```
</Accordion>

#### 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.

<Accordion title="KerberosWebClientService">
  ```java theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
  @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);
      }
    }
  }
  ```
</Accordion>

## Further resources

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

* [Spring Security Kerberos](https://docs.spring.io/spring-security-kerberos/reference/index.)
* [Spring Security Kerberos examples](https://github.com/spring-projects/spring-security-kerberos/tree/main/spring-security-kerberos-samples)
* [Spring Security Kerberos Integration with MiniKdc](https://www.baeldung.com/spring-security-kerberos-integration)
* [SPNEGO/Kerberos Authentication in Spring](https://www.baeldung.com/spring-security-kerberos)
