[FEATURE] LDAP StartTLS (#1500)

* add start_tls config option
* add StartTLS method to the LDAP conn factory and the mock
* implemented use of the StartTLS method when the config is set to true
* add mock unit tests
* add docs
* add TLS min version support
* add tests to tls version method
* fix lint issues
* minor adjustments
* remove SSL3.0
* add tls consts
* deprecate old filter placeholders
* remove redundant fake hashing in file auth provider (to delay username enumeration, was replaced by #993
* make suite ActiveDirectory use StartTLS
* misc adjustments to docs
* suggested changes from code review
* deprecation notice conformity
* add mock test for LDAPS plus StartTLS
pull/1506/head
James Elliott 2020-12-03 16:23:52 +11:00 committed by GitHub
parent ba9e89e750
commit 426f5260ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 507 additions and 80 deletions

View File

@ -6,6 +6,14 @@ recommended not to use the 'latest' Docker image tag blindly but pick a version
and read this documentation before upgrading. This is where you will get information about and read this documentation before upgrading. This is where you will get information about
breaking changes and about what you should do to overcome those changes. breaking changes and about what you should do to overcome those changes.
## Breaking in v4.24.0
### Deprecation Notice(s)
* LDAP User Provider Filters (final removal in 4.27.0):
* User Filters containing `{0}` are being deprecated and will generate warnings. Replaced with `{input}`.
* Group Filters containing `{0}` or `{1}` are being deprecated and will generate warnings.
Replaced with `{input}` and `{username}` respectively.
## Breaking in v4.21.0 ## Breaking in v4.21.0
* New LDAP attribute `display_name_attribute` has been introduced, defaults to value: `displayname`. * New LDAP attribute `display_name_attribute` has been introduced, defaults to value: `displayname`.
* New key `displayname` has been introduced into the file based user database. * New key `displayname` has been introduced into the file based user database.

View File

@ -102,16 +102,21 @@ authentication_backend:
# Depending on the option here certain other values in this section have a default value, notably all # Depending on the option here certain other values in this section have a default value, notably all
# of the attribute mappings have a default value that this config overrides, you can read more # of the attribute mappings have a default value that this config overrides, you can read more
# about these default values at https://docs.authelia.com/configuration/authentication/ldap.html#defaults # about these default values at https://docs.authelia.com/configuration/authentication/ldap.html#defaults
implementation: custom implementation: custom
# The url to the ldap server. Scheme can be ldap:// or ldaps:// # The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>].
url: ldap://127.0.0.1 url: ldap://127.0.0.1
# Skip verifying the server certificate (to allow self-signed certificate). # Skip verifying the server certificate (to allow a self-signed certificate).
skip_verify: false skip_verify: false
# The base dn for every entries # Use StartTLS with the LDAP connection.
start_tls: false
# Minimum TLS version for either Secure LDAP or LDAP StartTLS.
minimum_tls_version: TLS1.2
# The base dn for every entries.
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
# The attribute holding the username of the user. This attribute is used to populate # The attribute holding the username of the user. This attribute is used to populate
@ -127,7 +132,7 @@ authentication_backend:
# https://www.ietf.org/rfc/rfc2307.txt. # https://www.ietf.org/rfc/rfc2307.txt.
# username_attribute: uid # username_attribute: uid
# An additional dn to define the scope to all users # An additional dn to define the scope to all users.
additional_users_dn: ou=users additional_users_dn: ou=users
# The users filter used in search queries to find the user profile based on input filled in login form. # The users filter used in search queries to find the user profile based on input filled in login form.
@ -145,7 +150,7 @@ authentication_backend:
# (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)) # (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))
users_filter: (&({username_attribute}={input})(objectClass=person)) users_filter: (&({username_attribute}={input})(objectClass=person))
# An additional dn to define the scope of groups # An additional dn to define the scope of groups.
additional_groups_dn: ou=groups additional_groups_dn: ou=groups
# The groups filter used in search queries to find the groups of the user. # The groups filter used in search queries to find the groups of the user.

View File

@ -50,13 +50,19 @@ authentication_backend:
# about these default values at https://docs.authelia.com/configuration/authentication/ldap.html#defaults # about these default values at https://docs.authelia.com/configuration/authentication/ldap.html#defaults
implementation: custom implementation: custom
# The url to the ldap server. Scheme can be ldap:// or ldaps:// # The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>].
url: ldap://127.0.0.1 url: ldap://127.0.0.1
# Skip verifying the server certificate (to allow self-signed certificate). # Skip verifying the server certificate (to allow a self-signed certificate).
skip_verify: false skip_verify: false
# The base dn for every entries # Use StartTLS with the LDAP connection.
start_tls: false
# Minimum TLS version for either Secure LDAP or LDAP StartTLS.
minimum_tls_version: TLS1.2
# The base dn for every entries.
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
# The attribute holding the username of the user. This attribute is used to populate # The attribute holding the username of the user. This attribute is used to populate
@ -72,7 +78,7 @@ authentication_backend:
# https://www.ietf.org/rfc/rfc2307.txt. # https://www.ietf.org/rfc/rfc2307.txt.
# username_attribute: uid # username_attribute: uid
# An additional dn to define the scope to all users # An additional dn to define the scope to all users.
additional_users_dn: ou=users additional_users_dn: ou=users
# The users filter used in search queries to find the user profile based on input filled in login form. # The users filter used in search queries to find the user profile based on input filled in login form.
@ -90,7 +96,7 @@ authentication_backend:
# (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)) # (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))
users_filter: (&({username_attribute}={input})(objectClass=person)) users_filter: (&({username_attribute}={input})(objectClass=person))
# An additional dn to define the scope of groups # An additional dn to define the scope of groups.
additional_groups_dn: ou=groups additional_groups_dn: ou=groups
# The groups filter used in search queries to find the groups of the user. # The groups filter used in search queries to find the groups of the user.
@ -123,6 +129,27 @@ The user must have an email address in order for Authelia to perform
identity verification when a user attempts to reset their password or identity verification when a user attempts to reset their password or
register a second factor device. register a second factor device.
## TLS Settings
### Skip Verify
The key `skip_verify` disables checking the authenticity of the TLS certificate. You should not disable this, instead
you should add the certificate that signed the certificate of your LDAP server to the machines certificate PKI trust.
For docker you can just add this to the hosts trusted store.
### Start TLS
The key `start_tls` enables use of the LDAP StartTLS process which is not commonly used. You should only configure this
if you know you need it. The initial connection will be over plain text, and Authelia will try to upgrade it with the
LDAP server. LDAPS URL's are slightly more secure.
### Minimum TLS Version
The key `minimum_tls_version` controls the minimum TLS version Authelia will use when opening LDAP connections.
The possible values are `TLS1.3`, `TLS1.2`, `TLS1.1`, `TLS1.0`. Anything other than `TLS1.3` or `TLS1.2`
are very old and deprecated. You should avoid using these and upgrade your LDAP solution instead of decreasing
this value.
## Implementation ## Implementation
There are currently two implementations, `custom` and `activedirectory`. The `activedirectory` implementation There are currently two implementations, `custom` and `activedirectory`. The `activedirectory` implementation

View File

@ -60,3 +60,7 @@ const sha512 = "sha512"
const testPassword = "my;secure*password" const testPassword = "my;secure*password"
const fileAuthenticationMode = 0600 const fileAuthenticationMode = 0600
// OWASP recommends to escape some special characters.
// https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.md
const specialLDAPRunes = ",#+<>;\"="

View File

@ -8,12 +8,10 @@ import (
"sync" "sync"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/simia-tech/crypt"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/logging" "github.com/authelia/authelia/internal/logging"
"github.com/authelia/authelia/internal/utils"
) )
// FileUserProvider is a provider reading details from a file. // FileUserProvider is a provider reading details from a file.
@ -21,9 +19,6 @@ type FileUserProvider struct {
configuration *schema.FileAuthenticationBackendConfiguration configuration *schema.FileAuthenticationBackendConfiguration
database *DatabaseModel database *DatabaseModel
lock *sync.Mutex lock *sync.Mutex
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
fakeHash string
} }
// UserDetailsModel is the model of user details in the file database. // UserDetailsModel is the model of user details in the file database.
@ -62,24 +57,10 @@ func NewFileUserProvider(configuration *schema.FileAuthenticationBackendConfigur
panic(err) panic(err)
} }
var cryptAlgo CryptAlgo = HashingAlgorithmArgon2id
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
// This generates a hash that should be usable to do a fake CheckUserPassword
if configuration.Password.Algorithm == sha512 {
cryptAlgo = HashingAlgorithmSHA512
}
settings := getCryptSettings(utils.RandomString(configuration.Password.SaltLength, HashingPossibleSaltCharacters),
cryptAlgo, configuration.Password.Iterations, configuration.Password.Memory*1024, configuration.Password.Parallelism,
configuration.Password.KeyLength)
data := crypt.Base64Encoding.EncodeToString([]byte(utils.RandomString(configuration.Password.KeyLength, HashingPossibleSaltCharacters)))
fakeHash := fmt.Sprintf("%s$%s", settings, data)
return &FileUserProvider{ return &FileUserProvider{
configuration: configuration, configuration: configuration,
database: database, database: database,
lock: &sync.Mutex{}, lock: &sync.Mutex{},
fakeHash: fakeHash,
} }
} }
@ -174,9 +155,6 @@ func (p *FileUserProvider) CheckUserPassword(username string, password string) (
return ok, nil return ok, nil
} }
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
_, _ = CheckPassword(password, p.fakeHash)
return false, ErrUserNotFound return false, ErrUserNotFound
} }

