[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
parent
95f6c1a893
commit
7a3e782dc0
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Reference in New Issue