Skip to content

Commit

Permalink
Add new spnego support to the documentation examples and possibly the…
Browse files Browse the repository at this point in the history
… client library (#105)

* Add details to Readme

* Added Kerberos support to client Library and Example

* Add additional Info to Readme

* Add Jaas config and kerberos to example config

* Added new functionality to README

* Update Readme with comments
  • Loading branch information
TheLydonKing authored Oct 21, 2024
1 parent 47c779b commit b3231b3
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 23 deletions.
147 changes: 143 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ On the side of the integrator, in order to trust the access token, one should do
2. verify that the access token
1. is valid against this public-key (e.g. using `jwtt` library or similar)
2. is not expired
3. has `type=access`

3. has `type=access`

## API documentation:
Swagger doc site is available at `http://localhost:port/swagger-ui.html`
Expand Down Expand Up @@ -88,8 +87,6 @@ sbt
service / Tomcat / start
```



## Authentication Providers
### Enabling and Selecting Authentication Providers
The Login Service allows users to select which authentication providers they would like to use
Expand Down Expand Up @@ -141,6 +138,41 @@ Format of attributes list under LDAP in config is:

`ldapFieldName` is the name of the field in the LDAP server and `claimName` is the name of the claim that will be added to the JWT token.

### Enabling SPNEGO authentication with Ldap
When Ldap authentication is used, there is the option of adding SPNEGO authentication via kerberos.
This will allow users to authenticate via Basic Auth or Kerberos Tickets.
The Config to enable this will look like this:
```
ldap:
# Auth Protocol
# Set the order of the protocol starting from 1
# Set to 0 to disable or simply exclude the ldap tag from config
# NOTE: At least 1 auth protocol needs to be enabled
order: 2
domain: "some.domain.com"
url: "ldaps://some.domain.com:636/"
search-filter: "(samaccountname={1})"
service-account:
account-pattern: "CN=%s,OU=Users,OU=CORP Accounts,DC=corp,DC=dsarena,DC=com"
in-config-account:
username: "svc-ldap"
password: "password"
enable-kerberos:
krb-file-location: "/etc/krb5.conf"
keytab-file-location: "/etc/keytab"
spn: "HTTP/Host"
debug: true
attributes:
<ldapFieldName>: "<claimName>"
```

Adding the `enable-kerberos` property to the config will enable SPNEGO authentication.
In order to facilitate the kerberos authentication, you will need to provide a krb5 file that includes
the kerberos configuration details such as domains and Kerberos distribution center address.
A Keytab file needs to be created and attached to the service. The SPN needs to match that which
is used in the keytab and matches the host of the Login Service. The `debug` property is used when
additional information is required from the logs when testing the service.

### ActiveDirectoryLDAPAuthenticationProvider
Uses LDAP(s) to authenticate user in Active Directory and to fetch groups that this user belongs to.

Expand Down Expand Up @@ -208,6 +240,113 @@ There are a few important configuration values to be provided:

Please note that only one configuration option (`loginsvc.rest.jwt.{aws-secrets-manager|generate-in-memory}`) can be used at a time.

## Generating Tokens via SPNEGO/Kerberos
To securely authenticate and retrieve a JWT token from a server using Kerberos and SPNEGO, clients (both Windows and Linux) need to be properly configured for Kerberos authentication.
The process involves obtaining a Kerberos ticket and using it to authenticate to the endpoint.

### Steps for Windows Clients
#### 1) Kerberos Configuration:

- Ensure the Windows client is joined to the appropriate Active Directory (AD) domain.
- Verify that the Kerberos configuration is correct in the `krb5.ini` file, typically located in `C:\ProgramData\MIT\Kerberos5\` or `C:\Windows\`.
The `krb5.ini` file should include the correct realm and KDC settings. An example configuration might look like this:
```
[libdefaults]
default_realm = YOURDOMAIN.COM
dns_lookup_realm = false
dns_lookup_kdc = true
[realms]
YOURDOMAIN.COM = {
kdc = kdc.yourdomain.com
admin_server = kdc.yourdomain.com
}
[domain_realm]
.yourdomain.com = YOURDOMAIN.COM
yourdomain.com = YOURDOMAIN.COM
```
#### 2) Optional: MIT Kerberos Installation:

- While Windows has built-in Kerberos support, you may choose to install MIT Kerberos if you need advanced features or compatibility with specific applications.
- Download the installer from the [MIT Kerberos website](https://web.mit.edu/kerberos/dist/).
- Follow the installation instructions, and ensure the krb5.ini file is properly configured as mentioned above.

#### 3) Check Kerberos Tickets:

- Use the `klist` command in the Command Prompt or use MIT Kerberos to verify the presence of a valid Kerberos ticket.
```
Credentials cache: API:1000
Principal: [email protected]
Cache version: 5
Ticket cache: /tmp/krb5cc_1000
Default principal: [email protected]
Valid starting Expires Service principal
10/20/2024 10:00:00 10/20/2024 20:00:00 krbtgt/[email protected]
10/20/2024 10:00:00 10/20/2024 20:00:00 host/[email protected]
```

#### 4) Environment Setup:

- Ensure that the required libraries (e.g., SPNEGO) are available in your application or tool (e.g., Postman, Curl).

#### 5) Sending the POST Request:

- Construct a POST request to the desired endpoint.
- Example using Curl:
```
curl -i --negotiate -u : -X POST <endpoint-url>/token/generate
```

#### 6) Receive the JWT:

- On successful authentication, the server will respond with an access and refresh JWT tokens.

### Steps for Linux Clients
#### 1) Kerberos Installation:

- Install the necessary Kerberos packages (e.g., krb5-libs).

#### 2) Kerberos Configuration:

- Locate and, if necessary, replace krb5.conf. The krb5.conf file is typically located in /etc/krb5.conf.
- Ensure it includes the correct realm and KDC (Key Distribution Center) settings. A basic configuration might look like this:
```
[libdefaults]
default_realm = YOURDOMAIN.COM
dns_lookup_realm = false
dns_lookup_kdc = true
[realms]
YOURDOMAIN.COM = {
kdc = kdc.yourdomain.com
admin_server = kdc.yourdomain.com
}
[domain_realm]
.yourdomain.com = YOURDOMAIN.COM
yourdomain.com = YOURDOMAIN.COM
```

#### 3) Obtaining a Kerberos Ticket:

- Use the following command to obtain a Kerberos ticket (A password may be required):
```
kinit [email protected]
```

#### 4) Sending the POST Request:

- Use a tool like Curl to send a POST request:
```
curl -i --negotiate -u : -X POST <endpoint-url>/token/generate
```

#### 5) Receive the JWT:

- On successful authentication, the server will respond with an access and refresh JWT tokens.

## How to generate Code coverage report
```
sbt jacoco
Expand Down
11 changes: 8 additions & 3 deletions clientLibrary/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ Public Key is available without authorization so just the relevant host needs to
## Token retrieval

The library provides a `TokenRetrievalClient` class that can be used to retrieve access and refresh tokens.
Refresh and Access Keys require authorization. Basic Auth is used for the initial retrieval so a valid username and password is required.
Refresh and Access Keys require authorization.
There are 2 authentication methods available:
1) Basic Auth is used for the initial retrieval so a valid username and password is required.
Please see the [login-service documentation](README.md) for more information on what a valid username and password is.
2) Spnego authentication. Please ensure that kerberos is enabled and configured correctly in your environment.
In order to support kerberos, we allow for the use of Keytabs as well as the use of Ticketcache authentication.
If required, you may specify a jaas configuration file and custom krb5 location programmatically using the `setKerberosProperties` function.