View File

@ -15,6 +15,7 @@ type LDAPConnection interface {
Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error)
Modify(modifyRequest *ldap.ModifyRequest) error Modify(modifyRequest *ldap.ModifyRequest) error
StartTLS(config *tls.Config) error
} }
// LDAPConnectionImpl the production implementation of an ldap connection. // LDAPConnectionImpl the production implementation of an ldap connection.
@ -47,6 +48,11 @@ func (lc *LDAPConnectionImpl) Modify(modifyRequest *ldap.ModifyRequest) error {
return lc.conn.Modify(modifyRequest) return lc.conn.Modify(modifyRequest)
} }
// StartTLS requests the LDAP server upgrades to TLS encryption.
func (lc *LDAPConnectionImpl) StartTLS(config *tls.Config) error {
return lc.conn.StartTLS(config)
}
// ********************* FACTORY ***********************. // ********************* FACTORY ***********************.
// LDAPConnectionFactory an interface of factory of ldap connections. // LDAPConnectionFactory an interface of factory of ldap connections.

View File

@ -1,15 +1,12 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: internal/authentication/ldap_connection_factory.go // Source: ldap_connection_factory.go
// Package authentication is a generated GoMock package.
package authentication package authentication
import ( import (
tls "crypto/tls" tls "crypto/tls"
reflect "reflect" ldap "github.com/go-ldap/ldap/v3"
ldap_v3 "github.com/go-ldap/ldap/v3"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
reflect "reflect"
) )
// MockLDAPConnection is a mock of LDAPConnection interface // MockLDAPConnection is a mock of LDAPConnection interface
@ -62,10 +59,10 @@ func (mr *MockLDAPConnectionMockRecorder) Close() *gomock.Call {
} }
// Search mocks base method // Search mocks base method
func (m *MockLDAPConnection) Search(searchRequest *ldap_v3.SearchRequest) (*ldap_v3.SearchResult, error) { func (m *MockLDAPConnection) Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", searchRequest) ret := m.ctrl.Call(m, "Search", searchRequest)
ret0, _ := ret[0].(*ldap_v3.SearchResult) ret0, _ := ret[0].(*ldap.SearchResult)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@ -77,7 +74,7 @@ func (mr *MockLDAPConnectionMockRecorder) Search(searchRequest interface{}) *gom
} }
// Modify mocks base method // Modify mocks base method
func (m *MockLDAPConnection) Modify(modifyRequest *ldap_v3.ModifyRequest) error { func (m *MockLDAPConnection) Modify(modifyRequest *ldap.ModifyRequest) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Modify", modifyRequest) ret := m.ctrl.Call(m, "Modify", modifyRequest)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -90,6 +87,20 @@ func (mr *MockLDAPConnectionMockRecorder) Modify(modifyRequest interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockLDAPConnection)(nil).Modify), modifyRequest) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockLDAPConnection)(nil).Modify), modifyRequest)
} }
// StartTLS mocks base method
func (m *MockLDAPConnection) StartTLS(config *tls.Config) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartTLS", config)
ret0, _ := ret[0].(error)
return ret0
}
// StartTLS indicates an expected call of StartTLS
func (mr *MockLDAPConnectionMockRecorder) StartTLS(config interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTLS", reflect.TypeOf((*MockLDAPConnection)(nil).StartTLS), config)
}
// MockLDAPConnectionFactory is a mock of LDAPConnectionFactory interface // MockLDAPConnectionFactory is a mock of LDAPConnectionFactory interface
type MockLDAPConnectionFactory struct { type MockLDAPConnectionFactory struct {
ctrl *gomock.Controller ctrl *gomock.Controller

View File

@ -11,47 +11,76 @@ import (
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/logging" "github.com/authelia/authelia/internal/logging"
"github.com/authelia/authelia/internal/utils"
) )
// LDAPUserProvider is a provider using a LDAP or AD as a user database. // LDAPUserProvider is a provider using a LDAP or AD as a user database.
type LDAPUserProvider struct { type LDAPUserProvider struct {
configuration schema.LDAPAuthenticationBackendConfiguration configuration schema.LDAPAuthenticationBackendConfiguration
tlsConfig *tls.Config
connectionFactory LDAPConnectionFactory connectionFactory LDAPConnectionFactory
} }
// NewLDAPUserProvider creates a new instance of LDAPUserProvider. // NewLDAPUserProvider creates a new instance of LDAPUserProvider.
func NewLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration) *LDAPUserProvider { func NewLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration) *LDAPUserProvider {
minimumTLSVersion, _ := utils.TLSStringToTLSConfigVersion(configuration.MinimumTLSVersion)
// TODO: RELEASE-4.27.0 Deprecated Completely in this release.
logger := logging.Logger()
if strings.Contains(configuration.UsersFilter, "{0}") {
logger.Warnf("DEPRECATION NOTICE: LDAP Users Filter will no longer support replacing `{0}` in 4.27.0. Please use `{input}` instead.")
configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{0}", "{input}")
}
if strings.Contains(configuration.GroupsFilter, "{0}") {
logger.Warnf("DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{0}` in 4.27.0. Please use `{input}` instead.")
configuration.GroupsFilter = strings.ReplaceAll(configuration.GroupsFilter, "{0}", "{input}")
}
if strings.Contains(configuration.GroupsFilter, "{1}") {
logger.Warnf("DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{1}` in 4.27.0. Please use `{username}` instead.")
configuration.GroupsFilter = strings.ReplaceAll(configuration.GroupsFilter, "{1}", "{username}")
}
// TODO: RELEASE-4.27.0 Deprecated Completely in this release.
configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{username_attribute}", configuration.UsernameAttribute)
configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{mail_attribute}", configuration.MailAttribute)
configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{display_name_attribute}", configuration.DisplayNameAttribute)
return &LDAPUserProvider{ return &LDAPUserProvider{
configuration: configuration, configuration: configuration,
tlsConfig: &tls.Config{InsecureSkipVerify: configuration.SkipVerify, MinVersion: minimumTLSVersion}, //nolint:gosec // Disabling InsecureSkipVerify is an informed choice by users.
connectionFactory: NewLDAPConnectionFactoryImpl(), connectionFactory: NewLDAPConnectionFactoryImpl(),
} }
} }
// NewLDAPUserProviderWithFactory creates a new instance of LDAPUserProvider with existing factory. // NewLDAPUserProviderWithFactory creates a new instance of LDAPUserProvider with existing factory.
func NewLDAPUserProviderWithFactory(configuration schema.LDAPAuthenticationBackendConfiguration, func NewLDAPUserProviderWithFactory(configuration schema.LDAPAuthenticationBackendConfiguration, connectionFactory LDAPConnectionFactory) *LDAPUserProvider {
connectionFactory LDAPConnectionFactory) *LDAPUserProvider { provider := NewLDAPUserProvider(configuration)
return &LDAPUserProvider{ provider.connectionFactory = connectionFactory
configuration: configuration,
connectionFactory: connectionFactory, return provider
}
} }
func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnection, error) { func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnection, error) {
var newConnection LDAPConnection var newConnection LDAPConnection
url, err := url.Parse(p.configuration.URL) ldapURL, err := url.Parse(p.configuration.URL)
if err != nil { if err != nil {
return nil, fmt.Errorf("Unable to parse URL to LDAP: %s", url) return nil, fmt.Errorf("Unable to parse URL to LDAP: %s", ldapURL)
} }
if url.Scheme == "ldaps" { if ldapURL.Scheme == "ldaps" {
logging.Logger().Trace("LDAP client starts a TLS session") logging.Logger().Trace("LDAP client starts a TLS session")
conn, err := p.connectionFactory.DialTLS("tcp", url.Host, &tls.Config{ conn, err := p.connectionFactory.DialTLS("tcp", ldapURL.Host, p.tlsConfig)
InsecureSkipVerify: p.configuration.SkipVerify, //nolint:gosec // This is a configurable option, is desirable in some situations and is off by default.
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -59,13 +88,19 @@ func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnecti
newConnection = conn newConnection = conn
} else { } else {
logging.Logger().Trace("LDAP client starts a session over raw TCP") logging.Logger().Trace("LDAP client starts a session over raw TCP")
conn, err := p.connectionFactory.Dial("tcp", url.Host) conn, err := p.connectionFactory.Dial("tcp", ldapURL.Host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
newConnection = conn newConnection = conn
} }
if p.configuration.StartTLS {
if err := newConnection.StartTLS(p.tlsConfig); err != nil {
return nil, err
}
}
if err := newConnection.Bind(userDN, password); err != nil { if err := newConnection.Bind(userDN, password); err != nil {
return nil, err return nil, err
} }
@ -95,10 +130,6 @@ func (p *LDAPUserProvider) CheckUserPassword(inputUsername string, password stri
return true, nil return true, nil
} }
// OWASP recommends to escape some special characters.
// https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.md
const specialLDAPRunes = ",#+<>;\"="
func (p *LDAPUserProvider) ldapEscape(inputUsername string) string { func (p *LDAPUserProvider) ldapEscape(inputUsername string) string {
inputUsername = ldap.EscapeFilter(inputUsername) inputUsername = ldap.EscapeFilter(inputUsername)
for _, c := range specialLDAPRunes { for _, c := range specialLDAPRunes {
@ -118,18 +149,9 @@ type ldapUserProfile struct {
func (p *LDAPUserProvider) resolveUsersFilter(userFilter string, inputUsername string) string { func (p *LDAPUserProvider) resolveUsersFilter(userFilter string, inputUsername string) string {
inputUsername = p.ldapEscape(inputUsername) inputUsername = p.ldapEscape(inputUsername)
// We temporarily keep placeholder {0} for backward compatibility. // The {input} placeholder is replaced by the users username input.
userFilter = strings.ReplaceAll(userFilter, "{0}", inputUsername)
// The {username} placeholder is equivalent to {0}, it's the new way, a named placeholder.
userFilter = strings.ReplaceAll(userFilter, "{input}", inputUsername) userFilter = strings.ReplaceAll(userFilter, "{input}", inputUsername)
// {username_attribute} and {mail_attribute} are replaced by the content of the attribute defined
// in configuration.
userFilter = strings.ReplaceAll(userFilter, "{username_attribute}", p.configuration.UsernameAttribute)
userFilter = strings.ReplaceAll(userFilter, "{mail_attribute}", p.configuration.MailAttribute)
userFilter = strings.ReplaceAll(userFilter, "{display_name_attribute}", p.configuration.DisplayNameAttribute)
return userFilter return userFilter
} }
@ -199,13 +221,10 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str
func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ldapUserProfile) (string, error) { //nolint:unparam func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ldapUserProfile) (string, error) { //nolint:unparam
inputUsername = p.ldapEscape(inputUsername) inputUsername = p.ldapEscape(inputUsername)
// We temporarily keep placeholder {0} for backward compatibility. // The {input} placeholder is replaced by the users username input.
groupFilter := strings.ReplaceAll(p.configuration.GroupsFilter, "{0}", inputUsername) groupFilter := strings.ReplaceAll(p.configuration.GroupsFilter, "{input}", inputUsername)
groupFilter = strings.ReplaceAll(groupFilter, "{input}", inputUsername)
if profile != nil { if profile != nil {
// We temporarily keep placeholder {1} for backward compatibility.
groupFilter = strings.ReplaceAll(groupFilter, "{1}", ldap.EscapeFilter(profile.Username))
groupFilter = strings.ReplaceAll(groupFilter, "{username}", ldap.EscapeFilter(profile.Username)) groupFilter = strings.ReplaceAll(groupFilter, "{username}", ldap.EscapeFilter(profile.Username))
groupFilter = strings.ReplaceAll(groupFilter, "{dn}", ldap.EscapeFilter(profile.DN)) groupFilter = strings.ReplaceAll(groupFilter, "{dn}", ldap.EscapeFilter(profile.DN))
} }

View File

@ -1,6 +1,7 @@
package authentication package authentication
import ( import (
"errors"
"testing" "testing"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
@ -151,7 +152,9 @@ func TestShouldEscapeUserInput(t *testing.T) {
Search(NewSearchRequestMatcher("(|(uid=john\\=abc)(mail=john\\=abc))")). Search(NewSearchRequestMatcher("(|(uid=john\\=abc)(mail=john\\=abc))")).
Return(&ldap.SearchResult{}, nil) Return(&ldap.SearchResult{}, nil)
ldapClient.getUserProfile(mockConn, "john=abc") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. _, err := ldapClient.getUserProfile(mockConn, "john=abc")
require.Error(t, err)
assert.EqualError(t, err, "user not found")
} }
func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) { func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
@ -177,7 +180,9 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")). Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")).
Return(&ldap.SearchResult{}, nil) Return(&ldap.SearchResult{}, nil)
ldapClient.getUserProfile(mockConn, "john") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. _, err := ldapClient.getUserProfile(mockConn, "john")
require.Error(t, err)
assert.EqualError(t, err, "user not found")
} }
func createSearchResultWithAttributes(attributes ...*ldap.EntryAttribute) *ldap.SearchResult { func createSearchResultWithAttributes(attributes ...*ldap.EntryAttribute) *ldap.SearchResult {
@ -386,3 +391,181 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) {
assert.Equal(t, details.DisplayName, "John Doe") assert.Equal(t, details.DisplayName, "John Doe")
assert.Equal(t, details.Username, "John") assert.Equal(t, details.Username, "John")
} }
func TestShouldCallStartTLSWhenEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
UsernameAttribute: "uid",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
StartTLS: true,
}, mockFactory)
mockFactory.EXPECT().
Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
mockConn.EXPECT().
StartTLS(ldapClient.tlsConfig)
mockConn.EXPECT().
Close()
searchGroups := mockConn.EXPECT().
Search(gomock.Any()).
Return(createSearchResultWithAttributes(), nil)
searchProfile := mockConn.EXPECT().
Search(gomock.Any()).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=test,dc=example,dc=com",
Attributes: []*ldap.EntryAttribute{
{
Name: "displayname",
Values: []string{"John Doe"},
},
{
Name: "mail",
Values: []string{"test@example.com"},
},
{
Name: "uid",
Values: []string{"john"},
},
},
},
},
}, nil)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)
assert.ElementsMatch(t, details.Groups, []string{})
assert.ElementsMatch(t, details.Emails, []string{"test@example.com"})
assert.Equal(t, details.DisplayName, "John Doe")
assert.Equal(t, details.Username, "john")
}
func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
UsernameAttribute: "uid",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
StartTLS: true,
SkipVerify: true,
}, mockFactory)
mockFactory.EXPECT().
Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
mockConn.EXPECT().
StartTLS(ldapClient.tlsConfig)
mockConn.EXPECT().
Close()
searchGroups := mockConn.EXPECT().
Search(gomock.Any()).
Return(createSearchResultWithAttributes(), nil)
searchProfile := mockConn.EXPECT().
Search(gomock.Any()).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=test,dc=example,dc=com",
Attributes: []*ldap.EntryAttribute{
{
Name: "displayname",
Values: []string{"John Doe"},
},
{
Name: "mail",
Values: []string{"test@example.com"},
},
{
Name: "uid",
Values: []string{"john"},
},
},
},
},
}, nil)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)
assert.ElementsMatch(t, details.Groups, []string{})
assert.ElementsMatch(t, details.Emails, []string{"test@example.com"})
assert.Equal(t, details.DisplayName, "John Doe")
assert.Equal(t, details.Username, "john")
}
func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
UsernameAttribute: "uid",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
StartTLS: true,
SkipVerify: true,
}, mockFactory)
mockFactory.EXPECT().
DialTLS(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389"), gomock.Any()).
Return(mockConn, nil)
mockConn.EXPECT().
StartTLS(ldapClient.tlsConfig).
Return(errors.New("LDAP Result Code 200 \"Network Error\": ldap: already encrypted"))
_, err := ldapClient.GetDetails("john")
assert.EqualError(t, err, "LDAP Result Code 200 \"Network Error\": ldap: already encrypted")
}

