[FEATURE][BREAKING] Allow users to sign in with email. (#792)

* [FEATURE][BREAKING] Allow users to sign in with email.

The users_filter purpose evolved with the introduction of username_attribute
but is reverted here to allow the most flexibility. users_filter is now the
actual filter used for searching the user and not a sub-filter based on the
username_attribute anymore.

* {input} placeholder has been introduced to later deprecate {0} which has been
kept for backward compatibility.
* {username_attribute} and {mail_attribute} are new placeholders used to back
reference other configuration options.

Fix #735

* [MISC] Introduce new placeholders for groups_filter too.

* [MISC] Update BREAKING.md to mention the change regarding users_filter.

* [MISC] Fix unit and integration tests.

* Log an error message in console when U2F is not supported.

* Apply suggestions from code review

* Update BREAKING.md

Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
pull/798/head^2
Clément Michaud 2020-03-31 00:36:04 +02:00 committed by GitHub
parent 95f6c1a893
commit 7a3e782dc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 264 additions and 107 deletions

View File

@ -6,6 +6,12 @@ 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
breaking changes and about what you should do to overcome those changes.
## Breaking in v4.10.0
* Revert of `users_filter` purpose. This option now represents the complete search filter again, meaning
there is no more automatic filter computation based on username. This gives the most flexibility.
For instance, this allows administrators to choose whether they want the users to be able to sign in with
username or email.
## Breaking in v4.7.0
* `logs_level` configuration key has been renamed to `log_level`.
* `users_filter` was a search pattern for a given user with the `{0}` matcher replaced with the

View File

@ -82,33 +82,42 @@ authentication_backend:
# The base dn for every entries
base_dn: dc=example,dc=com
# The attribute holding the username of the user (introduced to handle
# case insensitive search queries: #561).
# Microsoft Active Directory usually uses 'sAMAccountName'
# OpenLDAP usually uses 'uid'
# The attribute holding the username of the user. This attribute is used to populate
# the username in the session information. It was introduced due to #561 to handle case
# insensitive search queries.
# For you information, Microsoft Active Directory usually uses 'sAMAccountName' and OpenLDAP
# usually uses 'uid'
username_attribute: uid
# An additional dn to define the scope to all users
additional_users_dn: ou=users
# This attribute is optional. The user filter used in the LDAP search queries
# is a combination of this filter and the username attribute.
# This filter is used to reduce the scope of users targeted by the LDAP search query.
# For instance, if the username attribute is set to 'uid', the computed filter is
# (&(uid=<username>)(objectClass=person))
# The users filter used in search queries to find the user profile based on input filled in login form.
# Various placeholders are available to represent the user input and back reference other options of the configuration:
# - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username_attribute} is a placeholder replaced by what is configured in `username_attribute`.
# - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`.
# - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it.
#
# Recommended settings are as follows:
# Microsoft Active Directory '(&(objectCategory=person)(objectClass=user))'
# OpenLDAP '(objectClass=person)' or '(objectClass=inetOrgPerson)'
users_filter: (objectClass=person)
# - Microsoft Active Directory: (&({username_attribute}={input})(objectCategory=person)(objectClass=user))
# - OpenLDAP: (&({username_attribute}={input})(objectClass=person))' or '(&({username_attribute}={input})(objectClass=inetOrgPerson))
#
# To allow sign in both with username and email, one can use a filter like
# (&(|({username_attribute}={input})({mail_attribute={input}))(objectClass=person))
users_filter: (&({username_attribute}={input})(objectClass=person))
# An additional dn to define the scope of groups
additional_groups_dn: ou=groups
# The groups filter used for retrieving groups of a given user.
# {0} is a matcher replaced by username (as provided in login portal).
# {1} is a matcher replaced by username (as stored in LDAP).
# {dn} is a matcher replaced by user DN.
# 'member={dn}' by default.
# The groups filter used in search queries to find the groups of the user.
# - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username} is a placeholder replace by the username stored in LDAP (based on `username_attribute`).
# - {dn} is a matcher replaced by the user distinguished name, aka, user DN.
# - {username_attribute} is a placeholder replaced by what is configured in `username_attribute`.
# - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`.
# - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it.
# - DON'T USE - {1} is an alias for {username} supported for backward compatibility but it will be deprecated in later version, so please don't use it.
groups_filter: (&(member={dn})(objectclass=groupOfNames))
# The attribute holding the name of the group

View File

@ -26,33 +26,42 @@ authentication_backend:
# The base dn for every entries
base_dn: dc=example,dc=com
# The attribute holding the username of the user (introduced to handle
# case insensitive search queries: #561).
# Microsoft Active Directory usually uses 'sAMAccountName'
# OpenLDAP usually uses 'uid'
# The attribute holding the username of the user. This attribute is used to populate
# the username in the session information. It was introduced due to #561 to handle case
# insensitive search queries.
# For you information, Microsoft Active Directory usually uses 'sAMAccountName' and OpenLDAP
# usually uses 'uid'
username_attribute: uid
# An additional dn to define the scope to all users
additional_users_dn: ou=users
# This attribute is optional. The user filter used in the LDAP search queries
# is a combination of this filter and the username attribute.
# This filter is used to reduce the scope of users targeted by the LDAP search query.
# For instance, if the username attribute is set to 'uid', the computed filter is
# (&(uid=<username>)(objectClass=person))
# The users filter used in search queries to find the user profile based on input filled in login form.
# Various placeholders are available to represent the user input and back reference other options of the configuration:
# - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username_attribute} is a placeholder replaced by what is configured in `username_attribute`.
# - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`.
# - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it.
#
# Recommended settings are as follows:
# Microsoft Active Directory '(&(objectCategory=person)(objectClass=user))'
# OpenLDAP '(objectClass=person)' or '(objectClass=inetOrgPerson)'
users_filter: (objectClass=person)
# - Microsoft Active Directory: (&({username_attribute}={input})(objectCategory=person)(objectClass=user))
# - OpenLDAP: (&({username_attribute}={input})(objectClass=person))' or '(&({username_attribute}={input})(objectClass=inetOrgPerson))
#
# To allow sign in both with username and email, one can use a filter like
# (&(|({username_attribute}={input})({mail_attribute={input}))(objectClass=person))
users_filter: (&({username_attribute}={input})(objectClass=person))
# An additional dn to define the scope of groups
additional_groups_dn: ou=groups
# The groups filter used for retrieving groups of a given user.
# {0} is a matcher replaced by username (as provided in login portal).
# {1} is a matcher replaced by username (as stored in LDAP).
# {dn} is a matcher replaced by user DN.
# 'member={dn}' by default.
# The groups filter used in search queries to find the groups of the user.
# - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username} is a placeholder replace by the username stored in LDAP (based on `username_attribute`).
# - {dn} is a matcher replaced by the user distinguished name, aka, user DN.
# - {username_attribute} is a placeholder replaced by what is configured in `username_attribute`.
# - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`.
# - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it.
# - DON'T USE - {1} is an alias for {username} supported for backward compatibility but it will be deprecated in later version, so please don't use it.
groups_filter: (&(member={dn})(objectclass=groupOfNames))
# The attribute holding the name of the group

View File

@ -68,21 +68,21 @@ func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnecti
}
// CheckUserPassword checks if provided password matches for the given user.
func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (bool, error) {
func (p *LDAPUserProvider) CheckUserPassword(inputUsername string, password string) (bool, error) {
adminClient, err := p.connect(p.configuration.User, p.configuration.Password)
if err != nil {
return false, err
}
defer adminClient.Close()
profile, err := p.getUserProfile(adminClient, username)
profile, err := p.getUserProfile(adminClient, inputUsername)
if err != nil {
return false, err
}
conn, err := p.connect(profile.DN, password)
if err != nil {
return false, fmt.Errorf("Authentication of user %s failed. Cause: %s", username, err)
return false, fmt.Errorf("Authentication of user %s failed. Cause: %s", inputUsername, err)
}
defer conn.Close()
@ -91,13 +91,14 @@ func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (
// OWASP recommends to escape some special characters
// https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.md
const SpecialLDAPRunes = "\\,#+<>;\"="
const specialLDAPRunes = ",#+<>;\"="
func (p *LDAPUserProvider) ldapEscape(input string) string {
for _, c := range SpecialLDAPRunes {
input = strings.ReplaceAll(input, string(c), fmt.Sprintf("\\%c", c))
func (p *LDAPUserProvider) ldapEscape(inputUsername string) string {
inputUsername = ldap.EscapeFilter(inputUsername)
for _, c := range specialLDAPRunes {
inputUsername = strings.ReplaceAll(inputUsername, string(c), fmt.Sprintf("\\%c", c))
}
return input
return inputUsername
}
type ldapUserProfile struct {
@ -106,12 +107,26 @@ type ldapUserProfile struct {
Username string
}
func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, username string) (*ldapUserProfile, error) {
username = p.ldapEscape(username)
userFilter := fmt.Sprintf("(%s=%s)", p.configuration.UsernameAttribute, username)
if p.configuration.UsersFilter != "" {
userFilter = fmt.Sprintf("(&%s%s)", userFilter, p.configuration.UsersFilter)
func (p *LDAPUserProvider) resolveUsersFilter(userFilter string, inputUsername string) string {
inputUsername = p.ldapEscape(inputUsername)
// We temporarily keep placeholder {0} for backward compatibility
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)
// {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)
return userFilter
}
func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername string) (*ldapUserProfile, error) {
userFilter := p.resolveUsersFilter(p.configuration.UsersFilter, inputUsername)
logging.Logger().Tracef("Computed user filter is %s", userFilter)
baseDN := p.configuration.BaseDN
if p.configuration.AdditionalUsersDN != "" {
baseDN = p.configuration.AdditionalUsersDN + "," + baseDN
@ -129,15 +144,15 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, username string)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, fmt.Errorf("Cannot find user DN of user %s. Cause: %s", username, err)
return nil, fmt.Errorf("Cannot find user DN of user %s. Cause: %s", inputUsername, err)
}
if len(sr.Entries) == 0 {
return nil, fmt.Errorf("No user %s found", username)
return nil, fmt.Errorf("No user %s found", inputUsername)
}
if len(sr.Entries) > 1 {
return nil, fmt.Errorf("Multiple users %s found", username)
return nil, fmt.Errorf("Multiple users %s found", inputUsername)
}
userProfile := ldapUserProfile{
@ -148,51 +163,55 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, username string)
userProfile.Emails = attr.Values
} else if attr.Name == p.configuration.UsernameAttribute {
if len(attr.Values) != 1 {
return nil, fmt.Errorf("User %s cannot have multiple value for attribute %s", username, p.configuration.UsernameAttribute)
return nil, fmt.Errorf("User %s cannot have multiple value for attribute %s",
inputUsername, p.configuration.UsernameAttribute)
}
userProfile.Username = attr.Values[0]
}
}
if userProfile.DN == "" {
return nil, fmt.Errorf("No DN has been found for user %s", username)
return nil, fmt.Errorf("No DN has been found for user %s", inputUsername)
}
return &userProfile, nil
}
func (p *LDAPUserProvider) createGroupsFilter(conn LDAPConnection, username string) (string, error) {
if strings.Contains(p.configuration.GroupsFilter, "{0}") {
return strings.Replace(p.configuration.GroupsFilter, "{0}", username, -1), nil
} else if strings.Contains(p.configuration.GroupsFilter, "{dn}") {
profile, err := p.getUserProfile(conn, username)
if err != nil {
return "", err
func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ldapUserProfile) (string, error) {
inputUsername = p.ldapEscape(inputUsername)
// We temporarily keep placeholder {0} for backward compatibility
groupFilter := strings.ReplaceAll(p.configuration.GroupsFilter, "{0}", inputUsername)
groupFilter = strings.ReplaceAll(groupFilter, "{input}", inputUsername)
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, "{dn}", ldap.EscapeFilter(profile.DN))
}
return strings.Replace(p.configuration.GroupsFilter, "{dn}", ldap.EscapeFilter(profile.DN), -1), nil
} else if strings.Contains(p.configuration.GroupsFilter, "{1}") {
profile, err := p.getUserProfile(conn, username)
if err != nil {
return "", err
}
return strings.Replace(p.configuration.GroupsFilter, "{1}", profile.Username, -1), nil
}
return p.configuration.GroupsFilter, nil
return groupFilter, nil
}
// GetDetails retrieve the groups a user belongs to.
func (p *LDAPUserProvider) GetDetails(username string) (*UserDetails, error) {
func (p *LDAPUserProvider) GetDetails(inputUsername string) (*UserDetails, error) {
conn, err := p.connect(p.configuration.User, p.configuration.Password)
if err != nil {
return nil, err
}
defer conn.Close()
groupsFilter, err := p.createGroupsFilter(conn, username)
profile, err := p.getUserProfile(conn, inputUsername)
if err != nil {
return nil, fmt.Errorf("Unable to create group filter for user %s. Cause: %s", username, err)
return nil, err
}
groupsFilter, err := p.resolveGroupsFilter(inputUsername, profile)
if err != nil {
return nil, fmt.Errorf("Unable to create group filter for user %s. Cause: %s", inputUsername, err)
}
logging.Logger().Tracef("Computed groups filter is %s", groupsFilter)
groupBaseDN := p.configuration.BaseDN
if p.configuration.AdditionalGroupsDN != "" {
groupBaseDN = p.configuration.AdditionalGroupsDN + "," + groupBaseDN
@ -207,24 +226,19 @@ func (p *LDAPUserProvider) GetDetails(username string) (*UserDetails, error) {
sr, err := conn.Search(searchGroupRequest)
if err != nil {
return nil, fmt.Errorf("Unable to retrieve groups of user %s. Cause: %s", username, err)
return nil, fmt.Errorf("Unable to retrieve groups of user %s. Cause: %s", inputUsername, err)
}
groups := make([]string, 0)
for _, res := range sr.Entries {
if len(res.Attributes) == 0 {
logging.Logger().Warningf("No groups retrieved from LDAP for user %s", username)
logging.Logger().Warningf("No groups retrieved from LDAP for user %s", inputUsername)
break
}
// append all values of the document. Normally there should be only one per document.
groups = append(groups, res.Attributes[0].Values...)
}
profile, err := p.getUserProfile(conn, username)
if err != nil {
return nil, err
}
return &UserDetails{
Username: profile.Username,
Emails: profile.Emails,
@ -233,14 +247,14 @@ func (p *LDAPUserProvider) GetDetails(username string) (*UserDetails, error) {
}
// UpdatePassword update the password of the given user.
func (p *LDAPUserProvider) UpdatePassword(username string, newPassword string) error {
func (p *LDAPUserProvider) UpdatePassword(inputUsername string, newPassword string) error {
client, err := p.connect(p.configuration.User, p.configuration.Password)
if err != nil {
return fmt.Errorf("Unable to update password. Cause: %s", err)
}
profile, err := p.getUserProfile(client, username)
profile, err := p.getUserProfile(client, inputUsername)
if err != nil {
return fmt.Errorf("Unable to update password. Cause: %s", err)

View File

@ -58,7 +58,7 @@ func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) {
require.NoError(t, err)
}
func TestEscapeSpecialChars(t *testing.T) {
func TestEscapeSpecialCharsFromUserInput(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@ -72,7 +72,9 @@ func TestEscapeSpecialChars(t *testing.T) {
// Escape
assert.Equal(t, "test\\,abc", ldap.ldapEscape("test,abc"))
assert.Equal(t, "test\\\\abc", ldap.ldapEscape("test\\abc"))
assert.Equal(t, "test\\5cabc", ldap.ldapEscape("test\\abc"))
assert.Equal(t, "test\\2aabc", ldap.ldapEscape("test*abc"))
assert.Equal(t, "test \\28abc\\29", ldap.ldapEscape("test (abc)"))
assert.Equal(t, "test\\#abc", ldap.ldapEscape("test#abc"))
assert.Equal(t, "test\\+abc", ldap.ldapEscape("test+abc"))
assert.Equal(t, "test\\<abc", ldap.ldapEscape("test<abc"))
@ -80,7 +82,30 @@ func TestEscapeSpecialChars(t *testing.T) {
assert.Equal(t, "test\\;abc", ldap.ldapEscape("test;abc"))
assert.Equal(t, "test\\\"abc", ldap.ldapEscape("test\"abc"))
assert.Equal(t, "test\\=abc", ldap.ldapEscape("test=abc"))
assert.Equal(t, "test\\,\\5c\\28abc\\29", ldap.ldapEscape("test,\\(abc)"))
}
func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
GroupsFilter: "(|(member={dn})(uid={username})(uid={input}))",
}, mockFactory)
profile := ldapUserProfile{
DN: "cn=john (external),dc=example,dc=com",
Username: "john",
Emails: []string{"john.doe@authelia.com"},
}
filter, _ := ldap.resolveGroupsFilter("john", &profile)
assert.Equal(t, "(|(member=cn=john \\28external\\29,dc=example,dc=com)(uid=john)(uid=john))", filter)
filter, _ = ldap.resolveGroupsFilter("john#=(abc,def)", &profile)
assert.Equal(t, "(|(member=cn=john \\28external\\29,dc=example,dc=com)(uid=john)(uid=john\\#\\=\\28abc\\,def\\29))", filter)
}
type SearchRequestMatcher struct {
@ -110,7 +135,9 @@ func TestShouldEscapeUserInput(t *testing.T) {
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
UsernameAttribute: "uid",
MailAttribute: "mail",
Password: "password",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
@ -118,7 +145,7 @@ func TestShouldEscapeUserInput(t *testing.T) {
mockConn.EXPECT().
// Here we ensure that the input has been correctly escaped.
Search(NewSearchRequestMatcher("(uid=john\\=abc)")).
Search(NewSearchRequestMatcher("(|(uid=john\\=abc)(mail=john\\=abc))")).
Return(&ldap.SearchResult{}, nil)
ldapClient.getUserProfile(mockConn, "john=abc")
@ -135,10 +162,11 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsernameAttribute: "uid",
UsersFilter: "(&(objectCategory=person)(objectClass=user))",
UsersFilter: "(&({username_attribute}={input})(&(objectCategory=person)(objectClass=user)))",
Password: "password",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
MailAttribute: "mail",
}, mockFactory)
mockConn.EXPECT().
@ -175,7 +203,7 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) {
Password: "password",
UsernameAttribute: "uid",
MailAttribute: "mail",
UsersFilter: "uid={0}",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
@ -214,7 +242,7 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) {
},
}, nil)
gomock.InOrder(searchGroups, searchProfile)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)
@ -236,7 +264,7 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) {
User: "cn=admin,dc=example,dc=com",
Password: "password",
UsernameAttribute: "uid",
UsersFilter: "uid={0}",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
@ -271,7 +299,7 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) {
},
}, nil)
gomock.InOrder(searchGroups, searchProfile)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)
@ -294,7 +322,7 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) {
Password: "password",
UsernameAttribute: "uid",
MailAttribute: "mail",
UsersFilter: "uid={0}",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
@ -333,7 +361,7 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) {
},
}, nil)
gomock.InOrder(searchGroups, searchProfile)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)

