From faeadc64b41ad05984494f2950b1f2453557023f Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 3 Dec 2025 11:26:52 +0200 Subject: [PATCH 1/2] Made Groups and Case Sensitivity optional --- .../client/TokenRetrievalClient.scala | 142 ++++++------------ .../tokenRetrieval/model/AuthMethod.scala | 21 +++ .../client/TokenRetrievalClientTest.scala | 6 +- .../co/absa/clientexample/Application.scala | 7 +- 4 files changed, 77 insertions(+), 99 deletions(-) create mode 100644 clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala diff --git a/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala index 1a1e031e..0b8ef4b6 100644 --- a/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala +++ b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala @@ -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} @@ -39,116 +39,48 @@ 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 Token is used to access resources which utilize the login Service for authentication. + * Fetches an access token from the login service. * - * @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 An AccessToken object representing the retrieved access token (JWT) from the login service. + * @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 The access token. */ - 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( - 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. + * Fetches a refresh token from the login service. * - * @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. + * @param authMethod The authentication method to use. + * @return The refresh token. */ - 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 = { + 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. + * Fetches both an access token and a refresh token from the login service. * - * @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. + * @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") @@ -159,13 +91,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" @@ -204,6 +150,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) diff --git a/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala new file mode 100644 index 00000000..69838f70 --- /dev/null +++ b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala @@ -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 \ No newline at end of file diff --git a/clientLibrary/src/test/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClientTest.scala b/clientLibrary/src/test/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClientTest.scala index f7ddbbf7..d1d0ee41 100644 --- a/clientLibrary/src/test/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClientTest.scala +++ b/clientLibrary/src/test/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClientTest.scala @@ -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{ @@ -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") diff --git a/examples/src/main/scala/za/co/absa/clientexample/Application.scala b/examples/src/main/scala/za/co/absa/clientexample/Application.scala index e535bdc8..ad9065e5 100644 --- a/examples/src/main/scala/za/co/absa/clientexample/Application.scala +++ b/examples/src/main/scala/za/co/absa/clientexample/Application.scala @@ -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 @@ -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 From a120511c7c9f10a4541d11fdb49db664a005b365 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 3 Dec 2025 11:44:51 +0200 Subject: [PATCH 2/2] Refactor descriptions --- .../client/TokenRetrievalClient.scala | 22 ++++++++++--------- .../tokenRetrieval/model/AuthMethod.scala | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala index 0b8ef4b6..b1469e82 100644 --- a/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala +++ b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/client/TokenRetrievalClient.scala @@ -39,12 +39,12 @@ case class TokenRetrievalClient(host: String) { private val logger: Logger = LoggerFactory.getLogger(this.getClass) /** - * Fetches an access token from the login service. - * - * @param authMethod The authentication method to use. + * 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 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 The access token. + * @return An AccessToken object representing the retrieved access token (JWT) from the login service. */ def fetchAccessToken( @@ -56,10 +56,10 @@ case class TokenRetrievalClient(host: String) { } /** - * Fetches a refresh token from the login service. - * - * @param authMethod The authentication method to use. - * @return The refresh token. + * 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 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(authMethod: AuthMethod): RefreshToken = { @@ -67,8 +67,10 @@ case class TokenRetrievalClient(host: String) { } /** - * Fetches both an access token and a refresh token from the login service. - * + * 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 (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. diff --git a/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala index 69838f70..601402d9 100644 --- a/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala +++ b/clientLibrary/src/main/scala/za/co/absa/loginclient/tokenRetrieval/model/AuthMethod.scala @@ -18,4 +18,4 @@ 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 \ No newline at end of file +case class KerberosAuth(keytabLocation: Option[String], userPrincipal: Option[String]) extends AuthMethod