View File

@ -5,6 +5,8 @@ type LDAPAuthenticationBackendConfiguration struct {
Implementation string `mapstructure:"implementation"` Implementation string `mapstructure:"implementation"`
URL string `mapstructure:"url"` URL string `mapstructure:"url"`
SkipVerify bool `mapstructure:"skip_verify"` SkipVerify bool `mapstructure:"skip_verify"`
StartTLS bool `mapstructure:"start_tls"`
MinimumTLSVersion string `mapstructure:"minimum_tls_version"`
BaseDN string `mapstructure:"base_dn"` BaseDN string `mapstructure:"base_dn"`
AdditionalUsersDN string `mapstructure:"additional_users_dn"` AdditionalUsersDN string `mapstructure:"additional_users_dn"`
UsersFilter string `mapstructure:"users_filter"` UsersFilter string `mapstructure:"users_filter"`
@ -76,6 +78,7 @@ var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendCon
MailAttribute: "mail", MailAttribute: "mail",
DisplayNameAttribute: "displayname", DisplayNameAttribute: "displayname",
GroupNameAttribute: "cn", GroupNameAttribute: "cn",
MinimumTLSVersion: "TLS1.2",
} }
// DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration represents the default LDAP config for the MSAD Implementation. // DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration represents the default LDAP config for the MSAD Implementation.

