Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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 za.co.absa.loginclient.tokenRetrieval.model.{AccessToken, AuthMethod, BasicAuth, KerberosAuth, RefreshToken}

import java.net.URLEncoder
import java.util.{Collections, Properties}
Expand All @@ -39,116 +39,50 @@ case class TokenRetrievalClient(host: String) {
private val logger: Logger = LoggerFactory.getLogger(this.getClass)

/**
* This method requests an access token (JWT) from the login service using the specified username and password.
* This method requests an access token (JWT) from the login service using the specified authentication method.
* This Token is used to access resources which utilize the login Service for authentication.
*
* @param username The username used for authentication.
* @param password The password associated with the provided username.
* @param groups An optional list of PAM groups. If provided, only JWTs associated with these groups are returned if the user belongs to them.
* @param caseSensitiveGroups A boolean indicating whether the group prefixes should be treated as case sensitive.
* @param authMethod The authentication method to use. Either Basic Auth or Kerberos Auth.
* @param groups A list of group prefixes to include in the token.
* @param caseSensitiveGroups Whether the group prefixes are case sensitive.
* @return An AccessToken object representing the retrieved access token (JWT) from the login service.
*/
def fetchAccessToken(
username: String,
password: String,
groups: List[String],
caseSensitiveGroups: Boolean): AccessToken = {
fetchAccessAndRefreshToken(username, password, groups, caseSensitiveGroups)._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.
* @param caseSensitiveGroups A boolean indicating whether the group prefixes should be treated as case sensitive.
* @return An AccessToken object representing the retrieved access token (JWT) from the login service.
*/
def fetchAccessToken(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This being a library, we should not just remove API as if it was never there between versions.
The existing API should stay in place with @deprecated annotation (ideally internally calling the new methods and thus be ready for removal in the future).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, will do

keytabLocation: Option[String],
userPrincipal: Option[String],
groups: List[String],
caseSensitiveGroups: Boolean): AccessToken = {
fetchAccessAndRefreshToken(keytabLocation, userPrincipal, groups, caseSensitiveGroups)._1
authMethod: AuthMethod,
groups: List[String] = List.empty,
caseSensitiveGroups: Boolean = false
): AccessToken = {
fetchAccessAndRefreshToken(authMethod, groups, caseSensitiveGroups)._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.
* @param authMethod The authentication method to use. Either Basic Auth or Kerberos Auth.
* @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, false)._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.
*
* @param username The username used for authentication.
* @param password The password associated with the provided username.
* @return A RefreshToken object representing the retrieved refresh token from the login service.
*/
def fetchRefreshToken(username: String, password: String): RefreshToken = {
fetchAccessAndRefreshToken(username, password, List.empty, false)._2
def fetchRefreshToken(authMethod: AuthMethod): RefreshToken = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither the new method nor the existing (which should be present for some time as described above) seem to be covered with unit tests.

That is unfortunate, because the appropriate class TokenRetrievalClientTest already exists and it would be nice to have it covered. I see two options:

  1. You may need to mock the creation of RestTemplate & KerberosRestTemplate (could be done by moving the creation to a method that you override in the test) to write the unit test properly.
  2. A half-measure would be just to mock the fetchToken methods in the class to check if those are receiving expected parameters. Not a complete unit test, but even this would be better than nothing if the option 1 cannot be reasonably done (I believe it should be).

fetchAccessAndRefreshToken(authMethod)._2
}

/**
* Fetches both an access token and a refresh token from the login service using the provided username, password, and optional groups.
* This method requests both an access token and a refresh token (JWTs) from the login service using the specified username and password.
* 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 username The username used for authentication.
* @param password The password associated with the provided username.
* @param groups An optional list of PAM groups. If provided, only JWTs associated with these groups are returned if the user belongs to them.
* @param caseSensitiveGroups A boolean indicating whether the group prefixes should be treated as case sensitive.
* @return A tuple containing the AccessToken and RefreshToken objects representing the retrieved access and refresh tokens (JWTs) from the login service.
* Additionally, it allows specifying optional groups (and their case sensitivity) that act as filters for the JWT,
* returning only the JWTs associated with the provided groups if the user belongs to them.
* @param authMethod The authentication method to use.
* @param groups A list of group prefixes to include in the token.
* @param caseSensitiveGroups Whether the group prefixes are case sensitive.
* @return A tuple containing the access token and the refresh token.
*/
def fetchAccessAndRefreshToken(
username: String,
password: String,
groups: List[String],
caseSensitiveGroups: Boolean
): (AccessToken, RefreshToken) = {
val issuerUri = if(groups.nonEmpty) {
val commaSeparatedString = groups.mkString(",")
val urlEncodedGroups = URLEncoder.encode(commaSeparatedString, "UTF-8")
var uri = s"$host/token/generate?group-prefixes=$urlEncodedGroups"
if(caseSensitiveGroups) {
uri += "&case-sensitive=true"
}
uri
} else s"$host/token/generate"

val jsonString = fetchToken(issuerUri, username, password)
val jsonObject = JsonParser.parseString(jsonString).getAsJsonObject
val accessToken = jsonObject.get("token").getAsString
val refreshToken = jsonObject.get("refresh").getAsString
(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.
* @param caseSensitiveGroups A boolean indicating whether the group prefixes should be treated as case sensitive.
* @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],
caseSensitiveGroups: Boolean
authMethod: AuthMethod,
groups: List[String] = List.empty,
caseSensitiveGroups: Boolean = false
): (AccessToken, RefreshToken) = {

val issuerUri = if(groups.nonEmpty) {
val commaSeparatedString = groups.mkString(",")
val urlEncodedGroups = URLEncoder.encode(commaSeparatedString, "UTF-8")
Expand All @@ -159,13 +93,27 @@ case class TokenRetrievalClient(host: String) {
uri
} else s"$host/token/generate"

val jsonString = fetchToken(issuerUri, keytabLocation, userPrincipal)
val jsonString = authMethod match {
case BasicAuth(username, password) =>
fetchToken(issuerUri, username, password)
case KerberosAuth(keytabLocation, userPrincipal) =>
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))
}

/**
* Refreshes an access token using a refresh token.
*
* @param accessToken The access token to refresh.
* @param refreshToken The refresh token to use.
* @return A tuple containing the new access token and the existing refresh token.
*/

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

Expand Down Expand Up @@ -204,6 +152,14 @@ case class TokenRetrievalClient(host: String) {
}
}

/**
* Sets the Kerberos properties for the JVM.
*
* @param jaasFileLocation The location of the JAAS file.
* @param krb5FileLocation The location of the krb5 file.
* @param debug Whether to enable Kerberos debugging.
*/

def setKerberosProperties(jaasFileLocation: String, krb5FileLocation: Option[String], debug: Option[Boolean]): Unit = {
val properties: Properties = new Properties()
properties.setProperty("java.security.auth.login.config", jaasFileLocation)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.loginclient.tokenRetrieval.model

sealed trait AuthMethod
case class BasicAuth(username: String, password: String) extends AuthMethod
case class KerberosAuth(keytabLocation: Option[String], userPrincipal: Option[String]) extends AuthMethod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package za.co.absa.loginclient.tokenRetrieval.client

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import za.co.absa.loginclient.tokenRetrieval.model.{AccessToken, RefreshToken}
import za.co.absa.loginclient.tokenRetrieval.model.{AccessToken, BasicAuth, RefreshToken}

class TokenRetrievalClientTest extends AnyFlatSpec with Matchers{

Expand All @@ -39,10 +39,10 @@ class TokenRetrievalClientTest extends AnyFlatSpec with Matchers{
"fetchAccessAndRefreshToken" should "return expected tokens" in {

val testClient = new testTokenRetrievalClient
val authMethod = BasicAuth(dummyUser, dummyPassword)

val (accessResult, refreshResult) = testClient.fetchAccessAndRefreshToken(
dummyUser,
dummyPassword,
authMethod,
dummyGroups,
dummyCaseSensitive)
accessResult shouldBe AccessToken("mock-access-token")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import za.co.absa.clientexample.config.ConfigProvider
import za.co.absa.loginclient.exceptions.LsJwtException
import za.co.absa.loginclient.authorization.{AccessTokenClaimsParser, AccessTokenVerificator, JwtDecoderProvider}
import za.co.absa.loginclient.tokenRetrieval.client.TokenRetrievalClient
import za.co.absa.loginclient.tokenRetrieval.model.{BasicAuth, KerberosAuth}

import java.nio.file.{Files, Paths}
import java.util.Scanner
Expand Down Expand Up @@ -91,9 +92,11 @@ object Application {
try {
val (accessToken, refreshToken) = authMethod match {
case "1" =>
tokenRetriever.fetchAccessAndRefreshToken(username, password, List.empty, false)
val basicAuth = BasicAuth(username, password)
tokenRetriever.fetchAccessAndRefreshToken(basicAuth)
case "2" =>
tokenRetriever.fetchAccessAndRefreshToken(None, None, List.empty, false)
val kerberosAuth = KerberosAuth(None, None)
tokenRetriever.fetchAccessAndRefreshToken(kerberosAuth)
}
val decodedAtJwt = accessVerificator.decodeAndVerifyAccessToken(accessToken) // throw Exception on verification fail
loggedIn = true
Expand Down
Loading