View File

@ -21,7 +21,7 @@ authentication_backend:
base_dn: dc=example,dc=com
username_attribute: uid
additional_users_dn: ou=users
users_filter: (&(objectCategory=person)(objectClass=user))
users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user))
additional_groups_dn: ou=groups
groups_filter: (&(member={dn})(objectclass=groupOfNames))
group_name_attribute: cn

View File

@ -120,9 +120,17 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
validator.Push(errors.New("Please provide a base DN to connect to the LDAP server"))
}
if configuration.UsersFilter != "" {
if configuration.UsersFilter == "" {
validator.Push(errors.New("Please provide a users filter with `users_filter` attribute"))
} else {
if !strings.HasPrefix(configuration.UsersFilter, "(") || !strings.HasSuffix(configuration.UsersFilter, ")") {
validator.Push(errors.New("The users filter should contain enclosing parenthesis. For instance uid={0} should be (uid={0})"))
validator.Push(errors.New("The users filter should contain enclosing parenthesis. For instance uid={input} should be (uid={input})"))
}
// This test helps the user know that users_filter is broken after the breaking change induced by this commit.
if !strings.Contains(configuration.UsersFilter, "{0}") && !strings.Contains(configuration.UsersFilter, "{input}") {
validator.Push(errors.New("Unable to detect {input} placeholder in users_filter, your configuration might be broken. " +
"Please review configuration options listed at https://docs.authelia.com/configuration/authentication/ldap.html"))
}
}
@ -130,7 +138,7 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
validator.Push(errors.New("Please provide a groups filter with `groups_filter` attribute"))
} else {
if !strings.HasPrefix(configuration.GroupsFilter, "(") || !strings.HasSuffix(configuration.GroupsFilter, ")") {
validator.Push(errors.New("The groups filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})"))
validator.Push(errors.New("The groups filter should contain enclosing parenthesis. For instance cn={input} should be (cn={input})"))
}
}