View File

@ -104,6 +104,12 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
configuration.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation configuration.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation
} }
if configuration.MinimumTLSVersion == "" {
configuration.MinimumTLSVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.MinimumTLSVersion
} else if _, err := utils.TLSStringToTLSConfigVersion(configuration.MinimumTLSVersion); err != nil {
validator.Push(fmt.Errorf("error occurred validating the LDAP minimum_tls_version key with value %s: %v", configuration.MinimumTLSVersion, err))
}
switch configuration.Implementation { switch configuration.Implementation {
case schema.LDAPImplementationCustom: case schema.LDAPImplementationCustom:
setDefaultImplementationCustomLdapAuthenticationBackend(configuration) setDefaultImplementationCustomLdapAuthenticationBackend(configuration)

View File

@ -312,6 +312,95 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {
assert.Equal(suite.T(), "ldaps://127.0.0.1:636", validateLdapURL("ldaps://127.0.0.1", suite.validator)) assert.Equal(suite.T(), "ldaps://127.0.0.1:636", validateLdapURL("ldaps://127.0.0.1", suite.validator))
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldDefaultTLS12() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), schema.DefaultLDAPAuthenticationBackendConfiguration.MinimumTLSVersion, suite.configuration.Ldap.MinimumTLSVersion)
}
func (suite *LdapAuthenticationBackendSuite) TestShouldNotAllowInvalidTLSValue() {
suite.configuration.Ldap.MinimumTLSVersion = "SSL2.0"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "error occurred validating the LDAP minimum_tls_version key with value SSL2.0: supplied TLS version isn't supported")
}
func TestLdapAuthenticationBackend(t *testing.T) { func TestLdapAuthenticationBackend(t *testing.T) {
suite.Run(t, new(LdapAuthenticationBackendSuite)) suite.Run(t, new(LdapAuthenticationBackendSuite))
} }
type ActiveDirectoryAuthenticationBackendSuite struct {
suite.Suite
configuration schema.AuthenticationBackendConfiguration
validator *schema.StructValidator
}
func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.configuration = schema.AuthenticationBackendConfiguration{}
suite.configuration.Ldap = &schema.LDAPAuthenticationBackendConfiguration{}
suite.configuration.Ldap.Implementation = schema.LDAPImplementationActiveDirectory
suite.configuration.Ldap.URL = "ldap://ldap"
suite.configuration.Ldap.User = "user"
suite.configuration.Ldap.Password = "password"
suite.configuration.Ldap.BaseDN = "base_dn"
}
func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirectoryDefaults() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(),
suite.configuration.Ldap.UsersFilter,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter)
assert.Equal(suite.T(),
suite.configuration.Ldap.UsernameAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute)
assert.Equal(suite.T(),
suite.configuration.Ldap.DisplayNameAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute)
assert.Equal(suite.T(),
suite.configuration.Ldap.MailAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute)
assert.Equal(suite.T(),
suite.configuration.Ldap.GroupsFilter,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter)
assert.Equal(suite.T(),
suite.configuration.Ldap.GroupNameAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute)
}
func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() {
suite.configuration.Ldap.UsersFilter = "(&({username_attribute}={input})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))"
suite.configuration.Ldap.UsernameAttribute = "cn"
suite.configuration.Ldap.MailAttribute = "userPrincipalName"
suite.configuration.Ldap.DisplayNameAttribute = "name"
suite.configuration.Ldap.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))"
suite.configuration.Ldap.GroupNameAttribute = "distinguishedName"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.NotEqual(suite.T(),
suite.configuration.Ldap.UsersFilter,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter)
assert.NotEqual(suite.T(),
suite.configuration.Ldap.UsernameAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute)
assert.NotEqual(suite.T(),
suite.configuration.Ldap.DisplayNameAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute)
assert.NotEqual(suite.T(),
suite.configuration.Ldap.MailAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute)
assert.NotEqual(suite.T(),
suite.configuration.Ldap.GroupsFilter,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter)
assert.NotEqual(suite.T(),
suite.configuration.Ldap.GroupNameAttribute,
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute)
}
func TestActiveDirectoryAuthenticationBackend(t *testing.T) {
suite.Run(t, new(ActiveDirectoryAuthenticationBackendSuite))
}

