From 88246fa15160d5a152404de8b89ac8664b2afdf4 Mon Sep 17 00:00:00 2001 From: TheLydonKing Date: Wed, 24 Apr 2024 16:40:14 +0200 Subject: [PATCH 01/33] Initial Commit --- .../main/resources/example.application.yaml | 2 +- .../absa/loginsvc/rest/SecurityConfig.scala | 7 +- ...KerberosSPNEGOAuthenticationProvider.scala | 113 ++++++++++++++++++ project/Dependencies.scala | 5 + 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala diff --git a/api/src/main/resources/example.application.yaml b/api/src/main/resources/example.application.yaml index 871b2c37..8bf16be8 100644 --- a/api/src/main/resources/example.application.yaml +++ b/api/src/main/resources/example.application.yaml @@ -56,7 +56,7 @@ loginsvc: # 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: 0 + order: 2 domain: "some.domain.com" url: "ldaps://some.domain.com:636/" search-filter: "(samaccountname={1})" diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index 326ccc97..b535c32d 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -17,10 +17,13 @@ package za.co.absa.loginsvc.rest import org.springframework.context.annotation.{Bean, Configuration} +import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter @Configuration @EnableWebSecurity @@ -48,10 +51,10 @@ class SecurityConfig { .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .httpBasic() + //.and() + //.addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManager), classOf[BasicAuthenticationFilter]) http.build() } - - } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala new file mode 100644 index 00000000..e6479731 --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -0,0 +1,113 @@ +package za.co.absa.loginsvc.rest.provider.kerberos + +import org.slf4j.LoggerFactory +import org.springframework.core.io.FileSystemResource +import org.springframework.ldap.core.DirContextOperations +import org.springframework.ldap.core.support.BaseLdapPathContextSource +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.core.{AuthenticationException, GrantedAuthority} +import org.springframework.security.core.authority.{AuthorityUtils, SimpleGrantedAuthority} +import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator +import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig +import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource +import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch +import org.springframework.security.ldap.userdetails.{LdapAuthoritiesPopulator, LdapUserDetailsMapper, LdapUserDetailsService} +import org.springframework.security.web.authentication.{AuthenticationFailureHandler, AuthenticationSuccessHandler} + +import java.util +import javax.naming.ldap.LdapName +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import scala.collection.JavaConverters._ + +class KerberosSPNEGOAuthenticationProvider { + + private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) + logger.debug(s"KerberosSPNEGOAuthenticationProvider init") + + private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = { + val ticketValidator = new SunJaasKerberosTicketValidator() + ticketValidator.setServicePrincipal("TODO: Insert Service Principal") + ticketValidator.setKeyTabLocation(new FileSystemResource("TODO: Insert Keytab Loacation")) + ticketValidator.setDebug(true) + ticketValidator.afterPropertiesSet() + ticketValidator + } + + private def loginConfig(): SunJaasKrb5LoginConfig = { + val loginConfig = new SunJaasKrb5LoginConfig() + loginConfig.setServicePrincipal("TODO: Insert Service Principal") + loginConfig.setKeyTabLocation(new FileSystemResource("TODO: Insert Keytab Loacation")) + loginConfig.setDebug(true) + loginConfig.setIsInitiator(true) + loginConfig.setUseTicketCache(false) + loginConfig.afterPropertiesSet() + loginConfig + } + + private def kerberosLdapContextSource(): KerberosLdapContextSource = { + val contextSource = new KerberosLdapContextSource("TODO: Add Ldap Server") + contextSource.setLoginConfig(loginConfig()) + contextSource.afterPropertiesSet() + contextSource + } + + private def ldapUserDetailsService(): LdapUserDetailsService = { + val userSearch = new kerberosLdapSearch("TODO: ldapSearchBase", "TODO: ldapSearchFilter", kerberosLdapContextSource()) + val service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator) + service.setUserDetailsMapper(new LdapUserDetailsMapper()) + service + } + + private def kerberosServiceAuthenticationProvider(): KerberosServiceAuthenticationProvider = { + val provider = new KerberosServiceAuthenticationProvider() + provider.setTicketValidator(sunJaasKerberosTicketValidator()) + provider.setUserDetailsService(ldapUserDetailsService()) + provider + } +} + +object RestApiKerberosAuthentication { + private val logger = LoggerFactory.getLogger(this.getClass) + + def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager, authenticationSuccessHandler: AuthenticationSuccessHandler): SpnegoAuthenticationProcessingFilter = { + val filter = new SpnegoAuthenticationProcessingFilter() + filter.setAuthenticationManager(authenticationManager) + filter.setSkipIfAlreadyAuthenticated(true) + filter.setSuccessHandler(authenticationSuccessHandler) + filter.setFailureHandler((request: HttpServletRequest, response: HttpServletResponse, exception: AuthenticationException) => { + logger.error(exception.getStackTrace.toString) + }) + filter + } +} + +private class kerberosLdapSearch(searchBase: String, searchFilter: String, contextSource: BaseLdapPathContextSource) + extends FilterBasedLdapUserSearch(searchBase, searchFilter, contextSource) { + override def searchForUser(username: String): DirContextOperations = { + val user = if(username.contains("@")) { + username.split("@").head + } + else { + username + } + super.searchForUser(user) + } +} + +private class ActiveDirectoryLdapAuthoritiesPopulator() extends LdapAuthoritiesPopulator { + override def getGrantedAuthorities(userData: DirContextOperations, username: String): util.Collection[_ <: GrantedAuthority] = { + val groups = userData.getStringAttributes("memberof") + + if(groups.isEmpty) + AuthorityUtils.NO_AUTHORITIES + else { + groups.map({group => + val ldapName = new LdapName(group) + val role = ldapName.getRdn(ldapName.size()-1).getValue.toString + new SimpleGrantedAuthority(role) + }).toList.asJava + } + } +} \ No newline at end of file diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 094484e2..7e3aae46 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -44,6 +44,9 @@ object Dependencies { lazy val springBootSecurity = "org.springframework.boot" % "spring-boot-starter-security" % Versions.springBoot lazy val springSecurityLDAP = "org.springframework.security" % "spring-security-ldap" % Versions.spring + lazy val springSecurityKerberosClient = "org.springframework.security.kerberos" % "spring-security-kerberos-client" % "1.0.1.RELEASE" + lazy val springSecurityKerberosWeb = "org.springframework.security.kerberos" % "spring-security-kerberos-web" % "1.0.1.RELEASE" + lazy val jjwtApi = "io.jsonwebtoken" % "jjwt-api" % Versions.jjwt lazy val jjwtImpl = "io.jsonwebtoken" % "jjwt-impl" % Versions.jjwt % Runtime @@ -84,6 +87,8 @@ object Dependencies { springBootSecurity, springSecurityLDAP, + springSecurityKerberosClient, + springSecurityKerberosWeb, jjwtApi, jjwtImpl, From 664a37c461e01b4f5561fd59e4d64dc47df941fe Mon Sep 17 00:00:00 2001 From: TheLydonKing Date: Thu, 25 Apr 2024 15:54:34 +0200 Subject: [PATCH 02/33] Added Kerberos to Config --- .../main/resources/example.application.yaml | 4 ++++ .../auth/ActiveDirectoryLDAPConfig.scala | 23 ++++++++++++++++++- ...KerberosSPNEGOAuthenticationProvider.scala | 6 ++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/api/src/main/resources/example.application.yaml b/api/src/main/resources/example.application.yaml index 8bf16be8..a8b0ea3b 100644 --- a/api/src/main/resources/example.application.yaml +++ b/api/src/main/resources/example.application.yaml @@ -73,6 +73,10 @@ loginsvc: #region: "region" #username-field-name: "username" #password-field-name: "password" + enable-kerberos: + krb-file-location: "/etc/krb5.conf" + keytab-file-location: "/etc/keytab" + debug: true attributes: # The FieldName is the key used to search ldap and the value is the value used to name the JWT claim. # ldapFieldName: claimFieldName diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala index 7df93c09..cad51945 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala @@ -34,6 +34,7 @@ case class ActiveDirectoryLDAPConfig(domain: String, searchFilter: String, order: Int, serviceAccount: ServiceAccountConfig, + enableKerberos: Option[KerberosConfig], attributes: Option[Map[String, String]]) extends ConfigValidatable with ConfigOrdering { @@ -63,12 +64,32 @@ case class ActiveDirectoryLDAPConfig(domain: String, .getOrElse(ConfigValidationError(ConfigValidationException("searchFilter is empty"))) ) - results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) + val requiredResults = results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) + val kerberosResults = enableKerberos match { + case Some(x) => x.validate() + case None => ConfigValidationSuccess + } + requiredResults.merge(kerberosResults) } else ConfigValidationSuccess } } +case class KerberosConfig(krbFileLocation: String, keytabFileLocation: String, debug: Option[Boolean]) extends ConfigValidatable { + + override def validate(): ConfigValidationResult = { + val results = Seq( + Option(krbFileLocation) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("krbFileLocation is empty"))), + Option(keytabFileLocation) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("keytabFileLocation is empty"))) + ) + results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) + } +} + case class ServiceAccountConfig(private val accountPattern: String, private val inConfigAccount: Option[LdapUserCredentialsConfig], private val awsSecretsManagerAccount: Option[AwsSecretsLdapUserConfig]) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index e6479731..703e6241 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -14,7 +14,7 @@ import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSour import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter import org.springframework.security.ldap.search.FilterBasedLdapUserSearch import org.springframework.security.ldap.userdetails.{LdapAuthoritiesPopulator, LdapUserDetailsMapper, LdapUserDetailsService} -import org.springframework.security.web.authentication.{AuthenticationFailureHandler, AuthenticationSuccessHandler} +import org.springframework.security.web.authentication.AuthenticationSuccessHandler import java.util import javax.naming.ldap.LdapName @@ -60,7 +60,7 @@ class KerberosSPNEGOAuthenticationProvider { service } - private def kerberosServiceAuthenticationProvider(): KerberosServiceAuthenticationProvider = { + def kerberosServiceAuthenticationProvider(): KerberosServiceAuthenticationProvider = { val provider = new KerberosServiceAuthenticationProvider() provider.setTicketValidator(sunJaasKerberosTicketValidator()) provider.setUserDetailsService(ldapUserDetailsService()) @@ -110,4 +110,4 @@ private class ActiveDirectoryLdapAuthoritiesPopulator() extends LdapAuthoritiesP }).toList.asJava } } -} \ No newline at end of file +} From 7c007784be09c119d447778426c4bd519a3bc28f Mon Sep 17 00:00:00 2001 From: TheLydonKing Date: Fri, 26 Apr 2024 15:42:15 +0200 Subject: [PATCH 03/33] Added config to required class --- ...KerberosSPNEGOAuthenticationProvider.scala | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 703e6241..db7b6733 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -15,31 +15,35 @@ import org.springframework.security.kerberos.web.authentication.SpnegoAuthentica import org.springframework.security.ldap.search.FilterBasedLdapUserSearch import org.springframework.security.ldap.userdetails.{LdapAuthoritiesPopulator, LdapUserDetailsMapper, LdapUserDetailsService} import org.springframework.security.web.authentication.AuthenticationSuccessHandler +import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig import java.util import javax.naming.ldap.LdapName import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import scala.collection.JavaConverters._ -class KerberosSPNEGOAuthenticationProvider { +class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { + private val serviceAccount = activeDirectoryLDAPConfig.serviceAccount + private val kerberos = activeDirectoryLDAPConfig.enableKerberos.get + private val kerberosDebug = kerberos.debug.getOrElse(false) private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) logger.debug(s"KerberosSPNEGOAuthenticationProvider init") private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = { val ticketValidator = new SunJaasKerberosTicketValidator() - ticketValidator.setServicePrincipal("TODO: Insert Service Principal") - ticketValidator.setKeyTabLocation(new FileSystemResource("TODO: Insert Keytab Loacation")) - ticketValidator.setDebug(true) + ticketValidator.setServicePrincipal(serviceAccount.username) + ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) + ticketValidator.setDebug(kerberosDebug) ticketValidator.afterPropertiesSet() ticketValidator } private def loginConfig(): SunJaasKrb5LoginConfig = { val loginConfig = new SunJaasKrb5LoginConfig() - loginConfig.setServicePrincipal("TODO: Insert Service Principal") - loginConfig.setKeyTabLocation(new FileSystemResource("TODO: Insert Keytab Loacation")) - loginConfig.setDebug(true) + loginConfig.setServicePrincipal(serviceAccount.username) + loginConfig.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) + loginConfig.setDebug(kerberosDebug) loginConfig.setIsInitiator(true) loginConfig.setUseTicketCache(false) loginConfig.afterPropertiesSet() @@ -47,14 +51,14 @@ class KerberosSPNEGOAuthenticationProvider { } private def kerberosLdapContextSource(): KerberosLdapContextSource = { - val contextSource = new KerberosLdapContextSource("TODO: Add Ldap Server") + val contextSource = new KerberosLdapContextSource(activeDirectoryLDAPConfig.url) contextSource.setLoginConfig(loginConfig()) contextSource.afterPropertiesSet() contextSource } private def ldapUserDetailsService(): LdapUserDetailsService = { - val userSearch = new kerberosLdapSearch("TODO: ldapSearchBase", "TODO: ldapSearchFilter", kerberosLdapContextSource()) + val userSearch = new kerberosLdapSearch(activeDirectoryLDAPConfig.domain, activeDirectoryLDAPConfig.searchFilter, kerberosLdapContextSource()) val service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator) service.setUserDetailsMapper(new LdapUserDetailsMapper()) service From 69cab58497fd4407c9f353ac3a70b39f7aabebb0 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Mon, 10 Jun 2024 15:18:57 +0200 Subject: [PATCH 04/33] Implement Kerberos Search --- .../absa/loginsvc/rest/SecurityConfig.scala | 25 +++++++++++++++---- ...KerberosSPNEGOAuthenticationProvider.scala | 7 ++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index b535c32d..0d89a590 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -16,18 +16,23 @@ package za.co.absa.loginsvc.rest +import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.{Bean, Configuration} -import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.{AuthenticationManager, ProviderManager} import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.www.BasicAuthenticationFilter +import za.co.absa.loginsvc.rest.config.provider.AuthConfigProvider +import za.co.absa.loginsvc.rest.provider.kerberos.{KerberosSPNEGOAuthenticationProvider, RestApiKerberosAuthentication} @Configuration @EnableWebSecurity -class SecurityConfig { +class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { + + //TODO: Neaten up checking for Config + private val KerberosConfig = authConfigsProvider.getLdapConfig.orNull @Bean def filterChain(http: HttpSecurity): SecurityFilterChain = { @@ -51,8 +56,18 @@ class SecurityConfig { .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .httpBasic() - //.and() - //.addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManager), classOf[BasicAuthenticationFilter]) + + //TODO: Neaten up checking for Config + if(KerberosConfig != null) + { + if(KerberosConfig.enableKerberos.isDefined) + { + val kerberos = new KerberosSPNEGOAuthenticationProvider(KerberosConfig) + http.addFilterBefore( + RestApiKerberosAuthentication.spnegoAuthenticationProcessingFilter( + new ProviderManager(kerberos.kerberosServiceAuthenticationProvider())), classOf[BasicAuthenticationFilter]) + } + } http.build() } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index db7b6733..53fd06c4 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -24,6 +24,7 @@ import scala.collection.JavaConverters._ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { + //TODO: Split into Multiple files for neater implementation private val serviceAccount = activeDirectoryLDAPConfig.serviceAccount private val kerberos = activeDirectoryLDAPConfig.enableKerberos.get private val kerberosDebug = kerberos.debug.getOrElse(false) @@ -75,14 +76,10 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire object RestApiKerberosAuthentication { private val logger = LoggerFactory.getLogger(this.getClass) - def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager, authenticationSuccessHandler: AuthenticationSuccessHandler): SpnegoAuthenticationProcessingFilter = { + def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager): SpnegoAuthenticationProcessingFilter = { val filter = new SpnegoAuthenticationProcessingFilter() filter.setAuthenticationManager(authenticationManager) filter.setSkipIfAlreadyAuthenticated(true) - filter.setSuccessHandler(authenticationSuccessHandler) - filter.setFailureHandler((request: HttpServletRequest, response: HttpServletResponse, exception: AuthenticationException) => { - logger.error(exception.getStackTrace.toString) - }) filter } } From 0f11ae1e267b2b17d1fffdbbd6d20bed3887ddc2 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Tue, 11 Jun 2024 17:36:39 +0200 Subject: [PATCH 05/33] Added Test LdapProvider just to test kerberosLdapSearch --- .../absa/loginsvc/rest/SecurityConfig.scala | 6 +- .../kerberos/KerberosLdapUserSearch.scala | 18 ++++ ...ala => KerberosSPNEGOAuthentication.scala} | 0 ...estApiKerberosAuthenticationProvider.scala | 100 ++++++++++++++++++ .../RestApiKerberosLdapContextSource.scala | 23 ++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala rename api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/{KerberosSPNEGOAuthenticationProvider.scala => KerberosSPNEGOAuthentication.scala} (100%) create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index 0d89a590..5ddfa3a6 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -25,7 +25,7 @@ import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.www.BasicAuthenticationFilter import za.co.absa.loginsvc.rest.config.provider.AuthConfigProvider -import za.co.absa.loginsvc.rest.provider.kerberos.{KerberosSPNEGOAuthenticationProvider, RestApiKerberosAuthentication} +import za.co.absa.loginsvc.rest.provider.kerberos.{KerberosSPNEGOAuthenticationProvider, RestApiKerberosAuthentication, RestApiKerberosAuthenticationProvider} @Configuration @EnableWebSecurity @@ -63,9 +63,11 @@ class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { if(KerberosConfig.enableKerberos.isDefined) { val kerberos = new KerberosSPNEGOAuthenticationProvider(KerberosConfig) + val provider = new RestApiKerberosAuthenticationProvider(KerberosConfig.url, KerberosConfig.searchFilter, KerberosConfig.domain); http.addFilterBefore( RestApiKerberosAuthentication.spnegoAuthenticationProcessingFilter( - new ProviderManager(kerberos.kerberosServiceAuthenticationProvider())), classOf[BasicAuthenticationFilter]) + new ProviderManager(provider, kerberos.kerberosServiceAuthenticationProvider())), + classOf[BasicAuthenticationFilter]) } } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala new file mode 100644 index 00000000..b7f05ed8 --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala @@ -0,0 +1,18 @@ +package za.co.absa.loginsvc.rest.provider.kerberos + +import org.springframework.ldap.core.DirContextOperations +import org.springframework.ldap.core.support.BaseLdapPathContextSource +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch + +class KerberosLdapUserSearch (searchBase: String, searchFilter: String, contextSource: BaseLdapPathContextSource) + extends FilterBasedLdapUserSearch(searchBase, searchFilter, contextSource) { + + override def searchForUser(username: String): DirContextOperations = { + val user = if (username.contains("@")) { + username.split("@").head + } else { + username + } + super.searchForUser(user) + } +} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala similarity index 100% rename from api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala rename to api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala new file mode 100644 index 00000000..3f90ba2d --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala @@ -0,0 +1,100 @@ +package za.co.absa.loginsvc.rest.provider.kerberos + +import org.slf4j.LoggerFactory +import org.springframework.security.authentication.{AuthenticationProvider, BadCredentialsException, UsernamePasswordAuthenticationToken} +import org.springframework.security.core.Authentication +import org.springframework.security.ldap.userdetails.LdapUserDetailsService +import sun.security.krb5.KrbException + +import java.net.{SocketTimeoutException, UnknownHostException} +import java.security.PrivilegedActionException +import javax.security.auth.Subject +import javax.security.auth.callback.{Callback, CallbackHandler, NameCallback, PasswordCallback} +import javax.security.auth.login.{AppConfigurationEntry, Configuration, LoginContext, LoginException} +import scala.util.control.NonFatal + +class RestApiKerberosAuthenticationProvider (adServer: String, searchFilter: String, baseDN: String) + extends AuthenticationProvider { + + case class RestApiKerberosLoginResult(loginContext: LoginContext, verifiedName: String) + + private val logger = LoggerFactory.getLogger(this.getClass) + + override def authenticate(authentication: Authentication): Authentication = { + val output = try { + val auth = authentication.asInstanceOf[UsernamePasswordAuthenticationToken] + val loginResult = login(auth.getName, auth.getCredentials.toString) + val userDetailsService = getUserDetailService(loginResult.loginContext.getSubject) + val userDetails = userDetailsService.loadUserByUsername(loginResult.verifiedName) + loginResult.loginContext.logout() + new UsernamePasswordAuthenticationToken(userDetails, auth.getCredentials, userDetails.getAuthorities) + } catch { + case ex: LoginException => + ex.getCause match { + // This is thrown when there is an issue contacting a KDC specified in krb5.conf for the realm + case nestedException: SocketTimeoutException => + throw new Exception(s"Timeout host: ${nestedException.getMessage}", ex) + case nestedException: UnknownHostException => + throw new Exception(s"Unknown authentication host: ${nestedException.getMessage}", ex) + case nestedException: KrbException => + throw new BadCredentialsException(s"Invalid credentials: ${nestedException.getMessage}", ex) + case NonFatal(_) => + throw ex + } + // This is thrown when there is an issue contacting an LDAP server specified in REST API configuration + case ex: PrivilegedActionException => + throw new Exception(ex.toString, ex) + case NonFatal(ex) => + throw ex + } + output.setDetails(authentication.getDetails) + output + } + + override def supports(authentication: Class[_]): Boolean = { + classOf[UsernamePasswordAuthenticationToken].isAssignableFrom(authentication) + } + + private def login(username: String, password: String): RestApiKerberosLoginResult = { + val loginContext = new LoginContext("", null, getSpringCBHandler(username, password), getLoginConfig) // scalastyle:ignore null + loginContext.login() + val loggedInUser = loginContext.getSubject.getPrincipals.iterator.next.toString + logger.debug(s"Logged In User: $loggedInUser") + RestApiKerberosLoginResult(loginContext, loggedInUser) + } + + private def getSpringCBHandler(username: String, password: String) = { + new CallbackHandler() { + def handle(callbacks: Array[Callback]): Unit = { + callbacks.foreach({ + case ncb: NameCallback => ncb.setName(username) + case pwdcb: PasswordCallback => pwdcb.setPassword(password.toCharArray) + }) + } + } + } + + private def getUserDetailService(subject: Subject) = { + val contextSource = new RestApiKerberosLdapContextSource(adServer, subject) + contextSource.afterPropertiesSet() + val userSearch = new KerberosLdapUserSearch(baseDN, searchFilter, contextSource) + new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator()) + } + + private def getLoginConfig: Configuration = { + import scala.collection.JavaConverters._ + + new Configuration { + override def getAppConfigurationEntry(name: String): Array[AppConfigurationEntry] = { + val opts = Map( + "storeKey" -> "true", + "refreshKrb5Config" -> "true", + "isInitiator" -> "true", + "debug" -> "true") + Array(new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts.asJava)) + } + } + } +} + diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala new file mode 100644 index 00000000..58dbbebe --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala @@ -0,0 +1,23 @@ +package za.co.absa.loginsvc.rest.provider.kerberos + +import org.springframework.security.ldap.DefaultSpringSecurityContextSource + +import java.security.PrivilegedAction +import javax.naming.Context +import javax.naming.directory.DirContext +import javax.security.auth.Subject + +class RestApiKerberosLdapContextSource (url: String, subject: Subject) extends DefaultSpringSecurityContextSource(url) { + override def getDirContextInstance(environment: java.util.Hashtable[String, Object]): DirContext = { + + environment.put(Context.SECURITY_AUTHENTICATION, "GSSAPI") + val sup = super.getDirContextInstance _ + + logger.debug(s"Trying to authenticate to LDAP as ${subject.getPrincipals}") + Subject.doAs(subject, new PrivilegedAction[DirContext]() { + override def run(): DirContext = { + sup(environment) + } + }) + } +} From 0b2621ec7e37e52bfc990edd2badfd8d141dc819 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 12 Jun 2024 12:53:39 +0200 Subject: [PATCH 06/33] Trying to use system property sets --- .../kerberos/KerberosSPNEGOAuthentication.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala index 53fd06c4..89bdc95a 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala @@ -31,9 +31,17 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) logger.debug(s"KerberosSPNEGOAuthenticationProvider init") + System.setProperty("javax.net.debug", kerberosDebug.toString) + System.setProperty("sun.security.krb5.debug", kerberosDebug.toString) + + if (kerberos.krbFileLocation.nonEmpty) { + logger.info(s"Using KRB5 CONF from ${kerberos.krbFileLocation}") + System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) + } + private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = { val ticketValidator = new SunJaasKerberosTicketValidator() - ticketValidator.setServicePrincipal(serviceAccount.username) + ticketValidator.setServicePrincipal("SVC-CPS-LOGIN-LDAP@CORP.DSARENA.COM") ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) ticketValidator.setDebug(kerberosDebug) ticketValidator.afterPropertiesSet() @@ -42,7 +50,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire private def loginConfig(): SunJaasKrb5LoginConfig = { val loginConfig = new SunJaasKrb5LoginConfig() - loginConfig.setServicePrincipal(serviceAccount.username) + loginConfig.setServicePrincipal("SVC-CPS-LOGIN-LDAP@CORP.DSARENA.COM") loginConfig.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) loginConfig.setDebug(kerberosDebug) loginConfig.setIsInitiator(true) From 892d391c5ce07c1a951e957fdc0dfb3a6305c6f5 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 3 Jul 2024 16:17:01 +0200 Subject: [PATCH 07/33] Simplify code for better debugging --- .../absa/loginsvc/rest/SecurityConfig.scala | 11 +- .../kerberos/KerberosLdapUserSearch.scala | 18 --- .../KerberosSPNEGOAuthentication.scala | 122 ------------------ ...KerberosSPNEGOAuthenticationProvider.scala | 71 ++++++++++ ...estApiKerberosAuthenticationProvider.scala | 100 -------------- .../RestApiKerberosLdapContextSource.scala | 23 ---- 6 files changed, 78 insertions(+), 267 deletions(-) delete mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala delete mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala delete mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala delete mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index 5ddfa3a6..4bb71e21 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -25,7 +25,7 @@ import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.www.BasicAuthenticationFilter import za.co.absa.loginsvc.rest.config.provider.AuthConfigProvider -import za.co.absa.loginsvc.rest.provider.kerberos.{KerberosSPNEGOAuthenticationProvider, RestApiKerberosAuthentication, RestApiKerberosAuthenticationProvider} +import za.co.absa.loginsvc.rest.provider.kerberos.KerberosSPNEGOAuthenticationProvider @Configuration @EnableWebSecurity @@ -63,10 +63,13 @@ class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { if(KerberosConfig.enableKerberos.isDefined) { val kerberos = new KerberosSPNEGOAuthenticationProvider(KerberosConfig) - val provider = new RestApiKerberosAuthenticationProvider(KerberosConfig.url, KerberosConfig.searchFilter, KerberosConfig.domain); + + val provider = kerberos.kerberosAuthenticationProvider() + val serviceProvider = kerberos.kerberosServiceAuthenticationProvider() + http.addFilterBefore( - RestApiKerberosAuthentication.spnegoAuthenticationProcessingFilter( - new ProviderManager(provider, kerberos.kerberosServiceAuthenticationProvider())), + kerberos.spnegoAuthenticationProcessingFilter( + new ProviderManager(provider, serviceProvider)), classOf[BasicAuthenticationFilter]) } } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala deleted file mode 100644 index b7f05ed8..00000000 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosLdapUserSearch.scala +++ /dev/null @@ -1,18 +0,0 @@ -package za.co.absa.loginsvc.rest.provider.kerberos - -import org.springframework.ldap.core.DirContextOperations -import org.springframework.ldap.core.support.BaseLdapPathContextSource -import org.springframework.security.ldap.search.FilterBasedLdapUserSearch - -class KerberosLdapUserSearch (searchBase: String, searchFilter: String, contextSource: BaseLdapPathContextSource) - extends FilterBasedLdapUserSearch(searchBase, searchFilter, contextSource) { - - override def searchForUser(username: String): DirContextOperations = { - val user = if (username.contains("@")) { - username.split("@").head - } else { - username - } - super.searchForUser(user) - } -} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala deleted file mode 100644 index 89bdc95a..00000000 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthentication.scala +++ /dev/null @@ -1,122 +0,0 @@ -package za.co.absa.loginsvc.rest.provider.kerberos - -import org.slf4j.LoggerFactory -import org.springframework.core.io.FileSystemResource -import org.springframework.ldap.core.DirContextOperations -import org.springframework.ldap.core.support.BaseLdapPathContextSource -import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.core.{AuthenticationException, GrantedAuthority} -import org.springframework.security.core.authority.{AuthorityUtils, SimpleGrantedAuthority} -import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider -import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator -import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig -import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource -import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter -import org.springframework.security.ldap.search.FilterBasedLdapUserSearch -import org.springframework.security.ldap.userdetails.{LdapAuthoritiesPopulator, LdapUserDetailsMapper, LdapUserDetailsService} -import org.springframework.security.web.authentication.AuthenticationSuccessHandler -import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig - -import java.util -import javax.naming.ldap.LdapName -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} -import scala.collection.JavaConverters._ - -class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { - - //TODO: Split into Multiple files for neater implementation - private val serviceAccount = activeDirectoryLDAPConfig.serviceAccount - private val kerberos = activeDirectoryLDAPConfig.enableKerberos.get - private val kerberosDebug = kerberos.debug.getOrElse(false) - private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) - logger.debug(s"KerberosSPNEGOAuthenticationProvider init") - - System.setProperty("javax.net.debug", kerberosDebug.toString) - System.setProperty("sun.security.krb5.debug", kerberosDebug.toString) - - if (kerberos.krbFileLocation.nonEmpty) { - logger.info(s"Using KRB5 CONF from ${kerberos.krbFileLocation}") - System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) - } - - private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = { - val ticketValidator = new SunJaasKerberosTicketValidator() - ticketValidator.setServicePrincipal("SVC-CPS-LOGIN-LDAP@CORP.DSARENA.COM") - ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) - ticketValidator.setDebug(kerberosDebug) - ticketValidator.afterPropertiesSet() - ticketValidator - } - - private def loginConfig(): SunJaasKrb5LoginConfig = { - val loginConfig = new SunJaasKrb5LoginConfig() - loginConfig.setServicePrincipal("SVC-CPS-LOGIN-LDAP@CORP.DSARENA.COM") - loginConfig.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) - loginConfig.setDebug(kerberosDebug) - loginConfig.setIsInitiator(true) - loginConfig.setUseTicketCache(false) - loginConfig.afterPropertiesSet() - loginConfig - } - - private def kerberosLdapContextSource(): KerberosLdapContextSource = { - val contextSource = new KerberosLdapContextSource(activeDirectoryLDAPConfig.url) - contextSource.setLoginConfig(loginConfig()) - contextSource.afterPropertiesSet() - contextSource - } - - private def ldapUserDetailsService(): LdapUserDetailsService = { - val userSearch = new kerberosLdapSearch(activeDirectoryLDAPConfig.domain, activeDirectoryLDAPConfig.searchFilter, kerberosLdapContextSource()) - val service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator) - service.setUserDetailsMapper(new LdapUserDetailsMapper()) - service - } - - def kerberosServiceAuthenticationProvider(): KerberosServiceAuthenticationProvider = { - val provider = new KerberosServiceAuthenticationProvider() - provider.setTicketValidator(sunJaasKerberosTicketValidator()) - provider.setUserDetailsService(ldapUserDetailsService()) - provider - } -} - -object RestApiKerberosAuthentication { - private val logger = LoggerFactory.getLogger(this.getClass) - - def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager): SpnegoAuthenticationProcessingFilter = { - val filter = new SpnegoAuthenticationProcessingFilter() - filter.setAuthenticationManager(authenticationManager) - filter.setSkipIfAlreadyAuthenticated(true) - filter - } -} - -private class kerberosLdapSearch(searchBase: String, searchFilter: String, contextSource: BaseLdapPathContextSource) - extends FilterBasedLdapUserSearch(searchBase, searchFilter, contextSource) { - override def searchForUser(username: String): DirContextOperations = { - val user = if(username.contains("@")) { - username.split("@").head - } - else { - username - } - super.searchForUser(user) - } -} - -private class ActiveDirectoryLdapAuthoritiesPopulator() extends LdapAuthoritiesPopulator { - override def getGrantedAuthorities(userData: DirContextOperations, username: String): util.Collection[_ <: GrantedAuthority] = { - val groups = userData.getStringAttributes("memberof") - - if(groups.isEmpty) - AuthorityUtils.NO_AUTHORITIES - else { - groups.map({group => - val ldapName = new LdapName(group) - val role = ldapName.getRdn(ldapName.size()-1).getValue.toString - new SimpleGrantedAuthority(role) - }).toList.asJava - } - } -} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala new file mode 100644 index 00000000..cdfd8f16 --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -0,0 +1,71 @@ +package za.co.absa.loginsvc.rest.provider.kerberos + +import org.slf4j.LoggerFactory +import org.springframework.core.io.FileSystemResource +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.core.authority.{AuthorityUtils, SimpleGrantedAuthority} +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.kerberos.authentication.{KerberosAuthenticationProvider, KerberosServiceAuthenticationProvider} +import org.springframework.security.kerberos.authentication.sun.{SunJaasKerberosClient, SunJaasKerberosTicketValidator} +import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter +import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig + +class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { + + //TODO: Split into Multiple files for neater implementation + private val serviceAccount = activeDirectoryLDAPConfig.serviceAccount + private val kerberos = activeDirectoryLDAPConfig.enableKerberos.get + private val kerberosDebug = kerberos.debug.getOrElse(false) + private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) + logger.debug(s"KerberosSPNEGOAuthenticationProvider init") + + System.setProperty("javax.net.debug", kerberosDebug.toString) + System.setProperty("sun.security.krb5.debug", kerberosDebug.toString) + + if (kerberos.krbFileLocation.nonEmpty) { + logger.info(s"Using KRB5 CONF from ${kerberos.krbFileLocation}") + System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) + } + + def kerberosAuthenticationProvider(): KerberosAuthenticationProvider = + { + val provider: KerberosAuthenticationProvider = new KerberosAuthenticationProvider() + val client: SunJaasKerberosClient = new SunJaasKerberosClient() + + client.setDebug(true) + provider.setKerberosClient(client) + provider.setUserDetailsService(dummyUserDetailsService) + provider + } + + def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager): SpnegoAuthenticationProcessingFilter = + { + val filter: SpnegoAuthenticationProcessingFilter = new SpnegoAuthenticationProcessingFilter() + filter.setAuthenticationManager(authenticationManager) + filter + } + + def kerberosServiceAuthenticationProvider(): KerberosServiceAuthenticationProvider = + { + val provider: KerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider() + provider.setTicketValidator(sunJaasKerberosTicketValidator()) + provider.setUserDetailsService(dummyUserDetailsService) + provider + } + + private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = + { + val ticketValidator: SunJaasKerberosTicketValidator = new SunJaasKerberosTicketValidator() + ticketValidator.setServicePrincipal(serviceAccount.username) + ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) + ticketValidator.setDebug(true) + ticketValidator + } + + private def dummyUserDetailsService = new DummyUserDetailsService +} + +class DummyUserDetailsService extends UserDetailsService { + override def loadUserByUsername(username: String) = new User(username, "{noop}notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")) +} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala deleted file mode 100644 index 3f90ba2d..00000000 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosAuthenticationProvider.scala +++ /dev/null @@ -1,100 +0,0 @@ -package za.co.absa.loginsvc.rest.provider.kerberos - -import org.slf4j.LoggerFactory -import org.springframework.security.authentication.{AuthenticationProvider, BadCredentialsException, UsernamePasswordAuthenticationToken} -import org.springframework.security.core.Authentication -import org.springframework.security.ldap.userdetails.LdapUserDetailsService -import sun.security.krb5.KrbException - -import java.net.{SocketTimeoutException, UnknownHostException} -import java.security.PrivilegedActionException -import javax.security.auth.Subject -import javax.security.auth.callback.{Callback, CallbackHandler, NameCallback, PasswordCallback} -import javax.security.auth.login.{AppConfigurationEntry, Configuration, LoginContext, LoginException} -import scala.util.control.NonFatal - -class RestApiKerberosAuthenticationProvider (adServer: String, searchFilter: String, baseDN: String) - extends AuthenticationProvider { - - case class RestApiKerberosLoginResult(loginContext: LoginContext, verifiedName: String) - - private val logger = LoggerFactory.getLogger(this.getClass) - - override def authenticate(authentication: Authentication): Authentication = { - val output = try { - val auth = authentication.asInstanceOf[UsernamePasswordAuthenticationToken] - val loginResult = login(auth.getName, auth.getCredentials.toString) - val userDetailsService = getUserDetailService(loginResult.loginContext.getSubject) - val userDetails = userDetailsService.loadUserByUsername(loginResult.verifiedName) - loginResult.loginContext.logout() - new UsernamePasswordAuthenticationToken(userDetails, auth.getCredentials, userDetails.getAuthorities) - } catch { - case ex: LoginException => - ex.getCause match { - // This is thrown when there is an issue contacting a KDC specified in krb5.conf for the realm - case nestedException: SocketTimeoutException => - throw new Exception(s"Timeout host: ${nestedException.getMessage}", ex) - case nestedException: UnknownHostException => - throw new Exception(s"Unknown authentication host: ${nestedException.getMessage}", ex) - case nestedException: KrbException => - throw new BadCredentialsException(s"Invalid credentials: ${nestedException.getMessage}", ex) - case NonFatal(_) => - throw ex - } - // This is thrown when there is an issue contacting an LDAP server specified in REST API configuration - case ex: PrivilegedActionException => - throw new Exception(ex.toString, ex) - case NonFatal(ex) => - throw ex - } - output.setDetails(authentication.getDetails) - output - } - - override def supports(authentication: Class[_]): Boolean = { - classOf[UsernamePasswordAuthenticationToken].isAssignableFrom(authentication) - } - - private def login(username: String, password: String): RestApiKerberosLoginResult = { - val loginContext = new LoginContext("", null, getSpringCBHandler(username, password), getLoginConfig) // scalastyle:ignore null - loginContext.login() - val loggedInUser = loginContext.getSubject.getPrincipals.iterator.next.toString - logger.debug(s"Logged In User: $loggedInUser") - RestApiKerberosLoginResult(loginContext, loggedInUser) - } - - private def getSpringCBHandler(username: String, password: String) = { - new CallbackHandler() { - def handle(callbacks: Array[Callback]): Unit = { - callbacks.foreach({ - case ncb: NameCallback => ncb.setName(username) - case pwdcb: PasswordCallback => pwdcb.setPassword(password.toCharArray) - }) - } - } - } - - private def getUserDetailService(subject: Subject) = { - val contextSource = new RestApiKerberosLdapContextSource(adServer, subject) - contextSource.afterPropertiesSet() - val userSearch = new KerberosLdapUserSearch(baseDN, searchFilter, contextSource) - new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator()) - } - - private def getLoginConfig: Configuration = { - import scala.collection.JavaConverters._ - - new Configuration { - override def getAppConfigurationEntry(name: String): Array[AppConfigurationEntry] = { - val opts = Map( - "storeKey" -> "true", - "refreshKrb5Config" -> "true", - "isInitiator" -> "true", - "debug" -> "true") - Array(new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts.asJava)) - } - } - } -} - diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala deleted file mode 100644 index 58dbbebe..00000000 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/RestApiKerberosLdapContextSource.scala +++ /dev/null @@ -1,23 +0,0 @@ -package za.co.absa.loginsvc.rest.provider.kerberos - -import org.springframework.security.ldap.DefaultSpringSecurityContextSource - -import java.security.PrivilegedAction -import javax.naming.Context -import javax.naming.directory.DirContext -import javax.security.auth.Subject - -class RestApiKerberosLdapContextSource (url: String, subject: Subject) extends DefaultSpringSecurityContextSource(url) { - override def getDirContextInstance(environment: java.util.Hashtable[String, Object]): DirContext = { - - environment.put(Context.SECURITY_AUTHENTICATION, "GSSAPI") - val sup = super.getDirContextInstance _ - - logger.debug(s"Trying to authenticate to LDAP as ${subject.getPrincipals}") - Subject.doAs(subject, new PrivilegedAction[DirContext]() { - override def run(): DirContext = { - sup(environment) - } - }) - } -} From 9fb0e2be82b5c249ffc2c2d402de0a2323231661 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Mon, 22 Jul 2024 13:55:38 +0200 Subject: [PATCH 08/33] Split up config among new files for better readability --- .../auth/ActiveDirectoryLDAPConfig.scala | 92 ------------------- .../rest/config/auth/KerberosConfig.scala | 19 ++++ .../config/auth/ServiceAccountConfig.scala | 84 +++++++++++++++++ ...KerberosSPNEGOAuthenticationProvider.scala | 12 ++- 4 files changed, 111 insertions(+), 96 deletions(-) create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala index cad51945..86870860 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfig.scala @@ -75,98 +75,6 @@ case class ActiveDirectoryLDAPConfig(domain: String, } } -case class KerberosConfig(krbFileLocation: String, keytabFileLocation: String, debug: Option[Boolean]) extends ConfigValidatable { - - override def validate(): ConfigValidationResult = { - val results = Seq( - Option(krbFileLocation) - .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("krbFileLocation is empty"))), - Option(keytabFileLocation) - .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("keytabFileLocation is empty"))) - ) - results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) - } -} - -case class ServiceAccountConfig(private val accountPattern: String, - private val inConfigAccount: Option[LdapUserCredentialsConfig], - private val awsSecretsManagerAccount: Option[AwsSecretsLdapUserConfig]) -{ - private val ldapUserDetails: LdapUser = (inConfigAccount, awsSecretsManagerAccount) match { - case (Some(_), Some(_)) => - throw ConfigValidationException("Both inConfigAccount and awsSecretsLdapUserConfig exist. Please choose only one.") - - case (None, None) => - throw ConfigValidationException("Neither integratedLdapUserConfig nor awsSecretsLdapUserConfig exists. Exactly one of them should be present.") - - case (Some(inConfig), None) => - inConfig.throwOnErrors() - inConfig - - case (None, Some(awsConfig)) => - awsConfig.throwOnErrors() - awsConfig - - case _ => - throw ConfigValidationException("Error with current config concerning inConfigAccount or awsSecretsLdapUserConfig") - } - - val username: String = String.format(accountPattern, ldapUserDetails.username) - val password: String = ldapUserDetails.password -} - -case class LdapUserCredentialsConfig (username: String, password: String) extends LdapUser -{ - def throwOnErrors(): Unit = this.validate().throwOnErrors() -} - -case class AwsSecretsLdapUserConfig(private val secretName: String, - private val region: String, - private val usernameFieldName: String, - private val passwordFieldName: String) extends LdapUser { - - private val logger = LoggerFactory.getLogger(classOf[LdapUser]) - - val (username, password) = getUsernameAndPasswordFromSecret - def throwOnErrors(): Unit = this.validate().throwOnErrors() - override def validate(): ConfigValidationResult = { - val results = Seq( - Option(secretName) - .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("secretName is empty"))), - - Option(region) - .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("region is empty"))), - - Option(usernameFieldName) - .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("usernameFieldName is empty"))), - - Option(passwordFieldName) - .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("passwordFieldName is empty"))) - ) - - val awsSecretsResultsMerge = results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) - awsSecretsResultsMerge.merge(super.validate()) - } - - private def getUsernameAndPasswordFromSecret: (String, String) = { - try { - val secrets = AwsSecretsUtils.fetchSecret(secretName, region, Array(usernameFieldName, passwordFieldName)) - (secrets(usernameFieldName), secrets(passwordFieldName)) - } - catch { - case e: Throwable => - logger.error(s"Error occurred retrieving account data from AWS Secrets Manager", e) - throw e - } - } -} - trait LdapUser extends ConfigValidatable { def username: String def password: String diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala new file mode 100644 index 00000000..93778eff --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala @@ -0,0 +1,19 @@ +package za.co.absa.loginsvc.rest.config.auth + +import za.co.absa.loginsvc.rest.config.validation.{ConfigValidatable, ConfigValidationException, ConfigValidationResult} +import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess} + +case class KerberosConfig(krbFileLocation: String, keytabFileLocation: String, debug: Option[Boolean]) extends ConfigValidatable { + + override def validate(): ConfigValidationResult = { + val results = Seq( + Option(krbFileLocation) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("krbFileLocation is empty"))), + Option(keytabFileLocation) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("keytabFileLocation is empty"))) + ) + results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) + } +} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala new file mode 100644 index 00000000..89de3859 --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala @@ -0,0 +1,84 @@ +package za.co.absa.loginsvc.rest.config.auth + +import org.slf4j.LoggerFactory +import za.co.absa.loginsvc.rest.config.validation.{ConfigValidationException, ConfigValidationResult} +import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess} +import za.co.absa.loginsvc.utils.AwsSecretsUtils + +case class ServiceAccountConfig(private val accountPattern: String, + private val inConfigAccount: Option[LdapUserCredentialsConfig], + private val awsSecretsManagerAccount: Option[AwsSecretsLdapUserConfig]) +{ + private val ldapUserDetails: LdapUser = (inConfigAccount, awsSecretsManagerAccount) match { + case (Some(_), Some(_)) => + throw ConfigValidationException("Both inConfigAccount and awsSecretsLdapUserConfig exist. Please choose only one.") + + case (None, None) => + throw ConfigValidationException("Neither integratedLdapUserConfig nor awsSecretsLdapUserConfig exists. Exactly one of them should be present.") + + case (Some(inConfig), None) => + inConfig.throwOnErrors() + inConfig + + case (None, Some(awsConfig)) => + awsConfig.throwOnErrors() + awsConfig + + case _ => + throw ConfigValidationException("Error with current config concerning inConfigAccount or awsSecretsLdapUserConfig") + } + + val username: String = String.format(accountPattern, ldapUserDetails.username) + val password: String = ldapUserDetails.password +} + +case class LdapUserCredentialsConfig (username: String, password: String) extends LdapUser +{ + def throwOnErrors(): Unit = this.validate().throwOnErrors() +} + +case class AwsSecretsLdapUserConfig(private val secretName: String, + private val region: String, + private val usernameFieldName: String, + private val passwordFieldName: String) extends LdapUser +{ + + private val logger = LoggerFactory.getLogger(classOf[LdapUser]) + + val (username, password) = getUsernameAndPasswordFromSecret + def throwOnErrors(): Unit = this.validate().throwOnErrors() + override def validate(): ConfigValidationResult = { + val results = Seq( + Option(secretName) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("secretName is empty"))), + + Option(region) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("region is empty"))), + + Option(usernameFieldName) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("usernameFieldName is empty"))), + + Option(passwordFieldName) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("passwordFieldName is empty"))) + ) + + val awsSecretsResultsMerge = results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) + awsSecretsResultsMerge.merge(super.validate()) + } + + private def getUsernameAndPasswordFromSecret: (String, String) = { + try { + val secrets = AwsSecretsUtils.fetchSecret(secretName, region, Array(usernameFieldName, passwordFieldName)) + (secrets(usernameFieldName), secrets(passwordFieldName)) + } + catch { + case e: Throwable => + logger.error(s"Error occurred retrieving account data from AWS Secrets Manager", e) + throw e + } + } +} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index cdfd8f16..6dbc410b 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -4,8 +4,7 @@ import org.slf4j.LoggerFactory import org.springframework.core.io.FileSystemResource import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.core.authority.{AuthorityUtils, SimpleGrantedAuthority} -import org.springframework.security.core.userdetails.User -import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.{User, UserDetails, UserDetailsService} import org.springframework.security.kerberos.authentication.{KerberosAuthenticationProvider, KerberosServiceAuthenticationProvider} import org.springframework.security.kerberos.authentication.sun.{SunJaasKerberosClient, SunJaasKerberosTicketValidator} import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter @@ -57,7 +56,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = { val ticketValidator: SunJaasKerberosTicketValidator = new SunJaasKerberosTicketValidator() - ticketValidator.setServicePrincipal(serviceAccount.username) + ticketValidator.setServicePrincipal("HTTP/localhost") ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) ticketValidator.setDebug(true) ticketValidator @@ -67,5 +66,10 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire } class DummyUserDetailsService extends UserDetailsService { - override def loadUserByUsername(username: String) = new User(username, "{noop}notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")) + private val logger = LoggerFactory.getLogger(classOf[DummyUserDetailsService]) + override def loadUserByUsername(username: String): UserDetails = + { + logger.info(s"returning dummy of $username") + new User(username, "{noop}notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")) + } } From 1693d3bb2144e2242cda66c3a519f15eb433388c Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Mon, 22 Jul 2024 15:39:13 +0200 Subject: [PATCH 09/33] Added Missing License --- .../rest/config/auth/KerberosConfig.scala | 16 ++++++++++++++++ .../rest/config/auth/ServiceAccountConfig.scala | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala index 93778eff..a9b3e552 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala @@ -1,3 +1,19 @@ +/* + * 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.loginsvc.rest.config.auth import za.co.absa.loginsvc.rest.config.validation.{ConfigValidatable, ConfigValidationException, ConfigValidationResult} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala index 89de3859..8f3a0233 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfig.scala @@ -1,3 +1,19 @@ +/* + * 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.loginsvc.rest.config.auth import org.slf4j.LoggerFactory From 3ef9aadd71fb01694f314f689ba48d6e33e2663f Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Mon, 22 Jul 2024 15:46:16 +0200 Subject: [PATCH 10/33] Fixed Tests --- .../KerberosSPNEGOAuthenticationProvider.scala | 16 ++++++++++++++++ .../auth/ActiveDirectoryLDAPConfigTest.scala | 4 ++-- .../search/DefaultUserRepositoriesTest.scala | 4 ++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 6dbc410b..5940f4fb 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.loginsvc.rest.provider.kerberos import org.slf4j.LoggerFactory diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala index d342b391..8ee2b999 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala @@ -28,7 +28,7 @@ class ActiveDirectoryLDAPConfigTest extends AnyFlatSpec with Matchers { "CN=%s,OU=Users,OU=CORP Accounts,DC=corp,DC=dsarena,DC=com", Option(integratedCfg), None) - private val ldapCfg = ActiveDirectoryLDAPConfig("some.domain.com", "ldaps://some.domain.com:636/","SomeAccount", 1, serviceAccountCfg, None) + private val ldapCfg = ActiveDirectoryLDAPConfig("some.domain.com", "ldaps://some.domain.com:636/","SomeAccount", 1, serviceAccountCfg, None, None) "ActiveDirectoryLDAPConfig" should "validate expected filled content" in { ldapCfg.validate() shouldBe ConfigValidationSuccess @@ -46,6 +46,6 @@ class ActiveDirectoryLDAPConfigTest extends AnyFlatSpec with Matchers { } it should "pass validation if disabled despite missing values" in { - ActiveDirectoryLDAPConfig(null, null, null, 0, null, None).validate() shouldBe ConfigValidationSuccess + ActiveDirectoryLDAPConfig(null, null, null, 0, null, None, None).validate() shouldBe ConfigValidationSuccess } } diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/service/search/DefaultUserRepositoriesTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/service/search/DefaultUserRepositoriesTest.scala index 5d507c87..baf8d2af 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/service/search/DefaultUserRepositoriesTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/service/search/DefaultUserRepositoriesTest.scala @@ -30,10 +30,10 @@ class DefaultUserRepositoriesTest extends AnyFlatSpec with BeforeAndAfterEach wi private val testConfig: ConfigProvider = new ConfigProvider("api/src/test/resources/application.yaml") private val emptyServiceAccount = ServiceAccountConfig("", Option(LdapUserCredentialsConfig("", "")), None) - private val enabledLdapTestConfig = Some(ActiveDirectoryLDAPConfig("", "", "", order = 2, emptyServiceAccount, None)) + private val enabledLdapTestConfig = Some(ActiveDirectoryLDAPConfig("", "", "", order = 2, emptyServiceAccount, None, None)) private val enabledUsersConfig = testConfig.getUsersConfig // has order = 1 - private val disabledLdapTestConfig = Some(ActiveDirectoryLDAPConfig("", "", "", order = 0, emptyServiceAccount, None)) + private val disabledLdapTestConfig = Some(ActiveDirectoryLDAPConfig("", "", "", order = 0, emptyServiceAccount, None, None)) private val disabledUsersConfig = Some(UsersConfig(Array.empty[UserConfig], order = 0)) private def createAuthConfigProviderUsing(optLdapConfig: Option[ActiveDirectoryLDAPConfig], optUsersConfig: Option[UsersConfig]): AuthConfigProvider = { From 864f79d88ad50cd68700b7921732a3eda0410c27 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Mon, 22 Jul 2024 16:19:01 +0200 Subject: [PATCH 11/33] Fixed Tests part 2 --- .../absa/loginsvc/rest/controller/TokenControllerTest.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala index 840dc0f3..0e1c83ef 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala @@ -26,8 +26,10 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.context.annotation.Import +import org.springframework.test.context.TestPropertySource import org.springframework.test.web.servlet.MockMvc import za.co.absa.loginsvc.model.User +import za.co.absa.loginsvc.rest.config.provider.ConfigProvider import za.co.absa.loginsvc.rest.model.{AccessToken, RefreshToken} import za.co.absa.loginsvc.rest.service.jwt.JWTService import za.co.absa.loginsvc.rest.{FakeAuthentication, RestResponseEntityExceptionHandler, SecurityConfig} @@ -36,7 +38,8 @@ import java.security.interfaces.RSAPublicKey import java.util.Base64 import scala.concurrent.duration._ -@Import(Array(classOf[SecurityConfig], classOf[RestResponseEntityExceptionHandler])) +@TestPropertySource(properties = Array("spring.config.location=api/src/test/resources/application.yaml")) +@Import(Array(classOf[ConfigProvider], classOf[SecurityConfig], classOf[RestResponseEntityExceptionHandler])) @WebMvcTest(controllers = Array(classOf[TokenController])) class TokenControllerTest extends AnyFlatSpec with ControllerIntegrationTestBase { From fb640655e677764e5d201f9f12ff40202fa361f3 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 31 Jul 2024 15:41:38 +0200 Subject: [PATCH 12/33] Added Changes to Dockerfile to include krb5 and keytab --- api/Dockerfile | 10 ++++++++++ api/src/main/resources/example.application.yaml | 1 + .../loginsvc/rest/config/auth/KerberosConfig.scala | 11 +++++++++-- .../KerberosSPNEGOAuthenticationProvider.scala | 5 ++--- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index f496afa9..16156de1 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -30,6 +30,10 @@ ARG LDAP_SSL_CERTS_PATH # ARG SSL_DNAME is defined below in the SSL-enabled image # In case you build the Dockerfile from another location than the default 'service' dir, provide a prefix to reach it ARG LS_PREFIX=. +# Provide path to the KRB5 location in .conf format +ARG SPNEGO_KRB5 +# Provide path to the relevant keytab in .keytab format +ARG SPNEGO_KEYTAB LABEL org.opencontainers.image.authors="ABSA" @@ -50,6 +54,12 @@ RUN mkdir -p /opt/certs # just PEMs are supported COPY $LDAP_SSL_CERTS_PATH/*.pem /opt/certs/ +RUN mkdir -p /opt/spnego +#Copy KRB5 +COPY $SPNEGO_KRB5 /opt/spnego/krb5.conf +#Copy keytab +COPY $SPNEGO_KEYTAB /opt/spnego/application.keytab + # Java 11 paths used RUN for file in `ls /opt/certs/*.pem`; \ do \ diff --git a/api/src/main/resources/example.application.yaml b/api/src/main/resources/example.application.yaml index a8b0ea3b..59fe75aa 100644 --- a/api/src/main/resources/example.application.yaml +++ b/api/src/main/resources/example.application.yaml @@ -76,6 +76,7 @@ loginsvc: enable-kerberos: krb-file-location: "/etc/krb5.conf" keytab-file-location: "/etc/keytab" + spn: "HTTP/Host" debug: true attributes: # The FieldName is the key used to search ldap and the value is the value used to name the JWT claim. diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala index a9b3e552..b915e446 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfig.scala @@ -19,7 +19,11 @@ package za.co.absa.loginsvc.rest.config.auth import za.co.absa.loginsvc.rest.config.validation.{ConfigValidatable, ConfigValidationException, ConfigValidationResult} import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess} -case class KerberosConfig(krbFileLocation: String, keytabFileLocation: String, debug: Option[Boolean]) extends ConfigValidatable { +case class KerberosConfig( + krbFileLocation: String, + keytabFileLocation: String, + spn: String, + debug: Option[Boolean]) extends ConfigValidatable { override def validate(): ConfigValidationResult = { val results = Seq( @@ -28,7 +32,10 @@ case class KerberosConfig(krbFileLocation: String, keytabFileLocation: String, d .getOrElse(ConfigValidationError(ConfigValidationException("krbFileLocation is empty"))), Option(keytabFileLocation) .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("keytabFileLocation is empty"))) + .getOrElse(ConfigValidationError(ConfigValidationException("keytabFileLocation is empty"))), + Option(spn) + .map(_ => ConfigValidationSuccess) + .getOrElse(ConfigValidationError(ConfigValidationException("spn is empty"))) ) results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 5940f4fb..e6461827 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -19,7 +19,7 @@ package za.co.absa.loginsvc.rest.provider.kerberos import org.slf4j.LoggerFactory import org.springframework.core.io.FileSystemResource import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.core.authority.{AuthorityUtils, SimpleGrantedAuthority} +import org.springframework.security.core.authority.AuthorityUtils import org.springframework.security.core.userdetails.{User, UserDetails, UserDetailsService} import org.springframework.security.kerberos.authentication.{KerberosAuthenticationProvider, KerberosServiceAuthenticationProvider} import org.springframework.security.kerberos.authentication.sun.{SunJaasKerberosClient, SunJaasKerberosTicketValidator} @@ -29,7 +29,6 @@ import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { //TODO: Split into Multiple files for neater implementation - private val serviceAccount = activeDirectoryLDAPConfig.serviceAccount private val kerberos = activeDirectoryLDAPConfig.enableKerberos.get private val kerberosDebug = kerberos.debug.getOrElse(false) private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) @@ -72,7 +71,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = { val ticketValidator: SunJaasKerberosTicketValidator = new SunJaasKerberosTicketValidator() - ticketValidator.setServicePrincipal("HTTP/localhost") + ticketValidator.setServicePrincipal(kerberos.spn) ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) ticketValidator.setDebug(true) ticketValidator From d26bf94679413ac51a386dded3b1bb124107e0ac Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Fri, 2 Aug 2024 14:33:15 +0200 Subject: [PATCH 13/33] Revert Dockerfile change --- api/Dockerfile | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index 16156de1..f496afa9 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -30,10 +30,6 @@ ARG LDAP_SSL_CERTS_PATH # ARG SSL_DNAME is defined below in the SSL-enabled image # In case you build the Dockerfile from another location than the default 'service' dir, provide a prefix to reach it ARG LS_PREFIX=. -# Provide path to the KRB5 location in .conf format -ARG SPNEGO_KRB5 -# Provide path to the relevant keytab in .keytab format -ARG SPNEGO_KEYTAB LABEL org.opencontainers.image.authors="ABSA" @@ -54,12 +50,6 @@ RUN mkdir -p /opt/certs # just PEMs are supported COPY $LDAP_SSL_CERTS_PATH/*.pem /opt/certs/ -RUN mkdir -p /opt/spnego -#Copy KRB5 -COPY $SPNEGO_KRB5 /opt/spnego/krb5.conf -#Copy keytab -COPY $SPNEGO_KEYTAB /opt/spnego/application.keytab - # Java 11 paths used RUN for file in `ls /opt/certs/*.pem`; \ do \ From 98731acff101b0f1373f75517bf8b4f4cabab964 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Mon, 5 Aug 2024 13:51:29 +0200 Subject: [PATCH 14/33] Add Ldap required functions for future testing once Dummy Service is verified --- ...iveDirectoryLdapAuthoritiesPopulator.scala | 45 +++++++++++++++++++ ...KerberosSPNEGOAuthenticationProvider.scala | 34 +++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala new file mode 100644 index 00000000..8fb34807 --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala @@ -0,0 +1,45 @@ +/* + * 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.loginsvc.rest.provider.kerberos + +import org.springframework.ldap.core.DirContextOperations +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.{AuthorityUtils, SimpleGrantedAuthority} +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator + +import java.util +import javax.naming.ldap.LdapName + +class ActiveDirectoryLdapAuthoritiesPopulator extends LdapAuthoritiesPopulator { + + import scala.collection.JavaConverters._ + + override def getGrantedAuthorities(userData: DirContextOperations, username: String): util.Collection[_ <: GrantedAuthority] = { + val groups = userData.getStringAttributes("memberOf") + + if (groups == null) { + AuthorityUtils.NO_AUTHORITIES + } + else { + groups.map({group => + val ldapName = new LdapName(group) + val role = ldapName.getRdn(ldapName.size() - 1).getValue.toString + new SimpleGrantedAuthority(role) + }).toList.asJava + } + } +} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index e6461827..f452838c 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -23,13 +23,18 @@ import org.springframework.security.core.authority.AuthorityUtils import org.springframework.security.core.userdetails.{User, UserDetails, UserDetailsService} import org.springframework.security.kerberos.authentication.{KerberosAuthenticationProvider, KerberosServiceAuthenticationProvider} import org.springframework.security.kerberos.authentication.sun.{SunJaasKerberosClient, SunJaasKerberosTicketValidator} +import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig +import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch +import org.springframework.security.ldap.userdetails.{LdapUserDetailsMapper, LdapUserDetailsService} import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { //TODO: Split into Multiple files for neater implementation - private val kerberos = activeDirectoryLDAPConfig.enableKerberos.get + private val ldapConfig = activeDirectoryLDAPConfig + private val kerberos = ldapConfig.enableKerberos.get private val kerberosDebug = kerberos.debug.getOrElse(false) private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) logger.debug(s"KerberosSPNEGOAuthenticationProvider init") @@ -39,7 +44,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire if (kerberos.krbFileLocation.nonEmpty) { logger.info(s"Using KRB5 CONF from ${kerberos.krbFileLocation}") - System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) + System.setProperty("java.security.krb5.ini", kerberos.krbFileLocation) } def kerberosAuthenticationProvider(): KerberosAuthenticationProvider = @@ -77,6 +82,31 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire ticketValidator } + private def loginConfig(): SunJaasKrb5LoginConfig = { + val loginConfig = new SunJaasKrb5LoginConfig() + loginConfig.setServicePrincipal(kerberos.spn) + loginConfig.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) + loginConfig.setDebug(kerberosDebug) + loginConfig.setIsInitiator(true) + loginConfig.setUseTicketCache(false) + loginConfig.afterPropertiesSet() + loginConfig + } + + private def kerberosLdapContextSource(): KerberosLdapContextSource = { + val contextSource = new KerberosLdapContextSource(ldapConfig.url) + contextSource.setLoginConfig(loginConfig()) + contextSource.afterPropertiesSet() + contextSource + } + + private def ldapUserDetailsService() = { + val userSearch = new FilterBasedLdapUserSearch(activeDirectoryLDAPConfig.domain, activeDirectoryLDAPConfig.searchFilter, kerberosLdapContextSource()) + val service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator()) + service.setUserDetailsMapper(new LdapUserDetailsMapper()) + service + } + private def dummyUserDetailsService = new DummyUserDetailsService } From 36c75f4afe5f8d896daa4ea2b7ed7afad0815217 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Mon, 12 Aug 2024 10:34:25 +0200 Subject: [PATCH 15/33] Fix krb5 property --- .../kerberos/KerberosSPNEGOAuthenticationProvider.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index f452838c..87f4110c 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -44,7 +44,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire if (kerberos.krbFileLocation.nonEmpty) { logger.info(s"Using KRB5 CONF from ${kerberos.krbFileLocation}") - System.setProperty("java.security.krb5.ini", kerberos.krbFileLocation) + System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) } def kerberosAuthenticationProvider(): KerberosAuthenticationProvider = From 0cf958637de55d8a78e2312490d3ce667996fb1e Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Tue, 13 Aug 2024 11:28:02 +0200 Subject: [PATCH 16/33] Set AfterProperties Set for Properties to ensure KRB5 is read --- .../kerberos/KerberosSPNEGOAuthenticationProvider.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 87f4110c..9ef06092 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -62,6 +62,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire { val filter: SpnegoAuthenticationProcessingFilter = new SpnegoAuthenticationProcessingFilter() filter.setAuthenticationManager(authenticationManager) + filter.afterPropertiesSet() filter } @@ -70,6 +71,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire val provider: KerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider() provider.setTicketValidator(sunJaasKerberosTicketValidator()) provider.setUserDetailsService(dummyUserDetailsService) + provider.afterPropertiesSet() provider } @@ -79,6 +81,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire ticketValidator.setServicePrincipal(kerberos.spn) ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) ticketValidator.setDebug(true) + ticketValidator.afterPropertiesSet() ticketValidator } From 9e35e25c9f0f53b4cd6f2eddff96a39eb7d9d952 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 11:35:52 +0200 Subject: [PATCH 17/33] Remove Dummy User Service and enable LdapUserDetailsService --- .../KerberosSPNEGOAuthenticationProvider.scala | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 9ef06092..573a351d 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -70,7 +70,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire { val provider: KerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider() provider.setTicketValidator(sunJaasKerberosTicketValidator()) - provider.setUserDetailsService(dummyUserDetailsService) + provider.setUserDetailsService(ldapUserDetailsService) provider.afterPropertiesSet() provider } @@ -103,21 +103,10 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire contextSource } - private def ldapUserDetailsService() = { + private def ldapUserDetailsService = { val userSearch = new FilterBasedLdapUserSearch(activeDirectoryLDAPConfig.domain, activeDirectoryLDAPConfig.searchFilter, kerberosLdapContextSource()) val service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator()) service.setUserDetailsMapper(new LdapUserDetailsMapper()) service } - - private def dummyUserDetailsService = new DummyUserDetailsService -} - -class DummyUserDetailsService extends UserDetailsService { - private val logger = LoggerFactory.getLogger(classOf[DummyUserDetailsService]) - override def loadUserByUsername(username: String): UserDetails = - { - logger.info(s"returning dummy of $username") - new User(username, "{noop}notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")) - } } From 3866b40e8d7d498b108681a8b5912bc9225a43b8 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 11:39:48 +0200 Subject: [PATCH 18/33] Minor Fix --- .../kerberos/KerberosSPNEGOAuthenticationProvider.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 573a351d..34872266 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -54,7 +54,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire client.setDebug(true) provider.setKerberosClient(client) - provider.setUserDetailsService(dummyUserDetailsService) + provider.setUserDetailsService(ldapUserDetailsService) provider } From 6d6fb83e2c36e76c84a007cc85688c306e0160e5 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 12:46:48 +0200 Subject: [PATCH 19/33] Test Just Authentication Provider Only --- .../co/absa/loginsvc/rest/SecurityConfig.scala | 2 +- .../KerberosSPNEGOAuthenticationProvider.scala | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index 4bb71e21..468547ee 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -69,7 +69,7 @@ class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { http.addFilterBefore( kerberos.spnegoAuthenticationProcessingFilter( - new ProviderManager(provider, serviceProvider)), + new ProviderManager(provider)), classOf[BasicAuthenticationFilter]) } } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 34872266..9ef06092 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -54,7 +54,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire client.setDebug(true) provider.setKerberosClient(client) - provider.setUserDetailsService(ldapUserDetailsService) + provider.setUserDetailsService(dummyUserDetailsService) provider } @@ -70,7 +70,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire { val provider: KerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider() provider.setTicketValidator(sunJaasKerberosTicketValidator()) - provider.setUserDetailsService(ldapUserDetailsService) + provider.setUserDetailsService(dummyUserDetailsService) provider.afterPropertiesSet() provider } @@ -103,10 +103,21 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire contextSource } - private def ldapUserDetailsService = { + private def ldapUserDetailsService() = { val userSearch = new FilterBasedLdapUserSearch(activeDirectoryLDAPConfig.domain, activeDirectoryLDAPConfig.searchFilter, kerberosLdapContextSource()) val service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator()) service.setUserDetailsMapper(new LdapUserDetailsMapper()) service } + + private def dummyUserDetailsService = new DummyUserDetailsService +} + +class DummyUserDetailsService extends UserDetailsService { + private val logger = LoggerFactory.getLogger(classOf[DummyUserDetailsService]) + override def loadUserByUsername(username: String): UserDetails = + { + logger.info(s"returning dummy of $username") + new User(username, "{noop}notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")) + } } From a4871739c395ab5f131cbade37a1afffce116efb Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 14:58:37 +0200 Subject: [PATCH 20/33] Test Just Service Authentication Provider Only --- .../absa/loginsvc/rest/SecurityConfig.scala | 3 +- ...KerberosSPNEGOAuthenticationProvider.scala | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index 468547ee..7fd42329 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -64,12 +64,11 @@ class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { { val kerberos = new KerberosSPNEGOAuthenticationProvider(KerberosConfig) - val provider = kerberos.kerberosAuthenticationProvider() val serviceProvider = kerberos.kerberosServiceAuthenticationProvider() http.addFilterBefore( kerberos.spnegoAuthenticationProcessingFilter( - new ProviderManager(provider)), + new ProviderManager(serviceProvider)), classOf[BasicAuthenticationFilter]) } } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 9ef06092..f12cb4ac 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -18,17 +18,20 @@ package za.co.absa.loginsvc.rest.provider.kerberos import org.slf4j.LoggerFactory import org.springframework.core.io.FileSystemResource -import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.core.authority.AuthorityUtils +import org.springframework.security.authentication.{AuthenticationManager, BadCredentialsException} +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.{User, UserDetails, UserDetailsService} -import org.springframework.security.kerberos.authentication.{KerberosAuthenticationProvider, KerberosServiceAuthenticationProvider} -import org.springframework.security.kerberos.authentication.sun.{SunJaasKerberosClient, SunJaasKerberosTicketValidator} +import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter import org.springframework.security.ldap.search.FilterBasedLdapUserSearch import org.springframework.security.ldap.userdetails.{LdapUserDetailsMapper, LdapUserDetailsService} import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig +import za.co.absa.loginsvc.rest.service.search.LdapUserRepository + +import scala.collection.JavaConverters._ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { @@ -47,17 +50,6 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) } - def kerberosAuthenticationProvider(): KerberosAuthenticationProvider = - { - val provider: KerberosAuthenticationProvider = new KerberosAuthenticationProvider() - val client: SunJaasKerberosClient = new SunJaasKerberosClient() - - client.setDebug(true) - provider.setKerberosClient(client) - provider.setUserDetailsService(dummyUserDetailsService) - provider - } - def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager): SpnegoAuthenticationProcessingFilter = { val filter: SpnegoAuthenticationProcessingFilter = new SpnegoAuthenticationProcessingFilter() @@ -110,14 +102,25 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire service } - private def dummyUserDetailsService = new DummyUserDetailsService + private def dummyUserDetailsService = DummyUserDetailsService(ldapConfig) } -class DummyUserDetailsService extends UserDetailsService { +case class DummyUserDetailsService(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) extends UserDetailsService { private val logger = LoggerFactory.getLogger(classOf[DummyUserDetailsService]) override def loadUserByUsername(username: String): UserDetails = { - logger.info(s"returning dummy of $username") - new User(username, "{noop}notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")) + val ldapContext = new LdapUserRepository(activeDirectoryLDAPConfig) + val user = ldapContext.searchForUser(username) + if(user.isEmpty) + throw new BadCredentialsException("Cannot Find User in Ldap") + + val grantedAuthorities = user.get.groups.map(new SimpleGrantedAuthority(_)).toList.asJava + + logger.info("Found Kerberos User:" + user.get.name) + User.builder() + .username(user.get.name) + .password("") + .authorities(grantedAuthorities) + .build() } } From 9e864c323bb28dc6763b7ed4483b2aeb7556022f Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 15:08:54 +0200 Subject: [PATCH 21/33] Minor Change --- .../kerberos/KerberosSPNEGOAuthenticationProvider.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index f12cb4ac..36c43def 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -109,8 +109,13 @@ case class DummyUserDetailsService(activeDirectoryLDAPConfig: ActiveDirectoryLDA private val logger = LoggerFactory.getLogger(classOf[DummyUserDetailsService]) override def loadUserByUsername(username: String): UserDetails = { + val userName = if(username.contains("@")) { + username.split("@").head + } else { + username + } val ldapContext = new LdapUserRepository(activeDirectoryLDAPConfig) - val user = ldapContext.searchForUser(username) + val user = ldapContext.searchForUser(userName) if(user.isEmpty) throw new BadCredentialsException("Cannot Find User in Ldap") From 7d8f197d2eecfa48a7c29b565921bab78321e2e4 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 16:23:48 +0200 Subject: [PATCH 22/33] Add new UserDetailsType --- .../rest/controller/TokenController.scala | 9 +++- .../rest/model/KerberosUserDetails.scala | 43 +++++++++++++++++++ ...KerberosSPNEGOAuthenticationProvider.scala | 7 +-- 3 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala index af4be469..f06e1eef 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala @@ -26,8 +26,9 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.{HttpStatus, MediaType} import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation._ +import org.springframework.web.server.ResponseStatusException import za.co.absa.loginsvc.model.User -import za.co.absa.loginsvc.rest.model.{PublicKey, TokensWrapper} +import za.co.absa.loginsvc.rest.model.{KerberosUserDetails, PublicKey, TokensWrapper} import za.co.absa.loginsvc.rest.service.jwt.JWTService import za.co.absa.loginsvc.utils.OptionUtils.ImplicitBuilderExt @@ -71,7 +72,11 @@ class TokenController @Autowired()(jwtService: JWTService) { @ResponseStatus(HttpStatus.OK) @SecurityRequirement(name = "basicAuth") def generateToken(authentication: Authentication, @RequestParam("group-prefixes") groupPrefixes: Optional[String]): CompletableFuture[TokensWrapper] = { - val user = authentication.getPrincipal.asInstanceOf[User] + val user: User = authentication.getPrincipal match { + case u: User => u + case k: KerberosUserDetails => User(k.username, k.groups, k.optionalAttributes); + case _ => throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated or unknown principal type") + } val groupPrefixesStrScala = groupPrefixes.toScalaOption val filteredGroupsUser = user.applyIfDefined(groupPrefixesStrScala) { (user: User, prefixesStr: String) => diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala new file mode 100644 index 00000000..f9cc309c --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala @@ -0,0 +1,43 @@ +/* + * 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.loginsvc.rest.model + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +import java.util +import scala.collection.JavaConverters._ + +case class KerberosUserDetails(username: String, groups: Seq[String], optionalAttributes: Map[String, Option[AnyRef]]) +extends UserDetails +{ + override def getAuthorities: util.Collection[_ <: GrantedAuthority] = + groups.map(new SimpleGrantedAuthority(_)).toList.asJava + + override def getPassword: String = "" + + override def getUsername: String = username + + override def isAccountNonExpired: Boolean = true + + override def isAccountNonLocked: Boolean = true + + override def isCredentialsNonExpired: Boolean = true + + override def isEnabled: Boolean = true +} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 36c43def..91238483 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -29,6 +29,7 @@ import org.springframework.security.kerberos.web.authentication.SpnegoAuthentica import org.springframework.security.ldap.search.FilterBasedLdapUserSearch import org.springframework.security.ldap.userdetails.{LdapUserDetailsMapper, LdapUserDetailsService} import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig +import za.co.absa.loginsvc.rest.model.KerberosUserDetails import za.co.absa.loginsvc.rest.service.search.LdapUserRepository import scala.collection.JavaConverters._ @@ -122,10 +123,6 @@ case class DummyUserDetailsService(activeDirectoryLDAPConfig: ActiveDirectoryLDA val grantedAuthorities = user.get.groups.map(new SimpleGrantedAuthority(_)).toList.asJava logger.info("Found Kerberos User:" + user.get.name) - User.builder() - .username(user.get.name) - .password("") - .authorities(grantedAuthorities) - .build() + KerberosUserDetails(user.get.name, user.get.groups, user.get.optionalAttributes) } } From f1e665f33dfafc370a34c26a793452ed04c28165 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 17:05:56 +0200 Subject: [PATCH 23/33] Slight Cleanup of unused code --- ...KerberosSPNEGOAuthenticationProvider.scala | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 91238483..64e90cfc 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -19,15 +19,10 @@ package za.co.absa.loginsvc.rest.provider.kerberos import org.slf4j.LoggerFactory import org.springframework.core.io.FileSystemResource import org.springframework.security.authentication.{AuthenticationManager, BadCredentialsException} -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.core.userdetails.{User, UserDetails, UserDetailsService} +import org.springframework.security.core.userdetails.{UserDetails, UserDetailsService} import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator -import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig -import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter -import org.springframework.security.ldap.search.FilterBasedLdapUserSearch -import org.springframework.security.ldap.userdetails.{LdapUserDetailsMapper, LdapUserDetailsService} import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig import za.co.absa.loginsvc.rest.model.KerberosUserDetails import za.co.absa.loginsvc.rest.service.search.LdapUserRepository @@ -78,31 +73,6 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire ticketValidator } - private def loginConfig(): SunJaasKrb5LoginConfig = { - val loginConfig = new SunJaasKrb5LoginConfig() - loginConfig.setServicePrincipal(kerberos.spn) - loginConfig.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation)) - loginConfig.setDebug(kerberosDebug) - loginConfig.setIsInitiator(true) - loginConfig.setUseTicketCache(false) - loginConfig.afterPropertiesSet() - loginConfig - } - - private def kerberosLdapContextSource(): KerberosLdapContextSource = { - val contextSource = new KerberosLdapContextSource(ldapConfig.url) - contextSource.setLoginConfig(loginConfig()) - contextSource.afterPropertiesSet() - contextSource - } - - private def ldapUserDetailsService() = { - val userSearch = new FilterBasedLdapUserSearch(activeDirectoryLDAPConfig.domain, activeDirectoryLDAPConfig.searchFilter, kerberosLdapContextSource()) - val service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator()) - service.setUserDetailsMapper(new LdapUserDetailsMapper()) - service - } - private def dummyUserDetailsService = DummyUserDetailsService(ldapConfig) } @@ -120,8 +90,6 @@ case class DummyUserDetailsService(activeDirectoryLDAPConfig: ActiveDirectoryLDA if(user.isEmpty) throw new BadCredentialsException("Cannot Find User in Ldap") - val grantedAuthorities = user.get.groups.map(new SimpleGrantedAuthority(_)).toList.asJava - logger.info("Found Kerberos User:" + user.get.name) KerberosUserDetails(user.get.name, user.get.groups, user.get.optionalAttributes) } From e6dca19b0c53e21ef3af438437c0577a9d8f5a8f Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Wed, 14 Aug 2024 17:42:52 +0200 Subject: [PATCH 24/33] Disable Cache --- .../provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index 64e90cfc..eabd59d2 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -44,6 +44,7 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire if (kerberos.krbFileLocation.nonEmpty) { logger.info(s"Using KRB5 CONF from ${kerberos.krbFileLocation}") System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) + System.setProperty("KRB5RCACHETYPE","none") } def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager): SpnegoAuthenticationProcessingFilter = From 85dac0513e85db7a3ccaceeb0da84b63c8cdf08d Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 08:57:50 +0000 Subject: [PATCH 25/33] Disable Cache attempt 2 --- .../kerberos/KerberosSPNEGOAuthenticationProvider.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index eabd59d2..a8c89b2c 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -40,11 +40,11 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire System.setProperty("javax.net.debug", kerberosDebug.toString) System.setProperty("sun.security.krb5.debug", kerberosDebug.toString) + System.setProperty("sun.security.krb5.rcache", "none") if (kerberos.krbFileLocation.nonEmpty) { logger.info(s"Using KRB5 CONF from ${kerberos.krbFileLocation}") System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) - System.setProperty("KRB5RCACHETYPE","none") } def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager): SpnegoAuthenticationProcessingFilter = From 824937735d0b2fdf15cf4768f2295a8bbcbbe049 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 11:34:00 +0200 Subject: [PATCH 26/33] Add UserProvider --- .../za/co/absa/loginsvc/rest/SecurityConfig.scala | 3 ++- .../KerberosSPNEGOAuthenticationProvider.scala | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index 7fd42329..4bb71e21 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -64,11 +64,12 @@ class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { { val kerberos = new KerberosSPNEGOAuthenticationProvider(KerberosConfig) + val provider = kerberos.kerberosAuthenticationProvider() val serviceProvider = kerberos.kerberosServiceAuthenticationProvider() http.addFilterBefore( kerberos.spnegoAuthenticationProcessingFilter( - new ProviderManager(serviceProvider)), + new ProviderManager(provider, serviceProvider)), classOf[BasicAuthenticationFilter]) } } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index a8c89b2c..d3f7c640 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -20,8 +20,8 @@ import org.slf4j.LoggerFactory import org.springframework.core.io.FileSystemResource import org.springframework.security.authentication.{AuthenticationManager, BadCredentialsException} import org.springframework.security.core.userdetails.{UserDetails, UserDetailsService} -import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider -import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator +import org.springframework.security.kerberos.authentication.{KerberosAuthenticationProvider, KerberosServiceAuthenticationProvider} +import org.springframework.security.kerberos.authentication.sun.{SunJaasKerberosClient, SunJaasKerberosTicketValidator} import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig import za.co.absa.loginsvc.rest.model.KerberosUserDetails @@ -55,6 +55,17 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire filter } + def kerberosAuthenticationProvider(): KerberosAuthenticationProvider = + { + val provider: KerberosAuthenticationProvider = new KerberosAuthenticationProvider() + val client: SunJaasKerberosClient = new SunJaasKerberosClient() + + client.setDebug(kerberosDebug) + provider.setKerberosClient(client) + provider.setUserDetailsService(dummyUserDetailsService) + provider + } + def kerberosServiceAuthenticationProvider(): KerberosServiceAuthenticationProvider = { val provider: KerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider() From 665696636bc4529d9346123c71dcb5b3dd2b8daf Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 13:03:47 +0200 Subject: [PATCH 27/33] Clean up code --- .../absa/loginsvc/rest/SecurityConfig.scala | 19 +++----- .../rest/controller/TokenController.scala | 3 +- .../rest/model/KerberosUserDetails.scala | 9 ++-- ...iveDirectoryLdapAuthoritiesPopulator.scala | 45 ------------------ ...KerberosSPNEGOAuthenticationProvider.scala | 47 +++++-------------- .../kerberos/KerberosUserDetailsService.scala | 34 ++++++++++++++ 6 files changed, 59 insertions(+), 98 deletions(-) delete mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala create mode 100644 api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala index 4bb71e21..687f5026 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/SecurityConfig.scala @@ -18,7 +18,6 @@ package za.co.absa.loginsvc.rest import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.{Bean, Configuration} -import org.springframework.security.authentication.{AuthenticationManager, ProviderManager} import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy @@ -29,10 +28,9 @@ import za.co.absa.loginsvc.rest.provider.kerberos.KerberosSPNEGOAuthenticationPr @Configuration @EnableWebSecurity -class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { +class SecurityConfig @Autowired()(authConfigsProvider: AuthConfigProvider) { - //TODO: Neaten up checking for Config - private val KerberosConfig = authConfigsProvider.getLdapConfig.orNull + private val ldapConfig = authConfigsProvider.getLdapConfig.orNull @Bean def filterChain(http: HttpSecurity): SecurityFilterChain = { @@ -57,19 +55,14 @@ class SecurityConfig@Autowired()(authConfigsProvider: AuthConfigProvider) { .and() .httpBasic() - //TODO: Neaten up checking for Config - if(KerberosConfig != null) + if(ldapConfig != null) { - if(KerberosConfig.enableKerberos.isDefined) + if(ldapConfig.enableKerberos.isDefined) { - val kerberos = new KerberosSPNEGOAuthenticationProvider(KerberosConfig) - - val provider = kerberos.kerberosAuthenticationProvider() - val serviceProvider = kerberos.kerberosServiceAuthenticationProvider() + val kerberos = new KerberosSPNEGOAuthenticationProvider(ldapConfig) http.addFilterBefore( - kerberos.spnegoAuthenticationProcessingFilter( - new ProviderManager(provider, serviceProvider)), + kerberos.spnegoAuthenticationProcessingFilter, classOf[BasicAuthenticationFilter]) } } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala index f06e1eef..bfd53dd1 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala @@ -72,9 +72,10 @@ class TokenController @Autowired()(jwtService: JWTService) { @ResponseStatus(HttpStatus.OK) @SecurityRequirement(name = "basicAuth") def generateToken(authentication: Authentication, @RequestParam("group-prefixes") groupPrefixes: Optional[String]): CompletableFuture[TokensWrapper] = { + val user: User = authentication.getPrincipal match { case u: User => u - case k: KerberosUserDetails => User(k.username, k.groups, k.optionalAttributes); + case k: KerberosUserDetails => k.getUser case _ => throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated or unknown principal type") } val groupPrefixesStrScala = groupPrefixes.toScalaOption diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala index f9cc309c..2b9f781e 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/model/KerberosUserDetails.scala @@ -19,19 +19,20 @@ package za.co.absa.loginsvc.rest.model import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.UserDetails +import za.co.absa.loginsvc.model.User import java.util import scala.collection.JavaConverters._ -case class KerberosUserDetails(username: String, groups: Seq[String], optionalAttributes: Map[String, Option[AnyRef]]) +case class KerberosUserDetails(user: User) extends UserDetails { override def getAuthorities: util.Collection[_ <: GrantedAuthority] = - groups.map(new SimpleGrantedAuthority(_)).toList.asJava + user.groups.map(new SimpleGrantedAuthority(_)).toList.asJava override def getPassword: String = "" - override def getUsername: String = username + override def getUsername: String = user.name override def isAccountNonExpired: Boolean = true @@ -40,4 +41,6 @@ extends UserDetails override def isCredentialsNonExpired: Boolean = true override def isEnabled: Boolean = true + + def getUser: User = user } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala deleted file mode 100644 index 8fb34807..00000000 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/ActiveDirectoryLdapAuthoritiesPopulator.scala +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.loginsvc.rest.provider.kerberos - -import org.springframework.ldap.core.DirContextOperations -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.core.authority.{AuthorityUtils, SimpleGrantedAuthority} -import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator - -import java.util -import javax.naming.ldap.LdapName - -class ActiveDirectoryLdapAuthoritiesPopulator extends LdapAuthoritiesPopulator { - - import scala.collection.JavaConverters._ - - override def getGrantedAuthorities(userData: DirContextOperations, username: String): util.Collection[_ <: GrantedAuthority] = { - val groups = userData.getStringAttributes("memberOf") - - if (groups == null) { - AuthorityUtils.NO_AUTHORITIES - } - else { - groups.map({group => - val ldapName = new LdapName(group) - val role = ldapName.getRdn(ldapName.size() - 1).getValue.toString - new SimpleGrantedAuthority(role) - }).toList.asJava - } - } -} diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index d3f7c640..fe0d308e 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -18,25 +18,19 @@ package za.co.absa.loginsvc.rest.provider.kerberos import org.slf4j.LoggerFactory import org.springframework.core.io.FileSystemResource -import org.springframework.security.authentication.{AuthenticationManager, BadCredentialsException} -import org.springframework.security.core.userdetails.{UserDetails, UserDetailsService} +import org.springframework.security.authentication.ProviderManager import org.springframework.security.kerberos.authentication.{KerberosAuthenticationProvider, KerberosServiceAuthenticationProvider} import org.springframework.security.kerberos.authentication.sun.{SunJaasKerberosClient, SunJaasKerberosTicketValidator} import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig -import za.co.absa.loginsvc.rest.model.KerberosUserDetails -import za.co.absa.loginsvc.rest.service.search.LdapUserRepository - -import scala.collection.JavaConverters._ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) { - //TODO: Split into Multiple files for neater implementation private val ldapConfig = activeDirectoryLDAPConfig private val kerberos = ldapConfig.enableKerberos.get private val kerberosDebug = kerberos.debug.getOrElse(false) private val logger = LoggerFactory.getLogger(classOf[KerberosSPNEGOAuthenticationProvider]) - logger.debug(s"KerberosSPNEGOAuthenticationProvider init") + logger.info(s"KerberosSPNEGOAuthenticationProvider init") System.setProperty("javax.net.debug", kerberosDebug.toString) System.setProperty("sun.security.krb5.debug", kerberosDebug.toString) @@ -47,35 +41,35 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire System.setProperty("java.security.krb5.conf", kerberos.krbFileLocation) } - def spnegoAuthenticationProcessingFilter(authenticationManager: AuthenticationManager): SpnegoAuthenticationProcessingFilter = + def spnegoAuthenticationProcessingFilter: SpnegoAuthenticationProcessingFilter = { val filter: SpnegoAuthenticationProcessingFilter = new SpnegoAuthenticationProcessingFilter() - filter.setAuthenticationManager(authenticationManager) + filter.setAuthenticationManager(new ProviderManager(kerberosAuthenticationProvider, kerberosServiceAuthenticationProvider)) filter.afterPropertiesSet() filter } - def kerberosAuthenticationProvider(): KerberosAuthenticationProvider = + def kerberosAuthenticationProvider: KerberosAuthenticationProvider = { val provider: KerberosAuthenticationProvider = new KerberosAuthenticationProvider() val client: SunJaasKerberosClient = new SunJaasKerberosClient() client.setDebug(kerberosDebug) provider.setKerberosClient(client) - provider.setUserDetailsService(dummyUserDetailsService) + provider.setUserDetailsService(kerberosUserDetailsService) provider } - def kerberosServiceAuthenticationProvider(): KerberosServiceAuthenticationProvider = + def kerberosServiceAuthenticationProvider: KerberosServiceAuthenticationProvider = { val provider: KerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider() - provider.setTicketValidator(sunJaasKerberosTicketValidator()) - provider.setUserDetailsService(dummyUserDetailsService) + provider.setTicketValidator(sunJaasKerberosTicketValidator) + provider.setUserDetailsService(kerberosUserDetailsService) provider.afterPropertiesSet() provider } - private def sunJaasKerberosTicketValidator(): SunJaasKerberosTicketValidator = + private def sunJaasKerberosTicketValidator: SunJaasKerberosTicketValidator = { val ticketValidator: SunJaasKerberosTicketValidator = new SunJaasKerberosTicketValidator() ticketValidator.setServicePrincipal(kerberos.spn) @@ -85,24 +79,5 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire ticketValidator } - private def dummyUserDetailsService = DummyUserDetailsService(ldapConfig) -} - -case class DummyUserDetailsService(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) extends UserDetailsService { - private val logger = LoggerFactory.getLogger(classOf[DummyUserDetailsService]) - override def loadUserByUsername(username: String): UserDetails = - { - val userName = if(username.contains("@")) { - username.split("@").head - } else { - username - } - val ldapContext = new LdapUserRepository(activeDirectoryLDAPConfig) - val user = ldapContext.searchForUser(userName) - if(user.isEmpty) - throw new BadCredentialsException("Cannot Find User in Ldap") - - logger.info("Found Kerberos User:" + user.get.name) - KerberosUserDetails(user.get.name, user.get.groups, user.get.optionalAttributes) - } + private def kerberosUserDetailsService = KerberosUserDetailsService(ldapConfig) } diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala new file mode 100644 index 00000000..6aee783f --- /dev/null +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala @@ -0,0 +1,34 @@ +package za.co.absa.loginsvc.rest.provider.kerberos + +import org.slf4j.LoggerFactory +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.core.userdetails.{UserDetails, UserDetailsService} +import za.co.absa.loginsvc.rest.config.auth.ActiveDirectoryLDAPConfig +import za.co.absa.loginsvc.rest.model.KerberosUserDetails +import za.co.absa.loginsvc.rest.service.search.LdapUserRepository + +case class KerberosUserDetailsService(activeDirectoryLDAPConfig: ActiveDirectoryLDAPConfig) extends UserDetailsService { + + private val logger = LoggerFactory.getLogger(classOf[KerberosUserDetailsService]) + + override def loadUserByUsername(username: String): UserDetails = + { + val name = if(username.contains("@")) { + username.split("@").head + } else { + username + } + + val ldapContext = new LdapUserRepository(activeDirectoryLDAPConfig) + logger.info(s"Searching for user:$name") + val userOption = ldapContext.searchForUser(name) + + if(userOption.isEmpty) + throw new BadCredentialsException(s"Cannot Find User, $name, in Ldap") + + val user = userOption.get + logger.info(s"Found Kerberos User: ${user.name}") + KerberosUserDetails(user) + } +} + From 821ba5acff1f2a1d4ed938658a06b955218d078e Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 13:07:22 +0200 Subject: [PATCH 28/33] Add Missing License --- .../kerberos/KerberosUserDetailsService.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala index 6aee783f..9d04403f 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala @@ -1,3 +1,19 @@ +/* + * 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.loginsvc.rest.provider.kerberos import org.slf4j.LoggerFactory From 89df77b3bd6376bffbd9d5bd82797c15402c4ad2 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 13:15:14 +0200 Subject: [PATCH 29/33] Fix Logging --- .../rest/provider/kerberos/KerberosUserDetailsService.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala index 9d04403f..c03c0b2c 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosUserDetailsService.scala @@ -36,11 +36,11 @@ case class KerberosUserDetailsService(activeDirectoryLDAPConfig: ActiveDirectory } val ldapContext = new LdapUserRepository(activeDirectoryLDAPConfig) - logger.info(s"Searching for user:$name") + logger.info(s"Searching for kerberos user:$name") val userOption = ldapContext.searchForUser(name) if(userOption.isEmpty) - throw new BadCredentialsException(s"Cannot Find User, $name, in Ldap") + throw new BadCredentialsException(s"Cannot find kerberos user, $name, in Ldap") val user = userOption.get logger.info(s"Found Kerberos User: ${user.name}") From fdb43a370689b1a08df407427fa26ce0a638ce34 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 14:44:54 +0200 Subject: [PATCH 30/33] Add Testing --- .../loginsvc/rest/FakeAuthentication.scala | 12 ++++ .../auth/ActiveDirectoryLDAPConfigTest.scala | 2 +- .../rest/config/auth/KerberosConfigTest.scala | 42 +++++++++++ .../auth/ServiceAccountConfigTest.scala | 69 +++++++++++++++++++ .../config/provider/ConfigProviderTest.scala | 1 + .../rest/controller/TokenControllerTest.scala | 15 +++- 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala create mode 100644 api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfigTest.scala diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala index 542f2384..fb1f8220 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala @@ -16,10 +16,13 @@ package za.co.absa.loginsvc.rest +import org.mockito.Mockito.mock import org.springframework.security.authentication.{AnonymousAuthenticationToken, UsernamePasswordAuthenticationToken} import org.springframework.security.core.authority.AuthorityUtils import org.springframework.security.core.{Authentication, GrantedAuthority} +import org.springframework.security.kerberos.authentication.{KerberosServiceRequestToken, KerberosTicketValidation} import za.co.absa.loginsvc.model.User +import za.co.absa.loginsvc.rest.model.KerberosUserDetails import java.util @@ -38,4 +41,13 @@ object FakeAuthentication { "key", "anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS") ) + val fakeUserDetails: KerberosUserDetails = KerberosUserDetails(fakeUser) + + private val ticketValidation = mock(classOf[KerberosTicketValidation]) + private val ticket = Array[Byte](1,2,3,4) + + val fakeUserDetailsAuthentication: Authentication = new KerberosServiceRequestToken( + fakeUserDetails, ticketValidation, fakeUserDetails.getAuthorities, ticket + ) + } diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala index 8ee2b999..453f1938 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ActiveDirectoryLDAPConfigTest.scala @@ -25,7 +25,7 @@ class ActiveDirectoryLDAPConfigTest extends AnyFlatSpec with Matchers { private val integratedCfg = LdapUserCredentialsConfig("svc-ldap", "password") private val serviceAccountCfg = ServiceAccountConfig( - "CN=%s,OU=Users,OU=CORP Accounts,DC=corp,DC=dsarena,DC=com", + "CN=%s,OU=Users,OU=Accounts,DC=domain,DC=subdomain,DC=com", Option(integratedCfg), None) private val ldapCfg = ActiveDirectoryLDAPConfig("some.domain.com", "ldaps://some.domain.com:636/","SomeAccount", 1, serviceAccountCfg, None, None) diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala new file mode 100644 index 00000000..5f274f9f --- /dev/null +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala @@ -0,0 +1,42 @@ +/* + * 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.loginsvc.rest.config.auth + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import za.co.absa.loginsvc.rest.config.validation.ConfigValidationException +import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess} + +class KerberosConfigTest extends AnyFlatSpec with Matchers{ + + private val kerberosConfig = new KerberosConfig("krb5", "keytab", "spn", None) + + "kerberosConfig" should "return the correct validation results" in { + kerberosConfig.validate() shouldBe ConfigValidationSuccess + + kerberosConfig.copy(debug = Some(true)).validate() shouldBe ConfigValidationSuccess + + kerberosConfig.copy(krbFileLocation = null).validate() shouldBe + ConfigValidationError(ConfigValidationException("krbFileLocation is empty")) + + kerberosConfig.copy(keytabFileLocation = null).validate() shouldBe + ConfigValidationError(ConfigValidationException("keytabFileLocation is empty")) + + kerberosConfig.copy(spn = null).validate() shouldBe + ConfigValidationError(ConfigValidationException("spn is empty")) + } +} diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfigTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfigTest.scala new file mode 100644 index 00000000..b6538b23 --- /dev/null +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/ServiceAccountConfigTest.scala @@ -0,0 +1,69 @@ +/* + * 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.loginsvc.rest.config.auth + +import org.mockito.Mockito.mock +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import za.co.absa.loginsvc.rest.config.validation.ConfigValidationException +import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess} + +class ServiceAccountConfigTest extends AnyFlatSpec with Matchers { + + private val integratedCfg = LdapUserCredentialsConfig("svc-ldap", "password") + private val cloudCfg = mock(classOf[AwsSecretsLdapUserConfig]) + private val serviceAccountCfg = ServiceAccountConfig( + "CN=%s,OU=Users,OU=Accounts,DC=domain,DC=subdomain,DC=com", + Option(integratedCfg), + None) + + "ServiceAccountConfig" should "have validated and gotten the correct username and password" in { + serviceAccountCfg.username shouldBe "CN=svc-ldap,OU=Users,OU=Accounts,DC=domain,DC=subdomain,DC=com" + serviceAccountCfg.password shouldBe "password" + } + + "ServiceAccountConfig" should "throw a ConfigValidationException for the incorrect configuration" in { + val bothNoneException = intercept[ConfigValidationException] + { + ServiceAccountConfig( + "CN=%s,OU=Users,OU=Accounts,DC=domain,DC=subdomain,DC=com", + None, + None) + } + bothNoneException.getMessage should be ("Neither integratedLdapUserConfig nor awsSecretsLdapUserConfig exists. Exactly one of them should be present.") + + val bothSomeException = intercept[ConfigValidationException] + { + ServiceAccountConfig( + "CN=%s,OU=Users,OU=Accounts,DC=domain,DC=subdomain,DC=com", + Some(integratedCfg), + Some(cloudCfg)) + } + + bothSomeException.getMessage should be ("Both inConfigAccount and awsSecretsLdapUserConfig exist. Please choose only one.") + } + + "LdapUserCredentialsConfig" should "return the correct validation results" in { + integratedCfg.validate() shouldBe ConfigValidationSuccess + + integratedCfg.copy(username = null).validate() shouldBe + ConfigValidationError(ConfigValidationException("username is empty")) + + integratedCfg.copy(password = null).validate() shouldBe + ConfigValidationError(ConfigValidationException("password is empty")) + } +} diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala index d71ca17a..22ac159e 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala @@ -48,6 +48,7 @@ class ConfigProviderTest extends AnyFlatSpec with Matchers { activeDirectoryLDAPConfig.order shouldBe 2 activeDirectoryLDAPConfig.serviceAccount.username shouldBe "CN=svc-ldap,OU=Users,OU=CORP Accounts,DC=corp,DC=dsarena,DC=com" activeDirectoryLDAPConfig.serviceAccount.password shouldBe "password" + activeDirectoryLDAPConfig.enableKerberos shouldBe None activeDirectoryLDAPConfig.attributes shouldBe Some(Map("mail" -> "email", "displayname" -> "displayname")) } diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala index 0e1c83ef..f1be56ab 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala @@ -62,7 +62,7 @@ class TokenControllerTest extends AnyFlatSpec val fakeRefreshJwt: RefreshToken = RefreshToken("ab.fakeJWTToken.cd") val refreshDuration: FiniteDuration = 10.minutes - it should "return tokens generated by mocked JWTService for the authenticated user" in { + it should "return tokens generated by mocked JWTService for the basic-auth authenticated user" in { when(jwtService.generateAccessToken(FakeAuthentication.fakeUser)).thenReturn(fakeAccessJwt) when(jwtService.generateRefreshToken(FakeAuthentication.fakeUser)).thenReturn(fakeRefreshJwt) when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration) @@ -113,6 +113,19 @@ class TokenControllerTest extends AnyFlatSpec )(FakeAuthentication.fakeAnonymousAuthentication) } + it should "return tokens generated by mocked JWTService for the kerberos authenticated user" in { + when(jwtService.generateAccessToken(FakeAuthentication.fakeUser)).thenReturn(fakeAccessJwt) + when(jwtService.generateRefreshToken(FakeAuthentication.fakeUser)).thenReturn(fakeRefreshJwt) + when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration) + + assertExpectedResponseFields( + "/token/generate", + Post() + )( + expectedJsonBody = s"""{"token": "${fakeAccessJwt.token}", "refresh": "${fakeRefreshJwt.token}"}""" + )(Some(FakeAuthentication.fakeUserDetailsAuthentication)) + } + behavior of "refreshToken" it should "return refresh tokens by mocked JWTService for the authenticated user" in { From a4aab77f16fb483f629883328805271d95600608 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 14:47:37 +0200 Subject: [PATCH 31/33] Fix example.application.yaml --- api/src/main/resources/example.application.yaml | 12 ++++++------ .../rest/config/auth/KerberosConfigTest.scala | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/src/main/resources/example.application.yaml b/api/src/main/resources/example.application.yaml index 59fe75aa..757d7cdb 100644 --- a/api/src/main/resources/example.application.yaml +++ b/api/src/main/resources/example.application.yaml @@ -56,7 +56,7 @@ loginsvc: # 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 + order: 0 domain: "some.domain.com" url: "ldaps://some.domain.com:636/" search-filter: "(samaccountname={1})" @@ -73,11 +73,11 @@ loginsvc: #region: "region" #username-field-name: "username" #password-field-name: "password" - enable-kerberos: - krb-file-location: "/etc/krb5.conf" - keytab-file-location: "/etc/keytab" - spn: "HTTP/Host" - debug: true + #enable-kerberos: + #krb-file-location: "/etc/krb5.conf" + #keytab-file-location: "/etc/keytab" + #spn: "HTTP/Host" + #debug: true attributes: # The FieldName is the key used to search ldap and the value is the value used to name the JWT claim. # ldapFieldName: claimFieldName diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala index 5f274f9f..c1e563d9 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/config/auth/KerberosConfigTest.scala @@ -23,7 +23,7 @@ import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{Config class KerberosConfigTest extends AnyFlatSpec with Matchers{ - private val kerberosConfig = new KerberosConfig("krb5", "keytab", "spn", None) + private val kerberosConfig = KerberosConfig("krb5", "keytab", "spn", None) "kerberosConfig" should "return the correct validation results" in { kerberosConfig.validate() shouldBe ConfigValidationSuccess From be8c91b527a54fcca79c6946f9c9178679a6a60e Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Thu, 15 Aug 2024 15:45:57 +0200 Subject: [PATCH 32/33] Remove Unused Code --- .../KerberosSPNEGOAuthenticationProvider.scala | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala index fe0d308e..7510faa9 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/kerberos/KerberosSPNEGOAuthenticationProvider.scala @@ -44,23 +44,12 @@ class KerberosSPNEGOAuthenticationProvider(activeDirectoryLDAPConfig: ActiveDire def spnegoAuthenticationProcessingFilter: SpnegoAuthenticationProcessingFilter = { val filter: SpnegoAuthenticationProcessingFilter = new SpnegoAuthenticationProcessingFilter() - filter.setAuthenticationManager(new ProviderManager(kerberosAuthenticationProvider, kerberosServiceAuthenticationProvider)) + filter.setAuthenticationManager(new ProviderManager(kerberosServiceAuthenticationProvider)) filter.afterPropertiesSet() filter } - def kerberosAuthenticationProvider: KerberosAuthenticationProvider = - { - val provider: KerberosAuthenticationProvider = new KerberosAuthenticationProvider() - val client: SunJaasKerberosClient = new SunJaasKerberosClient() - - client.setDebug(kerberosDebug) - provider.setKerberosClient(client) - provider.setUserDetailsService(kerberosUserDetailsService) - provider - } - - def kerberosServiceAuthenticationProvider: KerberosServiceAuthenticationProvider = + private def kerberosServiceAuthenticationProvider: KerberosServiceAuthenticationProvider = { val provider: KerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider() provider.setTicketValidator(sunJaasKerberosTicketValidator) From 68d8a2bb998ed01ef1b866509ffd95781690ce73 Mon Sep 17 00:00:00 2001 From: Lydon da Rocha Date: Tue, 20 Aug 2024 09:34:32 +0200 Subject: [PATCH 33/33] Amend Test Account Pattern --- api/src/test/resources/application.yaml | 2 +- .../absa/loginsvc/rest/config/provider/ConfigProviderTest.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/test/resources/application.yaml b/api/src/test/resources/application.yaml index 73d0d9b0..d5785638 100644 --- a/api/src/test/resources/application.yaml +++ b/api/src/test/resources/application.yaml @@ -17,7 +17,7 @@ loginsvc: 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" + account-pattern: "CN=svc-ldap,OU=Users,OU=Accounts,DC=domain,DC=subdomain,DC=com" in-config-account: username: "svc-ldap" password: "password" diff --git a/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala b/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala index 22ac159e..d6e2c7b8 100644 --- a/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala +++ b/api/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala @@ -46,7 +46,7 @@ class ConfigProviderTest extends AnyFlatSpec with Matchers { activeDirectoryLDAPConfig.domain shouldBe "some.domain.com" activeDirectoryLDAPConfig.searchFilter shouldBe "(samaccountname={1})" activeDirectoryLDAPConfig.order shouldBe 2 - activeDirectoryLDAPConfig.serviceAccount.username shouldBe "CN=svc-ldap,OU=Users,OU=CORP Accounts,DC=corp,DC=dsarena,DC=com" + activeDirectoryLDAPConfig.serviceAccount.username shouldBe "CN=svc-ldap,OU=Users,OU=Accounts,DC=domain,DC=subdomain,DC=com" activeDirectoryLDAPConfig.serviceAccount.password shouldBe "password" activeDirectoryLDAPConfig.enableKerberos shouldBe None activeDirectoryLDAPConfig.attributes shouldBe Some(Map("mail" -> "email", "displayname" -> "displayname"))