View File

@ -170,8 +170,8 @@ func (suite *LdapAuthenticationBackendSuite) SetupTest() {
suite.configuration.Ldap.Password = "password"
suite.configuration.Ldap.BaseDN = "base_dn"
suite.configuration.Ldap.UsernameAttribute = "uid"
suite.configuration.Ldap.UsersFilter = "(uid={0})"
suite.configuration.Ldap.GroupsFilter = "(cn={0})"
suite.configuration.Ldap.UsersFilter = "(uid={input})"
suite.configuration.Ldap.GroupsFilter = "(cn={input})"
}
func (suite *LdapAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() {
@ -214,10 +214,11 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyGroupsFilter(
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a groups filter with `groups_filter` attribute")
}
func (suite *LdapAuthenticationBackendSuite) TestShouldAllowEmptyUsersGroupsFilter() {
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsersFilter() {
suite.configuration.Ldap.UsersFilter = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
require.Len(suite.T(), suite.validator.Errors(), 0)
require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a users filter with `users_filter` attribute")
}
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsernameAttribute() {
@ -240,17 +241,24 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute()
}
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() {
suite.configuration.Ldap.UsersFilter = "uid={0}"
suite.configuration.Ldap.UsersFilter = "uid={input}"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance uid={0} should be (uid={0})")
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance uid={input} should be (uid={input})")
}
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoesNotContainEnclosingParenthesis() {
suite.configuration.Ldap.GroupsFilter = "cn={0}"
suite.configuration.Ldap.GroupsFilter = "cn={input}"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The groups filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})")
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The groups filter should contain enclosing parenthesis. For instance cn={input} should be (cn={input})")
}
func (suite *LdapAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlaceholder() {
suite.configuration.Ldap.UsersFilter = "(objectClass=person)"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unable to detect {input} placeholder in users_filter, your configuration might be broken. Please review configuration options listed at https://docs.authelia.com/configuration/authentication/ldap.html")
}
func (suite *LdapAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {

View File

@ -17,7 +17,7 @@ authentication_backend:
base_dn: dc=example,dc=com
username_attribute: uid
additional_users_dn: ou=users
users_filter: (objectClass=person)
users_filter: (&({username_attribute}={input})(objectClass=person))
additional_groups_dn: ou=groups
groups_filter: (&(member={dn})(objectclass=groupOfNames))
group_name_attribute: cn

View File

@ -17,7 +17,7 @@ authentication_backend:
base_dn: dc=example,dc=com
username_attribute: uid
additional_users_dn: ou=users
users_filter: (objectClass=person)
users_filter: (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(objectClass=inetOrgPerson))
additional_groups_dn: ou=groups
groups_filter: (&(member={dn})(objectclass=groupOfNames))
group_name_attribute: cn

View File

@ -14,7 +14,7 @@ authentication_backend:
base_dn: dc=example,dc=com
username_attribute: uid
additional_users_dn: ou=users
users_filter: (objectClass=person)
users_filter: (&({username_attribute}={input})(objectClass=person))
additional_groups_dn: ou=groups
groups_filter: (&(member={dn})(objectclass=groupOfNames))
group_name_attribute: cn

View File

@ -0,0 +1,63 @@
package suites
// This scenario is used to test sign in using the user email address.
import (
"context"
"fmt"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type SigninEmailScenario struct {
*SeleniumSuite
}
func NewSigninEmailScenario() *SigninEmailScenario {
return &SigninEmailScenario{
SeleniumSuite: new(SeleniumSuite),
}
}
func (s *SigninEmailScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *SigninEmailScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *SigninEmailScenario) SetupTest() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s.doLogout(ctx, s.T())
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
}
func (s *SigninEmailScenario) TestShouldSignInWithUserEmail() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
targetURL := fmt.Sprintf("%s/secret.html", SingleFactorBaseURL)
s.doLoginOneFactor(ctx, s.T(), "john.doe@authelia.com", "password", false, targetURL)
s.verifySecretAuthorized(ctx, s.T())
}
func TestSigninEmailScenario(t *testing.T) {
suite.Run(t, NewSigninEmailScenario())
}

View File

@ -22,6 +22,14 @@ func (s *LDAPSuite) TestTwoFactorScenario() {
suite.Run(s.T(), NewTwoFactorScenario())
}
func (s *LDAPSuite) TestResetPassword() {
suite.Run(s.T(), NewResetPasswordScenario())
}
func (s *LDAPSuite) TestSigninEmailScenario() {
suite.Run(s.T(), NewSigninEmailScenario())
}
func TestLDAPSuite(t *testing.T) {
suite.Run(t, NewLDAPSuite())
}

View File

@ -44,7 +44,11 @@ export default function (props: Props) {
const [u2fSupported, setU2fSupported] = useState(false);
// Check that U2F is supported.
useEffect(() => { u2fApi.ensureSupport().then(() => setU2fSupported(true)) }, [setU2fSupported]);
useEffect(() => {
u2fApi.ensureSupport().then(
() => setU2fSupported(true),
() => console.error("U2F not supported"));
}, [setU2fSupported]);
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>) => {
return async () => {