View File

@ -94,6 +94,8 @@ var validKeys = []string{
"authentication_backend.ldap.implementation", "authentication_backend.ldap.implementation",
"authentication_backend.ldap.url", "authentication_backend.ldap.url",
"authentication_backend.ldap.skip_verify", "authentication_backend.ldap.skip_verify",
"authentication_backend.ldap.start_tls",
"authentication_backend.ldap.minimum_tls_version",
"authentication_backend.ldap.base_dn", "authentication_backend.ldap.base_dn",
"authentication_backend.ldap.username_attribute", "authentication_backend.ldap.username_attribute",
"authentication_backend.ldap.additional_users_dn", "authentication_backend.ldap.additional_users_dn",

View File

@ -15,8 +15,9 @@ jwt_secret: very_important_secret
authentication_backend: authentication_backend:
ldap: ldap:
implementation: activedirectory implementation: activedirectory
url: ldaps://sambaldap url: ldap://sambaldap
skip_verify: true skip_verify: true
start_tls: true
base_dn: DC=example,DC=com base_dn: DC=example,DC=com
username_attribute: sAMAccountName username_attribute: sAMAccountName
additional_users_dn: OU=Users additional_users_dn: OU=Users

View File

@ -32,3 +32,18 @@ const testStringInput = "abcdefghijkl"
// AlphaNumericCharacters are literally just valid alphanumeric chars. // AlphaNumericCharacters are literally just valid alphanumeric chars.
var AlphaNumericCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") var AlphaNumericCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied.
var ErrTLSVersionNotSupported = errors.New("supplied TLS version isn't supported")
// TLS13 is the textual representation of TLS 1.3.
const TLS13 = "1.3"
// TLS12 is the textual representation of TLS 1.2.
const TLS12 = "1.2"
// TLS11 is the textual representation of TLS 1.1.
const TLS11 = "1.1"
// TLS10 is the textual representation of TLS 1.0.
const TLS10 = "1.0"

View File

@ -1,7 +1,9 @@
package utils package utils
import ( import (
"crypto/tls"
"math/rand" "math/rand"
"strings"
"time" "time"
"unicode" "unicode"
) )
@ -91,3 +93,19 @@ func RandomString(n int, characters []rune) (randomString string) {
return string(b) return string(b)
} }
// TLSStringToTLSConfigVersion returns a go crypto/tls version for a tls.Config based on string input.
func TLSStringToTLSConfigVersion(input string) (version uint16, err error) {
switch strings.ToUpper(input) {
case "TLS1.3", TLS13:
return tls.VersionTLS13, nil
case "TLS1.2", TLS12:
return tls.VersionTLS12, nil
case "TLS1.1", TLS11:
return tls.VersionTLS11, nil
case "TLS1.0", TLS10:
return tls.VersionTLS10, nil
}
return 0, ErrTLSVersionNotSupported
}

