Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

100 using spnego similarly like in enceladus to negotiate the auth #103

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
88246fa
Initial Commit
TheLydonKing Apr 24, 2024
664a37c
Added Kerberos to Config
TheLydonKing Apr 25, 2024
7c00778
Added config to required class
TheLydonKing Apr 26, 2024
69cab58
Implement Kerberos Search
TheLydonKing Jun 10, 2024
0f11ae1
Added Test LdapProvider just to test kerberosLdapSearch
TheLydonKing Jun 11, 2024
0b2621e
Trying to use system property sets
TheLydonKing Jun 12, 2024
892d391
Simplify code for better debugging
TheLydonKing Jul 3, 2024
9fb0e2b
Split up config among new files for better readability
TheLydonKing Jul 22, 2024
1693d3b
Added Missing License
TheLydonKing Jul 22, 2024
3ef9aad
Fixed Tests
TheLydonKing Jul 22, 2024
864f79d
Fixed Tests part 2
TheLydonKing Jul 22, 2024
fb64065
Added Changes to Dockerfile to include krb5 and keytab
TheLydonKing Jul 31, 2024
d26bf94
Revert Dockerfile change
TheLydonKing Aug 2, 2024
98731ac
Add Ldap required functions for future testing once Dummy Service is …
TheLydonKing Aug 5, 2024
36c75f4
Fix krb5 property
TheLydonKing Aug 12, 2024
0cf9586
Set AfterProperties Set for Properties to ensure KRB5 is read
TheLydonKing Aug 13, 2024
9e35e25
Remove Dummy User Service and enable LdapUserDetailsService
TheLydonKing Aug 14, 2024
3866b40
Minor Fix
TheLydonKing Aug 14, 2024
6d6fb83
Test Just Authentication Provider Only
TheLydonKing Aug 14, 2024
a487173
Test Just Service Authentication Provider Only
TheLydonKing Aug 14, 2024
9e864c3
Minor Change
TheLydonKing Aug 14, 2024
7d8f197
Add new UserDetailsType
TheLydonKing Aug 14, 2024
f1e665f
Slight Cleanup of unused code
TheLydonKing Aug 14, 2024
e6dca19
Disable Cache
TheLydonKing Aug 14, 2024
85dac05
Disable Cache attempt 2
TheLydonKing Aug 15, 2024
8249377
Add UserProvider
TheLydonKing Aug 15, 2024
6656966
Clean up code
TheLydonKing Aug 15, 2024
821ba5a
Add Missing License
TheLydonKing Aug 15, 2024
89df77b
Fix Logging
TheLydonKing Aug 15, 2024
fdb43a3
Add Testing
TheLydonKing Aug 15, 2024
a4aab77
Fix example.application.yaml
TheLydonKing Aug 15, 2024
be8c91b
Remove Unused Code
TheLydonKing Aug 15, 2024
68d8a2b
Amend Test Account Pattern
TheLydonKing Aug 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/src/main/resources/example.application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +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, 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.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}

@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 = {
Expand All @@ -49,9 +57,21 @@ class SecurityConfig {
.and()
.httpBasic()

//TODO: Neaten up checking for Config
if(KerberosConfig != null)
{
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(provider, kerberos.kerberosServiceAuthenticationProvider())),
classOf[BasicAuthenticationFilter])
}
}

http.build()
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am surprised that it is necessary to force the the Kerberos...AuthProvider this way. Isn't this just another AuthenticationProvider just like ConfigUsersAuthenticationProvider and ActiveDirectoryLDAPAuthenticationProvider?

Perhaps it could be provided the same way as those are in AuthManagerConfig.authManager:47?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So I'm forcing the Authmanager here so that I can try and debug any issues with the kerberos auth specifically. Just trying to make it easier to find the above issue that is being run into.

}



}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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 {
dk1844 marked this conversation as resolved.
Show resolved Hide resolved

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])
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
dk1844 marked this conversation as resolved.
Show resolved Hide resolved
super.searchForUser(user)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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("[email protected]")
ticketValidator.setKeyTabLocation(new FileSystemResource(kerberos.keytabFileLocation))
ticketValidator.setDebug(kerberosDebug)
ticketValidator.afterPropertiesSet()
ticketValidator
}

private def loginConfig(): SunJaasKrb5LoginConfig = {
val loginConfig = new SunJaasKrb5LoginConfig()
loginConfig.setServicePrincipal("[email protected]")
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
}
}
dk1844 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}

Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
5 changes: 5 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,6 +87,8 @@ object Dependencies {
springBootSecurity,

springSecurityLDAP,
springSecurityKerberosClient,
springSecurityKerberosWeb,

jjwtApi,
jjwtImpl,
Expand Down
Loading