[FEATURE] Support MSAD password reset via unicodePwd attribute (#1460)

* Added `ActiveDirectory` suite for integration tests with Samba AD
* Updated documentation
* Minor styling refactor to suites
* Clean up LDAP user provisioning
* Fix Authelia home splash to reference correct link for webmail
* Add notification message for password complexity errors
* Add password complexity integration test
* Rename implementation default from rfc to custom
* add specific defaults for LDAP (activedirectory implementation)
* add docs to show the new defaults
* add docs explaining the importance of users filter
* add tests
* update instances of LDAP implementation names to use the new consts where applicable
* made the 'custom' case in the UpdatePassword method for the implementation switch the default case instead
* update config examples due to the new defaults
* apply changes from code review
* replace schema default name from MSAD to ActiveDirectory for consistency
* fix missing default for username_attribute
* replace test raising on empty username attribute with not raising on empty

Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/1491/head
Amir Zarrinkafsh 2020-11-27 20:59:22 +11:00 committed by GitHub
parent ffde77bdfd
commit aa64d0c4e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 721 additions and 99 deletions

View File

@ -8,15 +8,20 @@ cat << EOF
retry: retry:
automatic: true automatic: true
EOF EOF
if [[ "${SUITE_NAME}" != "Kubernetes" ]]; then if [[ "${SUITE_NAME}" = "ActiveDirectory" ]]; then
cat << EOF cat << EOF
agents: agents:
suite: "all" suite: "activedirectory"
EOF EOF
else elif [[ "${SUITE_NAME}" = "Kubernetes" ]]; then
cat << EOF cat << EOF
agents: agents:
suite: "kubernetes" suite: "kubernetes"
EOF EOF
else
cat << EOF
agents:
suite: "all"
EOF
fi fi
done done

View File

@ -93,6 +93,18 @@ authentication_backend:
# than one instance and therefore is recommended for # than one instance and therefore is recommended for
# production. # production.
ldap: ldap:
# The LDAP implementation, this affects elements like the attribute utilised for resetting a password.
# Acceptable options are as follows:
# - 'activedirectory' - For Microsoft Active Directory.
# - 'custom' - For custom specifications of attributes and filters.
# This currently defaults to 'custom' to maintain existing behaviour.
#
# 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
# about these default values at https://docs.authelia.com/configuration/authentication/ldap.html#defaults
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://
url: ldap://127.0.0.1 url: ldap://127.0.0.1
@ -113,7 +125,7 @@ authentication_backend:
# for that user. Technically, non-unique attributes like 'mail' can also be used but we don't recommend using # for that user. Technically, non-unique attributes like 'mail' can also be used but we don't recommend using
# them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow # them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow
# 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
@ -147,14 +159,14 @@ authentication_backend:
groups_filter: (&(member={dn})(objectclass=groupOfNames)) groups_filter: (&(member={dn})(objectclass=groupOfNames))
# The attribute holding the name of the group # The attribute holding the name of the group
group_name_attribute: cn # group_name_attribute: cn
# The attribute holding the mail address of the user. If multiple email addresses are defined for a user, only the first # The attribute holding the mail address of the user. If multiple email addresses are defined for a user, only the first
# one returned by the LDAP server is used. # one returned by the LDAP server is used.
mail_attribute: mail # mail_attribute: mail
# The attribute holding the display name of the user. This will be used to greet an authenticated user. # The attribute holding the display name of the user. This will be used to greet an authenticated user.
display_name_attribute: displayname # display_name_attribute: displayname
# The username and password of the admin user. # The username and password of the admin user.
user: cn=admin,dc=example,dc=com user: cn=admin,dc=example,dc=com

View File

@ -15,6 +15,11 @@ nav_order: 2
Configuration of the LDAP backend is done as follows Configuration of the LDAP backend is done as follows
```yaml ```yaml
# The authentication backend to use for verifying user passwords
# and retrieve information such as email address and groups
# users belong to.
#
# There are two supported backends: 'ldap' and 'file'.
authentication_backend: authentication_backend:
# Disable both the HTML element and the API for reset password functionality # Disable both the HTML element and the API for reset password functionality
disable_reset_password: false disable_reset_password: false
@ -28,16 +33,32 @@ authentication_backend:
# Refresh Interval docs: https://docs.authelia.com/configuration/authentication/ldap.html#refresh-interval # Refresh Interval docs: https://docs.authelia.com/configuration/authentication/ldap.html#refresh-interval
refresh_interval: 5m refresh_interval: 5m
# LDAP backend configuration.
#
# This backend allows Authelia to be scaled to more
# than one instance and therefore is recommended for
# production.
ldap: ldap:
# The LDAP implementation, this affects elements like the attribute utilised for resetting a password.
# Acceptable options are as follows:
# - 'activedirectory' - For Microsoft Active Directory.
# - 'custom' - For custom specifications of attributes and filters.
# This currently defaults to 'custom' to maintain existing behaviour.
#
# 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
# about these default values at https://docs.authelia.com/configuration/authentication/ldap.html#defaults
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://
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 self-signed certificate).
skip_verify: false skip_verify: false
# The base dn for every entries # 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
# the username in the session information. It was introduced due to #561 to handle case # the username in the session information. It was introduced due to #561 to handle case
# insensitive search queries. # insensitive search queries.
@ -49,11 +70,11 @@ authentication_backend:
# for that user. Technically, non-unique attributes like 'mail' can also be used but we don't recommend using # for that user. Technically, non-unique attributes like 'mail' can also be used but we don't recommend using
# them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow # them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow
# 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.
# Various placeholders are available to represent the user input and back reference other options of the configuration: # 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. # - {input} is a placeholder replaced by what the user inputs in the login form.
@ -68,7 +89,7 @@ authentication_backend:
# To allow sign in both with username and email, one can use a filter like # To allow sign in both with username and email, one can use a filter like
# (&(|({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
@ -81,20 +102,19 @@ authentication_backend:
# - 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 - {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. # - 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)) groups_filter: (&(member={dn})(objectclass=groupOfNames))
# The attribute holding the name of the group
group_name_attribute: cn
# The attribute holding the mail address of the user
mail_attribute: mail
# The attribute holding the display name of the user. This will be used to greet an authenticated user.
display_name_attribute: displayname
# The username and password of the admin user. If multiple email addresses are defined for a user, only the first # The attribute holding the name of the group
# group_name_attribute: cn
# The attribute holding the mail address of the user. If multiple email addresses are defined for a user, only the first
# one returned by the LDAP server is used. # one returned by the LDAP server is used.
# mail_attribute: mail
# The attribute holding the display name of the user. This will be used to greet an authenticated user.
# display_name_attribute: displayname
# The username and password of the admin user.
user: cn=admin,dc=example,dc=com user: cn=admin,dc=example,dc=com
# Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
password: password password: password
``` ```
@ -103,6 +123,39 @@ 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.
## Implementation
There are currently two implementations, `custom` and `activedirectory`. The `activedirectory` implementation
must be used if you wish to allow users to change or reset their password as Active Directory
uses a custom attribute for this, and an input format other implementations do not use. The long term
intention of this is to have logical defaults for various RFC implementations of LDAP.
### Defaults
The below tables describes the current attribute defaults for each implementation.
#### Attributes
This table describes the attribute defaults for each implementation. i.e. the username_attribute is
described by the Username column.
|Implementation |Username |Display Name|Mail|Group Name|
|:-------------:|:------------:|:----------:|:--:|:--------:|
|custom |n/a |displayname |mail|cn |
|activedirectory|sAMAccountName|displayname |mail|cn |
#### Filters
The filters are probably the most important part to get correct when setting up LDAP.
You want to exclude disabled accounts. The active directory example has two attribute
filters that accomplish this as an example (more examples would be appreciated). The
userAccountControl filter checks that the account is not disabled and the pwdLastSet
makes sure that value is not 0 which means the password requires changing at the next login.
|Implementation |Users Filter |Groups Filter|
|:-------------:|:------------:|:-----------:|
|custom |n/a |n/a |
|activedirectory|(&(&#124;({username_attribute}={input})({mail_attribute}={input}))(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!pwdLastSet=0))|(&(member={dn})(objectClass=group)(objectCategory=group))|
## Refresh Interval ## Refresh Interval
@ -129,7 +182,7 @@ be guaranteed by the administrator to be unique. If multiple users have the same
fail authenticating the user and display an error message in the logs. fail authenticating the user and display an error message in the logs.
In order to avoid such problems, we highly recommended you follow https://www.ietf.org/rfc/rfc2307.txt by using In order to avoid such problems, we highly recommended you follow https://www.ietf.org/rfc/rfc2307.txt by using
`sAMAccountName` for Microsoft Active Directory and `uid` for other implementations as the attribute holding the `sAMAccountName` for Active Directory and `uid` for other implementations as the attribute holding the
unique identifier for your users. unique identifier for your users.
## Loading a password from a secret instead of inside the configuration ## Loading a password from a secret instead of inside the configuration

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"golang.org/x/text/encoding/unicode"
"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"
@ -284,7 +285,16 @@ func (p *LDAPUserProvider) UpdatePassword(inputUsername string, newPassword stri
modifyRequest := ldap.NewModifyRequest(profile.DN, nil) modifyRequest := ldap.NewModifyRequest(profile.DN, nil)
modifyRequest.Replace("userPassword", []string{newPassword}) switch p.configuration.Implementation {
case schema.LDAPImplementationActiveDirectory:
utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
// The password needs to be enclosed in quotes
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2
pwdEncoded, _ := utf16.NewEncoder().String(fmt.Sprintf("\"%s\"", newPassword))
modifyRequest.Replace("unicodePwd", []string{pwdEncoded})
default:
modifyRequest.Replace("userPassword", []string{newPassword})
}
err = client.Modify(modifyRequest) err = client.Modify(modifyRequest)

View File

@ -2,6 +2,7 @@ package schema
// LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server. // LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server.
type LDAPAuthenticationBackendConfiguration struct { type LDAPAuthenticationBackendConfiguration struct {
Implementation string `mapstructure:"implementation"`
URL string `mapstructure:"url"` URL string `mapstructure:"url"`
SkipVerify bool `mapstructure:"skip_verify"` SkipVerify bool `mapstructure:"skip_verify"`
BaseDN string `mapstructure:"base_dn"` BaseDN string `mapstructure:"base_dn"`
@ -70,7 +71,19 @@ var DefaultPasswordSHA512Configuration = PasswordConfiguration{
// DefaultLDAPAuthenticationBackendConfiguration represents the default LDAP config. // DefaultLDAPAuthenticationBackendConfiguration represents the default LDAP config.
var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendConfiguration{ var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendConfiguration{
Implementation: LDAPImplementationCustom,
UsernameAttribute: "uid",
MailAttribute: "mail", MailAttribute: "mail",
DisplayNameAttribute: "displayname", DisplayNameAttribute: "displayname",
GroupNameAttribute: "cn", GroupNameAttribute: "cn",
} }
// DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration represents the default LDAP config for the MSAD Implementation.
var DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration = LDAPAuthenticationBackendConfiguration{
UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!pwdLastSet=0))",
UsernameAttribute: "sAMAccountName",
MailAttribute: "mail",
DisplayNameAttribute: "displayName",
GroupsFilter: "(&(member={dn})(objectClass=group))",
GroupNameAttribute: "cn",
}

View File

@ -19,3 +19,9 @@ const RefreshIntervalDefault = "5m"
// RefreshIntervalAlways represents the duration value refresh interval should have if set to always. // RefreshIntervalAlways represents the duration value refresh interval should have if set to always.
const RefreshIntervalAlways = 0 * time.Millisecond const RefreshIntervalAlways = 0 * time.Millisecond
// LDAPImplementationCustom is the string for the custom LDAP implementation.
const LDAPImplementationCustom = "custom"
// LDAPImplementationActiveDirectory is the string for the Active Directory LDAP implementation.
const LDAPImplementationActiveDirectory = "activedirectory"

View File

@ -100,6 +100,19 @@ func validateLdapURL(ldapURL string, validator *schema.StructValidator) string {
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting. //nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting.
func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) { func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.Implementation == "" {
configuration.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation
}
switch configuration.Implementation {
case schema.LDAPImplementationCustom:
setDefaultImplementationCustomLdapAuthenticationBackend(configuration)
case schema.LDAPImplementationActiveDirectory:
setDefaultImplementationActiveDirectoryLdapAuthenticationBackend(configuration)
default:
validator.Push(fmt.Errorf("authentication backend ldap implementation must be blank or one of the following values `%s`, `%s`", schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory))
}
if configuration.URL == "" { if configuration.URL == "" {
validator.Push(errors.New("Please provide a URL to the LDAP server")) validator.Push(errors.New("Please provide a URL to the LDAP server"))
} else { } else {
@ -143,6 +156,38 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
if configuration.UsernameAttribute == "" { if configuration.UsernameAttribute == "" {
validator.Push(errors.New("Please provide a username attribute with `username_attribute`")) validator.Push(errors.New("Please provide a username attribute with `username_attribute`"))
} }
}
func setDefaultImplementationActiveDirectoryLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration) {
if configuration.UsersFilter == "" {
configuration.UsersFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter
}
if configuration.UsernameAttribute == "" {
configuration.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute
}
if configuration.DisplayNameAttribute == "" {
configuration.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute
}
if configuration.MailAttribute == "" {
configuration.MailAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute
}
if configuration.GroupsFilter == "" {
configuration.GroupsFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter
}
if configuration.GroupNameAttribute == "" {
configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute
}
}
func setDefaultImplementationCustomLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration) {
if configuration.UsernameAttribute == "" {
configuration.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute
}
if configuration.GroupNameAttribute == "" { if configuration.GroupNameAttribute == "" {
configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute

View File

@ -16,7 +16,7 @@ func TestShouldRaiseErrorsWhenNoBackendProvided(t *testing.T) {
ValidateAuthenticationBackend(&backendConfig, validator) ValidateAuthenticationBackend(&backendConfig, validator)
assert.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Please provide `ldap` or `file` object in `authentication_backend`") assert.EqualError(t, validator.Errors()[0], "Please provide `ldap` or `file` object in `authentication_backend`")
} }
@ -47,7 +47,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfigura
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() {
suite.configuration.File.Path = "" suite.configuration.File.Path = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`")
} }
@ -55,7 +55,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMo
suite.configuration.File.Password.Memory = 8 suite.configuration.File.Password.Memory = 8
suite.configuration.File.Password.Parallelism = 2 suite.configuration.File.Password.Parallelism = 2
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Memory for argon2id must be 16 or more (parallelism * 8), you configured memory as 8 and parallelism as 2") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Memory for argon2id must be 16 or more (parallelism * 8), you configured memory as 8 and parallelism as 2")
} }
@ -98,35 +98,35 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWh
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() {
suite.configuration.File.Password.KeyLength = 1 suite.configuration.File.Password.KeyLength = 1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Key length for argon2id must be 16, you configured 1") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Key length for argon2id must be 16, you configured 1")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() {
suite.configuration.File.Password.SaltLength = -1 suite.configuration.File.Password.SaltLength = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The salt length must be 2 or more, you configured -1") assert.EqualError(suite.T(), suite.validator.Errors()[0], "The salt length must be 2 or more, you configured -1")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() {
suite.configuration.File.Password.Algorithm = "bogus" suite.configuration.File.Password.Algorithm = "bogus"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured 'bogus'") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured 'bogus'")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() {
suite.configuration.File.Password.Iterations = -1 suite.configuration.File.Password.Iterations = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "The number of iterations specified is invalid, must be 1 or more, you configured -1") assert.EqualError(suite.T(), suite.validator.Errors()[0], "The number of iterations specified is invalid, must be 1 or more, you configured -1")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() {
suite.configuration.File.Password.Parallelism = -1 suite.configuration.File.Password.Parallelism = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Parallelism for argon2id must be 1 or more, you configured -1") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Parallelism for argon2id must be 1 or more, you configured -1")
} }
@ -159,6 +159,7 @@ func (suite *LdapAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration = schema.AuthenticationBackendConfiguration{} suite.configuration = schema.AuthenticationBackendConfiguration{}
suite.configuration.Ldap = &schema.LDAPAuthenticationBackendConfiguration{} suite.configuration.Ldap = &schema.LDAPAuthenticationBackendConfiguration{}
suite.configuration.Ldap.Implementation = schema.LDAPImplementationCustom
suite.configuration.Ldap.URL = "ldap://ldap" suite.configuration.Ldap.URL = "ldap://ldap"
suite.configuration.Ldap.User = "user" suite.configuration.Ldap.User = "user"
suite.configuration.Ldap.Password = "password" suite.configuration.Ldap.Password = "password"
@ -173,31 +174,38 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldValidateCompleteConfigura
assert.Len(suite.T(), suite.validator.Errors(), 0) assert.Len(suite.T(), suite.validator.Errors(), 0)
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementationIsInvalidMSAD() {
suite.configuration.Ldap.Implementation = "masd"
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "authentication backend ldap implementation must be blank or one of the following values `custom`, `activedirectory`")
}
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() { func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() {
suite.configuration.Ldap.URL = "" suite.configuration.Ldap.URL = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a URL to the LDAP server") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a URL to the LDAP server")
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProvided() { func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProvided() {
suite.configuration.Ldap.User = "" suite.configuration.Ldap.User = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a user name to connect to the LDAP server") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a user name to connect to the LDAP server")
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNotProvided() { func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNotProvided() {
suite.configuration.Ldap.Password = "" suite.configuration.Ldap.Password = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a password to connect to the LDAP server") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a password to connect to the LDAP server")
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotProvided() { func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotProvided() {
suite.configuration.Ldap.BaseDN = "" suite.configuration.Ldap.BaseDN = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a base DN to connect to the LDAP server") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a base DN to connect to the LDAP server")
} }
@ -215,11 +223,10 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsersFilter()
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a users filter with `users_filter` attribute") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a users filter with `users_filter` attribute")
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsernameAttribute() { func (suite *LdapAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAttribute() {
suite.configuration.Ldap.UsernameAttribute = "" suite.configuration.Ldap.UsernameAttribute = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
require.Len(suite.T(), suite.validator.Errors(), 1) assert.Len(suite.T(), suite.validator.Errors(), 0)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a username attribute with `username_attribute`")
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() { func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() {
@ -229,6 +236,12 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Auth Backend `refresh_interval` is configured to 'blah' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: Could not convert the input string of blah into a duration") assert.EqualError(suite.T(), suite.validator.Errors()[0], "Auth Backend `refresh_interval` is configured to 'blah' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: Could not convert the input string of blah into a duration")
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultImplementation() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), schema.LDAPImplementationCustom, suite.configuration.Ldap.Implementation)
}
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() { func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0) assert.Len(suite.T(), suite.validator.Errors(), 0)
@ -237,34 +250,40 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttrib
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() { func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0) require.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), "mail", suite.configuration.Ldap.MailAttribute) assert.Equal(suite.T(), "mail", suite.configuration.Ldap.MailAttribute)
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttribute() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
require.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), "displayname", suite.configuration.Ldap.DisplayNameAttribute)
}
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() { func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 0) require.Len(suite.T(), suite.validator.Errors(), 0)
assert.Equal(suite.T(), "5m", suite.configuration.RefreshInterval) assert.Equal(suite.T(), "5m", suite.configuration.RefreshInterval)
} }
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() { func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() {
suite.configuration.Ldap.UsersFilter = "uid={input}" suite.configuration.Ldap.UsersFilter = "uid={input}"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.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={input} should be (uid={input})") 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() { func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoesNotContainEnclosingParenthesis() {
suite.configuration.Ldap.GroupsFilter = "cn={input}" suite.configuration.Ldap.GroupsFilter = "cn={input}"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.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={input} should be (cn={input})") 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() { func (suite *LdapAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlaceholder() {
suite.configuration.Ldap.UsersFilter = "(objectClass=person)" suite.configuration.Ldap.UsersFilter = "(objectClass=person)"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.configuration, suite.validator)
assert.Len(suite.T(), suite.validator.Errors(), 1) require.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") 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")
} }
@ -289,3 +308,79 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {
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

@ -91,6 +91,7 @@ var validKeys = []string{
"authentication_backend.refresh_interval", "authentication_backend.refresh_interval",
// LDAP Authentication Backend Keys. // LDAP Authentication Backend Keys.
"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.base_dn", "authentication_backend.ldap.base_dn",

View File

@ -37,6 +37,8 @@ const unableToRegisterSecurityKeyMessage = "Unable to register your security key
const unableToResetPasswordMessage = "Unable to reset your password." const unableToResetPasswordMessage = "Unable to reset your password."
const mfaValidationFailedMessage = "Authentication failed, please retry later." const mfaValidationFailedMessage = "Authentication failed, please retry later."
const ldapPasswordComplexityCode = "0000052D"
const testInactivity = "10" const testInactivity = "10"
const testRedirectionURL = "http://redirection.local" const testRedirectionURL = "http://redirection.local"
const testResultAllow = "allow" const testResultAllow = "allow"

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"fmt" "fmt"
"strings"
"github.com/authelia/authelia/internal/middlewares" "github.com/authelia/authelia/internal/middlewares"
) )
@ -29,7 +30,13 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password) err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password)
if err != nil { if err != nil {
ctx.Error(fmt.Errorf("Unable to update password: %s", err), unableToResetPasswordMessage) switch {
case strings.Contains(err.Error(), ldapPasswordComplexityCode):
ctx.Error(fmt.Errorf("%s", err), ldapPasswordComplexityCode)
default:
ctx.Error(fmt.Errorf("%s", err), unableToResetPasswordMessage)
}
return return
} }

View File

@ -0,0 +1,68 @@
###############################################################
# Authelia minimal configuration #
###############################################################
port: 9091
tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem
log_level: debug
default_redirection_url: https://home.example.com:8080/
jwt_secret: very_important_secret
authentication_backend:
ldap:
implementation: activedirectory
url: ldaps://sambaldap
skip_verify: true
base_dn: DC=example,DC=com
username_attribute: sAMAccountName
additional_users_dn: OU=Users
users_filter: (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(objectClass=user))
additional_groups_dn: OU=Groups
groups_filter: (&(member={dn})(objectClass=group))
group_name_attribute: cn
mail_attribute: mail
display_name_attribute: displayName
user: CN=Administrator,CN=Users,DC=example,DC=com
password: password
session:
secret: unsecure_session_secret
domain: example.com
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
remember_me_duration: 1y
storage:
local:
path: /config/db.sqlite3
totp:
issuer: example.com
access_control:
default_policy: deny
rules:
- domain: "public.example.com"
policy: bypass
- domain: "admin.example.com"
policy: two_factor
- domain: "secure.example.com"
policy: two_factor
- domain: "singlefactor.example.com"
policy: one_factor
regulation:
max_retries: 3
find_time: 300
ban_time: 900
notifier:
smtp:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -0,0 +1,6 @@
version: '3'
services:
authelia-backend:
volumes:
- './ActiveDirectory/configuration.yml:/config/configuration.yml:ro'
- './common/ssl:/config/ssl:ro'

View File

@ -27,9 +27,18 @@ func (wds *WebDriverSession) doSuccessfullyCompletePasswordReset(ctx context.Con
wds.verifyIsFirstFactorPage(ctx, t) wds.verifyIsFirstFactorPage(ctx, t)
} }
func (wds *WebDriverSession) doResetPassword(ctx context.Context, t *testing.T, username, newPassword1, newPassword2 string) { func (wds *WebDriverSession) doUnsuccessfulPasswordReset(ctx context.Context, t *testing.T, newPassword1, newPassword2 string) {
wds.doCompletePasswordReset(ctx, t, newPassword1, newPassword2)
}
func (wds *WebDriverSession) doResetPassword(ctx context.Context, t *testing.T, username, newPassword1, newPassword2 string, unsuccessful bool) {
wds.doInitiatePasswordReset(ctx, t, username) wds.doInitiatePasswordReset(ctx, t, username)
// then wait for the "email sent notification" // then wait for the "email sent notification"
wds.verifyMailNotificationDisplayed(ctx, t) wds.verifyMailNotificationDisplayed(ctx, t)
wds.doSuccessfullyCompletePasswordReset(ctx, t, newPassword1, newPassword2)
if unsuccessful {
wds.doUnsuccessfulPasswordReset(ctx, t, newPassword1, newPassword2)
} else {
wds.doSuccessfullyCompletePasswordReset(ctx, t, newPassword1, newPassword2)
}
} }

View File

@ -58,7 +58,16 @@ func waitUntilAutheliaFrontendIsReady(dockerEnvironment *DockerEnvironment) erro
[]string{"You can now view web in the browser.", "Compiled with warnings", "Compiled successfully!"}) []string{"You can now view web in the browser.", "Compiled with warnings", "Compiled successfully!"})
} }
func waitUntilAutheliaIsReady(dockerEnvironment *DockerEnvironment) error { func waitUntilSambaIsReady(dockerEnvironment *DockerEnvironment) error {
return waitUntilServiceLogDetected(
5*time.Second,
90*time.Second,
dockerEnvironment,
"sambaldap",
[]string{"samba entered RUNNING state"})
}
func waitUntilAutheliaIsReady(dockerEnvironment *DockerEnvironment, suite string) error {
log.Info("Waiting for Authelia to be ready...") log.Info("Waiting for Authelia to be ready...")
if err := waitUntilAutheliaBackendIsReady(dockerEnvironment); err != nil { if err := waitUntilAutheliaBackendIsReady(dockerEnvironment); err != nil {
@ -71,6 +80,12 @@ func waitUntilAutheliaIsReady(dockerEnvironment *DockerEnvironment) error {
} }
} }
if suite == "ActiveDirectory" {
if err := waitUntilSambaIsReady(dockerEnvironment); err != nil {
return err
}
}
log.Info("Authelia is now ready!") log.Info("Authelia is now ready!")
return nil return nil

View File

@ -65,13 +65,3 @@ sn: Dean
uid: james uid: james
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
dn: cn=Billy Blackhat,ou=users,dc=example,dc=com
cn: Billy Blackhat
displayname: Billy Blackhat
givenName: Billy
objectclass: inetOrgPerson
objectclass: top
mail: billy.blackhat@authelia.com
sn: BlackHat
uid: blackhat
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/

View File

@ -68,7 +68,7 @@
<br /> Once first factor is passed, you will need to follow the links to register a secret for the second <br /> Once first factor is passed, you will need to follow the links to register a secret for the second
factor.<br /> Authelia factor.<br /> Authelia
will send you a fictitious email in a <strong>fake webmail</strong> at <a will send you a fictitious email in a <strong>fake webmail</strong> at <a
href="http://localhost:8085">http://localhost:8085</a>.<br /> href="https://mail.example.com:8080/">https://mail.example.com:8080/</a>.<br />
It will provide you with the link to complete the registration allowing you to authenticate with 2-factor. It will provide you with the link to complete the registration allowing you to authenticate with 2-factor.
<ul> <ul>

View File

@ -0,0 +1,12 @@
FROM alpine:3.12.1
RUN \
apk add --no-cache \
bash \
krb5 \
openldap-clients \
samba-dc \
supervisor
ADD init.sh /init.sh
CMD /init.sh setup

View File

@ -0,0 +1,14 @@
version: '3'
services:
sambaldap:
build:
context: ./example/compose/samba
cap_add:
- SYS_ADMIN
hostname: ldap.example.com
environment:
- DOMAIN=example.com
- DOMAINPASS=Password1
- NOCOMPLEXITY=true
networks:
- authelianet

View File

@ -0,0 +1,103 @@
#!/bin/bash
set -e
appSetup () {
# Set variables
DOMAIN=${DOMAIN:-SAMDOM.LOCAL}
DOMAINPASS=${DOMAINPASS:-youshouldsetapassword}
NOCOMPLEXITY=${NOCOMPLEXITY:-false}
INSECURELDAP=${INSECURELDAP:-false}
LDOMAIN=${DOMAIN,,}
UDOMAIN=${DOMAIN^^}
URDOMAIN=${UDOMAIN%%.*}
# Set up samba
mv /etc/krb5.conf /etc/krb5.conf.orig
echo "[libdefaults]" > /etc/krb5.conf
echo " dns_lookup_realm = false" >> /etc/krb5.conf
echo " dns_lookup_kdc = true" >> /etc/krb5.conf
echo " default_realm = ${UDOMAIN}" >> /etc/krb5.conf
# If the finished file isn't there, this is brand new, we're not just moving to a new container
if [[ ! -f /etc/samba/external/smb.conf ]]; then
mv /etc/samba/smb.conf /etc/samba/smb.conf.orig
samba-tool domain provision --use-rfc2307 --domain=${URDOMAIN} --realm=${UDOMAIN} --server-role=dc --dns-backend=SAMBA_INTERNAL --adminpass=${DOMAINPASS}
if [[ ${NOCOMPLEXITY,,} == "true" ]]; then
samba-tool domain passwordsettings set --complexity=off
samba-tool domain passwordsettings set --history-length=0
samba-tool domain passwordsettings set --min-pwd-length=3
samba-tool domain passwordsettings set --min-pwd-age=0
samba-tool domain passwordsettings set --max-pwd-age=0
fi
sed -i "/\[global\]/a \
\\\tidmap_ldb:use rfc2307 = yes\\n\
wins support = yes\\n\
template shell = /bin/bash\\n\
winbind nss info = rfc2307\\n\
idmap config ${URDOMAIN}: range = 10000-20000\\n\
idmap config ${URDOMAIN}: backend = ad\
" /etc/samba/smb.conf
if [[ ${INSECURELDAP,,} == "true" ]]; then
sed -i "/\[global\]/a \
\\\tldap server require strong auth = no\
" /etc/samba/smb.conf
fi
# Once we are set up, we'll make a file so that we know to use it if we ever spin this up again
mkdir -p /etc/samba/external
cp /etc/samba/smb.conf /etc/samba/external/smb.conf
else
cp /etc/samba/external/smb.conf /etc/samba/smb.conf
fi
# Set up supervisor
mkdir /etc/supervisor.d/
echo "[supervisord]" > /etc/supervisor.d/supervisord.ini
echo "nodaemon=true" >> /etc/supervisor.d/supervisord.ini
echo "" >> /etc/supervisor.d/supervisord.ini
echo "[program:samba]" >> /etc/supervisor.d/supervisord.ini
echo "command=/usr/sbin/samba -i" >> /etc/supervisor.d/supervisord.ini
appProvision
appStart
}
appStart () {
/usr/bin/supervisord
}
appProvision () {
samba-tool user setpassword administrator --newpassword=password
samba-tool ou create "OU=Users"
samba-tool ou create "OU=Groups"
samba-tool group add dev --groupou=OU=Groups
samba-tool group add admins --groupou=OU=Groups
samba-tool user create john password --userou=OU=Users --use-username-as-cn --given-name John --surname Doe --mail-address john.doe@authelia.com
samba-tool user create harry password --userou=OU=Users --use-username-as-cn --given-name Harry --surname Potter --mail-address harry.potter@authelia.com
samba-tool user create bob password --userou=OU=Users --use-username-as-cn --given-name Bob --surname Dylan --mail-address bob.dylan@authelia.com
samba-tool user create james password --userou=OU=Users --use-username-as-cn --given-name James --surname Dean --mail-address james.dean@authelia.com
samba-tool group addmembers "dev" john,bob
samba-tool group addmembers "admins" john
}
case "$1" in
start)
if [[ -f /etc/samba/external/smb.conf ]]; then
cp /etc/samba/external/smb.conf /etc/samba/smb.conf
appStart
else
echo "Config file is missing."
fi
;;
setup)
# If the supervisor conf isn't there, we're spinning up a new container
if [[ -f /etc/supervisor.d/supervisord.ini ]]; then
appStart
else
appSetup
fi
;;
esac
exit 0

View File

@ -0,0 +1,61 @@
package suites
import (
"context"
"log"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type PasswordComplexityScenario struct {
*SeleniumSuite
}
func NewPasswordComplexityScenario() *PasswordComplexityScenario {
return &PasswordComplexityScenario{SeleniumSuite: new(SeleniumSuite)}
}
func (s *PasswordComplexityScenario) SetupSuite() {
wds, err := StartWebDriver()
if err != nil {
log.Fatal(err)
}
s.WebDriverSession = wds
}
func (s *PasswordComplexityScenario) TearDownSuite() {
err := s.WebDriverSession.Stop()
if err != nil {
log.Fatal(err)
}
}
func (s *PasswordComplexityScenario) 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 *PasswordComplexityScenario) TestShouldRejectPasswordReset() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
s.doVisit(s.T(), GetLoginBaseURL())
s.verifyIsFirstFactorPage(ctx, s.T())
// Attempt to reset the password to a
s.doResetPassword(ctx, s.T(), "john", "a", "a", true)
s.verifyNotificationDisplayed(ctx, s.T(), "Your supplied password does not meet the password policy requirements.")
}
func TestRunPasswordComplexityScenario(t *testing.T) {
suite.Run(t, NewPasswordComplexityScenario())
}

View File

@ -52,7 +52,7 @@ func (s *ResetPasswordScenario) TestShouldResetPassword() {
s.verifyIsFirstFactorPage(ctx, s.T()) s.verifyIsFirstFactorPage(ctx, s.T())
// Reset the password to abc // Reset the password to abc
s.doResetPassword(ctx, s.T(), "john", "abc", "abc") s.doResetPassword(ctx, s.T(), "john", "abc", "abc", false)
// Try to login with the old password // Try to login with the old password
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
@ -65,7 +65,7 @@ func (s *ResetPasswordScenario) TestShouldResetPassword() {
s.doLogout(ctx, s.T()) s.doLogout(ctx, s.T())
// Reset the original password // Reset the original password
s.doResetPassword(ctx, s.T(), "john", "password", "password") s.doResetPassword(ctx, s.T(), "john", "password", "password", false)
} }
func (s *ResetPasswordScenario) TestShouldMakeAttackerThinkPasswordResetIsInitiated() { func (s *ResetPasswordScenario) TestShouldMakeAttackerThinkPasswordResetIsInitiated() {

View File

@ -0,0 +1,62 @@
package suites
import (
"fmt"
"time"
)
var activedirectorySuiteName = "ActiveDirectory"
func init() {
dockerEnvironment := NewDockerEnvironment([]string{
"internal/suites/docker-compose.yml",
"internal/suites/ActiveDirectory/docker-compose.yml",
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
"internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
"internal/suites/example/compose/nginx/backend/docker-compose.yml",
"internal/suites/example/compose/nginx/portal/docker-compose.yml",
"internal/suites/example/compose/smtp/docker-compose.yml",
"internal/suites/example/compose/samba/docker-compose.yml",
})
setup := func(suitePath string) error {
if err := dockerEnvironment.Up(); err != nil {
return err
}
return waitUntilAutheliaIsReady(dockerEnvironment, activedirectorySuiteName)
}
displayAutheliaLogs := func() error {
backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil)
if err != nil {
return err
}
fmt.Println(backendLogs)
frontendLogs, err := dockerEnvironment.Logs("authelia-frontend", nil)
if err != nil {
return err
}
fmt.Println(frontendLogs)
return nil
}
teardown := func(suitePath string) error {
err := dockerEnvironment.Down()
return err
}
GlobalRegistry.Register(activedirectorySuiteName, Suite{
SetUp: setup,
SetUpTimeout: 5 * time.Minute,
OnSetupTimeout: displayAutheliaLogs,
TestTimeout: 120 * time.Second,
TearDown: teardown,
TearDownTimeout: 2 * time.Minute,
OnError: displayAutheliaLogs,
})
}

View File

@ -0,0 +1,39 @@
package suites
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ActiveDirectorySuite struct {
*SeleniumSuite
}
func NewActiveDirectorySuite() *ActiveDirectorySuite {
return &ActiveDirectorySuite{SeleniumSuite: new(SeleniumSuite)}
}
func (s *ActiveDirectorySuite) TestOneFactorScenario() {
suite.Run(s.T(), NewOneFactorScenario())
}
func (s *ActiveDirectorySuite) TestTwoFactorScenario() {
suite.Run(s.T(), NewTwoFactorScenario())
}
func (s *ActiveDirectorySuite) TestResetPassword() {
suite.Run(s.T(), NewResetPasswordScenario())
}
func (s *ActiveDirectorySuite) TestPasswordComplexity() {
suite.Run(s.T(), NewPasswordComplexityScenario())
}
func (s *ActiveDirectorySuite) TestSigninEmailScenario() {
suite.Run(s.T(), NewSigninEmailScenario())
}
func TestActiveDirectorySuite(t *testing.T) {
suite.Run(t, NewActiveDirectorySuite())
}

View File

@ -25,7 +25,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, bypassAllSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -5,6 +5,8 @@ import (
"time" "time"
) )
var dockerSuiteName = "Docker"
func init() { func init() {
dockerEnvironment := NewDockerEnvironment([]string{ dockerEnvironment := NewDockerEnvironment([]string{
"internal/suites/docker-compose.yml", "internal/suites/docker-compose.yml",
@ -21,7 +23,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, dockerSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {
@ -46,7 +48,7 @@ func init() {
return dockerEnvironment.Down() return dockerEnvironment.Down()
} }
GlobalRegistry.Register("Docker", Suite{ GlobalRegistry.Register(dockerSuiteName, Suite{
SetUp: setup, SetUp: setup,
SetUpTimeout: 5 * time.Minute, SetUpTimeout: 5 * time.Minute,
OnSetupTimeout: displayAutheliaLogs, OnSetupTimeout: displayAutheliaLogs,

View File

@ -23,7 +23,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, duoPushSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -20,13 +20,11 @@ func init() {
}) })
setup := func(suitePath string) error { setup := func(suitePath string) error {
err := dockerEnvironment.Up() if err := dockerEnvironment.Up(); err != nil {
if err != nil {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, haproxySuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -28,7 +28,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(haDockerEnvironment) return waitUntilAutheliaIsReady(haDockerEnvironment, highAvailabilitySuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -21,13 +21,11 @@ func init() {
}) })
setup := func(suitePath string) error { setup := func(suitePath string) error {
err := dockerEnvironment.Up() if err := dockerEnvironment.Up(); err != nil {
if err != nil {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, ldapSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -25,7 +25,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, mariadbSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -25,7 +25,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, mysqlSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -26,7 +26,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, networkACLSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -22,7 +22,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, oneFactorOnlySuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -20,13 +20,11 @@ func init() {
}) })
setup := func(suitePath string) error { setup := func(suitePath string) error {
err := dockerEnvironment.Up() if err := dockerEnvironment.Up(); err != nil {
if err != nil {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, pathPrefixSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -25,7 +25,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, postgresSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -23,7 +23,7 @@ func init() {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, shortTimeoutsSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -25,13 +25,11 @@ func init() {
}) })
setup := func(suitePath string) error { setup := func(suitePath string) error {
err := dockerEnvironment.Up() if err := dockerEnvironment.Up(); err != nil {
if err != nil {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, standaloneSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -20,13 +20,11 @@ func init() {
}) })
setup := func(suitePath string) error { setup := func(suitePath string) error {
err := dockerEnvironment.Up() if err := dockerEnvironment.Up(); err != nil {
if err != nil {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, traefikSuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -20,13 +20,11 @@ func init() {
}) })
setup := func(suitePath string) error { setup := func(suitePath string) error {
err := dockerEnvironment.Up() if err := dockerEnvironment.Up(); err != nil {
if err != nil {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment) return waitUntilAutheliaIsReady(dockerEnvironment, traefik2SuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {

View File

@ -70,7 +70,11 @@ const ResetPasswordStep2 = function () {
setFormDisabled(true); setFormDisabled(true);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createErrorNotification("There was an issue resetting the password."); if (err.message.indexOf("0000052D")) {
createErrorNotification("Your supplied password does not meet the password policy requirements.");
} else {
createErrorNotification("There was an issue resetting the password.");
}
} }
} }