View File

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"crypto/tls"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -68,3 +69,54 @@ func TestShouldNotFindSliceDifferences(t *testing.T) {
diff := IsStringSlicesDifferent(a, b) diff := IsStringSlicesDifferent(a, b)
assert.False(t, diff) assert.False(t, diff)
} }
func TestShouldReturnCorrectTLSVersions(t *testing.T) {
tls13 := uint16(tls.VersionTLS13)
tls12 := uint16(tls.VersionTLS12)
tls11 := uint16(tls.VersionTLS11)
tls10 := uint16(tls.VersionTLS10)
version, err := TLSStringToTLSConfigVersion(TLS13)
assert.Equal(t, tls13, version)
assert.NoError(t, err)
version, err = TLSStringToTLSConfigVersion("TLS" + TLS13)
assert.Equal(t, tls13, version)
assert.NoError(t, err)
version, err = TLSStringToTLSConfigVersion(TLS12)
assert.Equal(t, tls12, version)
assert.NoError(t, err)
version, err = TLSStringToTLSConfigVersion("TLS" + TLS12)
assert.Equal(t, tls12, version)
assert.NoError(t, err)
version, err = TLSStringToTLSConfigVersion(TLS11)
assert.Equal(t, tls11, version)
assert.NoError(t, err)
version, err = TLSStringToTLSConfigVersion("TLS" + TLS11)
assert.Equal(t, tls11, version)
assert.NoError(t, err)
version, err = TLSStringToTLSConfigVersion(TLS10)
assert.Equal(t, tls10, version)
assert.NoError(t, err)
version, err = TLSStringToTLSConfigVersion("TLS" + TLS10)
assert.Equal(t, tls10, version)
assert.NoError(t, err)
}
func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) {
version, err := TLSStringToTLSConfigVersion("TLS1.4")
assert.Error(t, err)
assert.Equal(t, uint16(0), version)
assert.EqualError(t, err, "supplied TLS version isn't supported")
version, err = TLSStringToTLSConfigVersion("SSL3.0")
assert.Error(t, err)
assert.Equal(t, uint16(0), version)
assert.EqualError(t, err, "supplied TLS version isn't supported")
}