mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-01 04:52:29 +00:00
LDAP: Add mTLS / client certificate authentication support (#509)
* ldap: Add mTLS authentication support to LDAP backend * ldap: Reuse BindService() for initial bind attempt * ldap: Make LdapService.config private Now that we have ldap.BindService(), we don't need to access any members of LdapService.config externally. * ldap: Add TODO note about STARTTLS/SASL authentication * ldap: Add TODO note about mTLS and extra CA certificates * chore: fix typo Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Stavros <steveiliop56@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,8 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
BaseDN: app.config.Ldap.BaseDN,
|
BaseDN: app.config.Ldap.BaseDN,
|
||||||
Insecure: app.config.Ldap.Insecure,
|
Insecure: app.config.Ldap.Insecure,
|
||||||
SearchFilter: app.config.Ldap.SearchFilter,
|
SearchFilter: app.config.Ldap.SearchFilter,
|
||||||
|
AuthCert: app.config.Ldap.AuthCert,
|
||||||
|
AuthKey: app.config.Ldap.AuthKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
err := ldapService.Init()
|
err := ldapService.Init()
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ type LdapConfig struct {
|
|||||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
||||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
||||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
||||||
|
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
||||||
|
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExperimentalConfig struct {
|
type ExperimentalConfig struct {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.ldap.Bind(auth.ldap.Config.BindDN, auth.ldap.Config.BindPassword)
|
err = auth.ldap.BindService(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -19,21 +19,44 @@ type LdapServiceConfig struct {
|
|||||||
BaseDN string
|
BaseDN string
|
||||||
Insecure bool
|
Insecure bool
|
||||||
SearchFilter string
|
SearchFilter string
|
||||||
|
AuthCert string
|
||||||
|
AuthKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LdapService struct {
|
type LdapService struct {
|
||||||
Config LdapServiceConfig // exported so as the auth service can use it
|
config LdapServiceConfig
|
||||||
conn *ldapgo.Conn
|
conn *ldapgo.Conn
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
cert *tls.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLdapService(config LdapServiceConfig) *LdapService {
|
func NewLdapService(config LdapServiceConfig) *LdapService {
|
||||||
return &LdapService{
|
return &LdapService{
|
||||||
Config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Init() error {
|
func (ldap *LdapService) Init() error {
|
||||||
|
// Check whether authentication with client certificate is possible
|
||||||
|
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
|
||||||
|
}
|
||||||
|
ldap.cert = &cert
|
||||||
|
log.Info().Msg("Using LDAP with mTLS authentication")
|
||||||
|
|
||||||
|
// TODO: Add optional extra CA certificates, instead of `InsecureSkipVerify`
|
||||||
|
/*
|
||||||
|
caCert, _ := ioutil.ReadFile(*caFile)
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
...
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
_, err := ldap.connect()
|
_, err := ldap.connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to LDAP server: %w", err)
|
return fmt.Errorf("failed to connect to LDAP server: %w", err)
|
||||||
@@ -60,31 +83,46 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
|
|||||||
ldap.mutex.Lock()
|
ldap.mutex.Lock()
|
||||||
defer ldap.mutex.Unlock()
|
defer ldap.mutex.Unlock()
|
||||||
|
|
||||||
conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
|
var conn *ldapgo.Conn
|
||||||
InsecureSkipVerify: ldap.Config.Insecure,
|
var err error
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
}))
|
// TODO: There's also STARTTLS (or SASL)-based mTLS authentication
|
||||||
|
// scenario, where we first connect to plain text port (389) and
|
||||||
|
// continue with a STARTTLS negotiation:
|
||||||
|
// 1. conn = ldap.DialURL("ldap://ldap.example.com:389")
|
||||||
|
// 2. conn.StartTLS(tlsConfig)
|
||||||
|
// 3. conn.externalBind()
|
||||||
|
if ldap.cert != nil {
|
||||||
|
conn, err = ldapgo.DialURL(ldap.config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
Certificates: []tls.Certificate{*ldap.cert},
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
conn, err = ldapgo.DialURL(ldap.config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
|
||||||
|
InsecureSkipVerify: ldap.config.Insecure,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}))
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.Bind(ldap.Config.BindDN, ldap.Config.BindPassword)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set and return the connection
|
|
||||||
ldap.conn = conn
|
ldap.conn = conn
|
||||||
return conn, nil
|
|
||||||
|
err = ldap.BindService(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ldap.conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Search(username string) (string, error) {
|
func (ldap *LdapService) Search(username string) (string, error) {
|
||||||
// Escape the username to prevent LDAP injection
|
// Escape the username to prevent LDAP injection
|
||||||
escapedUsername := ldapgo.EscapeFilter(username)
|
escapedUsername := ldapgo.EscapeFilter(username)
|
||||||
filter := fmt.Sprintf(ldap.Config.SearchFilter, escapedUsername)
|
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
|
||||||
|
|
||||||
searchRequest := ldapgo.NewSearchRequest(
|
searchRequest := ldapgo.NewSearchRequest(
|
||||||
ldap.Config.BaseDN,
|
ldap.config.BaseDN,
|
||||||
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
||||||
filter,
|
filter,
|
||||||
[]string{"dn"},
|
[]string{"dn"},
|
||||||
@@ -107,6 +145,19 @@ func (ldap *LdapService) Search(username string) (string, error) {
|
|||||||
return userDN, nil
|
return userDN, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ldap *LdapService) BindService(rebind bool) error {
|
||||||
|
// Locks must not be used for initial binding attempt
|
||||||
|
if rebind {
|
||||||
|
ldap.mutex.Lock()
|
||||||
|
defer ldap.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ldap.cert != nil {
|
||||||
|
return ldap.conn.ExternalBind()
|
||||||
|
}
|
||||||
|
return ldap.conn.Bind(ldap.config.BindDN, ldap.config.BindPassword)
|
||||||
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Bind(userDN string, password string) error {
|
func (ldap *LdapService) Bind(userDN string, password string) error {
|
||||||
ldap.mutex.Lock()
|
ldap.mutex.Lock()
|
||||||
defer ldap.mutex.Unlock()
|
defer ldap.mutex.Unlock()
|
||||||
|
|||||||
Reference in New Issue
Block a user