Refresh token from initial retrieval is used to refresh the access token.

## Creating and Using a JWT Decoder
Expand Down Expand Up @@ -66,5 +72,4 @@ An example of how to use the library can be found in the [examples](examples) fo
The example makes use of a [configuration file](examples/src/main/resources/example.application.yaml) to provide the necessary configuration to the library.

Configurations required are:
1. `host` - the url of the login-service (Including Port if required)
2. 'refresh-period' - the period between refreshing the public-key used for verification. This Parameter is optional.
1. `host` - the url of the login-service (Including Port if required)
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ package za.co.absa.loginclient.tokenRetrieval.client
import com.google.gson.{JsonObject, JsonParser}
import org.slf4j.{Logger, LoggerFactory}
import org.springframework.http.{HttpEntity, HttpHeaders, HttpMethod, MediaType, ResponseEntity}
import org.springframework.security.kerberos.client.KerberosRestTemplate
import org.springframework.web.client.RestTemplate
import za.co.absa.loginclient.tokenRetrieval.model.{AccessToken, RefreshToken}

import java.net.URLEncoder
import java.util.Collections
import java.util.{Collections, Properties}
import javax.security.auth.login.Configuration

/**
* This class is used to retrieve tokens from the login service.
Expand All @@ -45,10 +47,35 @@ case class TokenRetrievalClient(host: String) {
* @param groups An optional list of PAM groups. If provided, only JWTs associated with these groups are returned if the user belongs to them.
* @return An AccessToken object representing the retrieved access token (JWT) from the login service.
*/
def fetchAccessToken(username: String, password: String, groups: List[String] = List.empty): AccessToken = {
def fetchAccessToken(username: String, password: String, groups: List[String]): AccessToken = {
fetchAccessAndRefreshToken(username, password, groups)._1
}

/**
* This method requests an access token (JWT) from the login service using SPNEGO.
* This Token is used to access resources which utilize the login Service for authentication.
*
* @param keytabLocation Optional location of the keytab file.
* @param userPrincipal Optional userPrincipal name included in the above keytab file.
* @param groups An optional list of PAM groups. If provided, only JWTs associated with these groups are returned if the user belongs to them.
* @return An AccessToken object representing the retrieved access token (JWT) from the login service.
*/
def fetchAccessToken(keytabLocation: Option[String], userPrincipal: Option[String], groups: List[String]): AccessToken = {
fetchAccessAndRefreshToken(keytabLocation, userPrincipal, groups)._1
}

/**
* This method requests a refresh token from the login service using SPNEGO.
* This token may be used to acquire a new access token (JWT) when the current access token expires.
*
* @param keytabLocation Optional location of the keytab file.
* @param userPrincipal Optional userPrincipal name included in the above keytab file.
* @return A RefreshToken object representing the retrieved refresh token from the login service.
*/
def fetchRefreshToken(keytabLocation: Option[String], userPrincipal: Option[String]): RefreshToken = {
fetchAccessAndRefreshToken(keytabLocation, userPrincipal, List.empty)._2
}

/**
* This method requests a refresh token from the login service using the specified username and password.
* This token may be used to acquire a new access token (JWT) when the current access token expires.
Expand All @@ -58,7 +85,7 @@ case class TokenRetrievalClient(host: String) {
* @return A RefreshToken object representing the retrieved refresh token from the login service.
*/
def fetchRefreshToken(username: String, password: String): RefreshToken = {
fetchAccessAndRefreshToken(username, password)._2
fetchAccessAndRefreshToken(username, password, List.empty)._2
}

/**
Expand All @@ -71,7 +98,7 @@ case class TokenRetrievalClient(host: String) {
* @param groups An optional list of PAM groups. If provided, only JWTs associated with these groups are returned if the user belongs to them.
* @return A tuple containing the AccessToken and RefreshToken objects representing the retrieved access and refresh tokens (JWTs) from the login service.
*/
def fetchAccessAndRefreshToken(username: String, password: String, groups: List[String] = List.empty): (AccessToken, RefreshToken) = {
def fetchAccessAndRefreshToken(username: String, password: String, groups: List[String]): (AccessToken, RefreshToken) = {

val issuerUri = if(groups.nonEmpty) {
val commaSeparatedString = groups.mkString(",")
Expand All @@ -86,6 +113,31 @@ case class TokenRetrievalClient(host: String) {
(AccessToken(accessToken), RefreshToken(refreshToken))
}

/**
* Fetches both an access token and a refresh token from the login service using SPNEGO.
* This method requests both an access token and a refresh token (JWTs) from the login service using kerberos, either with a keytab or the users cached ticket.
* Additionally, it allows specifying optional groups that act as filters for the JWT, returning only the JWTs associated with the provided groups if the user belongs to them.
*
* @param keytabLocation Optional location of the keytab file.
* @param userPrincipal Optional userPrincipal name included in the above keytab file.
* @param groups An optional list of PAM groups. If provided, only JWTs associated with these groups are returned if the user belongs to them.
* @return A tuple containing the AccessToken and RefreshToken objects representing the retrieved access and refresh tokens (JWTs) from the login service.
*/
def fetchAccessAndRefreshToken(keytabLocation: Option[String], userPrincipal: Option[String], groups: List[String]): (AccessToken, RefreshToken) = {

val issuerUri = if(groups.nonEmpty) {
val commaSeparatedString = groups.mkString(",")
val urlEncodedGroups = URLEncoder.encode(commaSeparatedString, "UTF-8")
s"$host/token/generate?group-prefixes=$urlEncodedGroups"
} else s"$host/token/generate"

val jsonString = fetchToken(issuerUri, keytabLocation, userPrincipal)
val jsonObject = JsonParser.parseString(jsonString).getAsJsonObject
val accessToken = jsonObject.get("token").getAsString
val refreshToken = jsonObject.get("refresh").getAsString
(AccessToken(accessToken), RefreshToken(refreshToken))
}

def refreshAccessToken(accessToken: AccessToken, refreshToken: RefreshToken): (AccessToken, RefreshToken) = {
val issuerUri = s"$host/token/refresh"

Expand Down Expand Up @@ -124,6 +176,19 @@ case class TokenRetrievalClient(host: String) {
}
}

def setKerberosProperties(jaasFileLocation: String, krb5FileLocation: Option[String], debug: Option[Boolean]): Unit = {
val properties: Properties = new Properties()
properties.setProperty("java.security.auth.login.config", jaasFileLocation)
properties.setProperty("sun.security.krb5.debug", debug.getOrElse(false).toString)

if(krb5FileLocation.nonEmpty)
properties.setProperty("java.security.krb5.conf", krb5FileLocation.get)

Configuration.getConfiguration.refresh()

System.setProperties(properties)
}

private def fetchToken(issuerUri: String, username: String, password: String): String = {

logger.info(s"Fetching token from $issuerUri for user $username")
Expand Down Expand Up @@ -152,4 +217,35 @@ case class TokenRetrievalClient(host: String) {
throw e
}
}

private def fetchToken(issuerUri: String, keyTabLocation: Option[String], userPrincipal: Option[String]): String = {

val restTemplate: KerberosRestTemplate = (keyTabLocation, userPrincipal) match {
case (Some(_), Some(_)) =>
logger.info(s"Fetching token from $issuerUri using user $userPrincipal")
new KerberosRestTemplate(keyTabLocation.get, userPrincipal.get)
case (None, None) =>
logger.info(s"Fetching token from $issuerUri using cached user ticket")
new KerberosRestTemplate()
case _ =>
throw new Error("Either both keyTabLocation and userPrincipal need to be available or omitted")
}

val headers = new HttpHeaders()
val entity = new HttpEntity[String](null, headers)

try {
val response: ResponseEntity[String] = restTemplate.exchange(
issuerUri,
HttpMethod.POST,
entity,
classOf[String])
response.getBody
}
catch {
case e: Throwable =>
logger.error(s"Error occurred retrieving and decoding Token from $issuerUri", e)
throw e
}
}
}
4 changes: 4 additions & 0 deletions examples/src/main/resources/example.application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
login-service:
example:
host: "http://localhost:9090"
#kerberos:
#jaas-file-location: "location/of/jaas.conf"
#krb-file-location: "location/of/krb5.conf" #Optional
#debug: "true" #Optional
8 changes: 8 additions & 0 deletions examples/src/main/resources/example.jaas.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
KerberosClient {
com.sun.security.auth.module.Krb5LoginModule required
useTicketCache=true
ticketCache="ticket/cache/location"
principal="principal.Domain.COM"
debug=true; #Enable debug output (optional)
};

Loading

0 comments on commit b3231b3

Please sign in to comment.