[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
parent
ffde77bdfd
commit
aa64d0c4e5
|
@ -8,15 +8,20 @@ cat << EOF
|
|||
retry:
|
||||
automatic: true
|
||||
EOF
|
||||
if [[ "${SUITE_NAME}" != "Kubernetes" ]]; then
|
||||
if [[ "${SUITE_NAME}" = "ActiveDirectory" ]]; then
|
||||
cat << EOF
|
||||
agents:
|
||||
suite: "all"
|
||||
suite: "activedirectory"
|
||||
EOF
|
||||
else
|
||||
elif [[ "${SUITE_NAME}" = "Kubernetes" ]]; then
|
||||
cat << EOF
|
||||
agents:
|
||||
suite: "kubernetes"
|
||||
EOF
|
||||
else
|
||||
cat << EOF
|
||||
agents:
|
||||
suite: "all"
|
||||
EOF
|
||||
fi
|
||||
done
|
|
@ -93,6 +93,18 @@ authentication_backend:
|
|||
# than one instance and therefore is recommended for
|
||||
# production.
|
||||
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://
|
||||
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
|
||||
# them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow
|
||||
# https://www.ietf.org/rfc/rfc2307.txt.
|
||||
username_attribute: uid
|
||||
# username_attribute: uid
|
||||
|
||||
# An additional dn to define the scope to all users
|
||||
additional_users_dn: ou=users
|
||||
|
@ -147,14 +159,14 @@ authentication_backend:
|
|||
groups_filter: (&(member={dn})(objectclass=groupOfNames))
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
display_name_attribute: displayname
|
||||
# display_name_attribute: displayname
|
||||
|
||||
# The username and password of the admin user.
|
||||
user: cn=admin,dc=example,dc=com
|
||||
|
|
|
@ -15,6 +15,11 @@ nav_order: 2
|
|||
Configuration of the LDAP backend is done as follows
|
||||
|
||||
```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:
|
||||
# Disable both the HTML element and the API for reset password functionality
|
||||
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: 5m
|
||||
|
||||
# LDAP backend configuration.
|
||||
#
|
||||
# This backend allows Authelia to be scaled to more
|
||||
# than one instance and therefore is recommended for
|
||||
# production.
|
||||
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://
|
||||
url: ldap://127.0.0.1
|
||||
|
||||
|
||||
# Skip verifying the server certificate (to allow self-signed certificate).
|
||||
skip_verify: false
|
||||
|
||||
|
||||
# The base dn for every entries
|
||||
base_dn: dc=example,dc=com
|
||||
|
||||
|
||||
# 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.
|
||||
|
@ -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
|
||||
# them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow
|
||||
# https://www.ietf.org/rfc/rfc2307.txt.
|
||||
username_attribute: uid
|
||||
# username_attribute: uid
|
||||
|
||||
# An additional dn to define the scope to all 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.
|
||||
# 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.
|
||||
|
@ -68,7 +89,7 @@ authentication_backend:
|
|||
# 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
|
||||
|
||||
|
@ -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 - {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
|
||||
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.
|
||||
# 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
|
||||
|
||||
# Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
|
||||
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
|
||||
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|(&(|({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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
## Loading a password from a secret instead of inside the configuration
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
"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.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)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
|||
|
||||
// LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server.
|
||||
type LDAPAuthenticationBackendConfiguration struct {
|
||||
Implementation string `mapstructure:"implementation"`
|
||||
URL string `mapstructure:"url"`
|
||||
SkipVerify bool `mapstructure:"skip_verify"`
|
||||
BaseDN string `mapstructure:"base_dn"`
|
||||
|
@ -70,7 +71,19 @@ var DefaultPasswordSHA512Configuration = PasswordConfiguration{
|
|||
|
||||
// DefaultLDAPAuthenticationBackendConfiguration represents the default LDAP config.
|
||||
var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendConfiguration{
|
||||
Implementation: LDAPImplementationCustom,
|
||||
UsernameAttribute: "uid",
|
||||
MailAttribute: "mail",
|
||||
DisplayNameAttribute: "displayname",
|
||||
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",
|
||||
}
|
||||
|
|
|
@ -19,3 +19,9 @@ const RefreshIntervalDefault = "5m"
|
|||
|
||||
// RefreshIntervalAlways represents the duration value refresh interval should have if set to always.
|
||||
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"
|
||||
|
|
|
@ -100,6 +100,19 @@ func validateLdapURL(ldapURL string, validator *schema.StructValidator) string {
|
|||
|
||||
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting.
|
||||
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 == "" {
|
||||
validator.Push(errors.New("Please provide a URL to the LDAP server"))
|
||||
} else {
|
||||
|
@ -143,6 +156,38 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
|
|||
if configuration.UsernameAttribute == "" {
|
||||
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 == "" {
|
||||
configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute
|
||||
|
|
|
@ -16,7 +16,7 @@ func TestShouldRaiseErrorsWhenNoBackendProvided(t *testing.T) {
|
|||
|
||||
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`")
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfigura
|
|||
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() {
|
||||
suite.configuration.File.Path = ""
|
||||
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`")
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMo
|
|||
suite.configuration.File.Password.Memory = 8
|
||||
suite.configuration.File.Password.Parallelism = 2
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -98,35 +98,35 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWh
|
|||
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() {
|
||||
suite.configuration.File.Password.KeyLength = 1
|
||||
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")
|
||||
}
|
||||
|
||||
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() {
|
||||
suite.configuration.File.Password.SaltLength = -1
|
||||
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")
|
||||
}
|
||||
|
||||
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() {
|
||||
suite.configuration.File.Password.Algorithm = "bogus"
|
||||
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'")
|
||||
}
|
||||
|
||||
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() {
|
||||
suite.configuration.File.Password.Iterations = -1
|
||||
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")
|
||||
}
|
||||
|
||||
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() {
|
||||
suite.configuration.File.Password.Parallelism = -1
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -159,6 +159,7 @@ func (suite *LdapAuthenticationBackendSuite) SetupTest() {
|
|||
suite.validator = schema.NewStructValidator()
|
||||
suite.configuration = schema.AuthenticationBackendConfiguration{}
|
||||
suite.configuration.Ldap = &schema.LDAPAuthenticationBackendConfiguration{}
|
||||
suite.configuration.Ldap.Implementation = schema.LDAPImplementationCustom
|
||||
suite.configuration.Ldap.URL = "ldap://ldap"
|
||||
suite.configuration.Ldap.User = "user"
|
||||
suite.configuration.Ldap.Password = "password"
|
||||
|
@ -173,31 +174,38 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldValidateCompleteConfigura
|
|||
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() {
|
||||
suite.configuration.Ldap.URL = ""
|
||||
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")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProvided() {
|
||||
suite.configuration.Ldap.User = ""
|
||||
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")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNotProvided() {
|
||||
suite.configuration.Ldap.Password = ""
|
||||
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")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotProvided() {
|
||||
suite.configuration.Ldap.BaseDN = ""
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsernameAttribute() {
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAttribute() {
|
||||
suite.configuration.Ldap.UsernameAttribute = ""
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
require.Len(suite.T(), suite.validator.Errors(), 1)
|
||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a username attribute with `username_attribute`")
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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() {
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
|
@ -237,34 +250,40 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttrib
|
|||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() {
|
||||
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)
|
||||
}
|
||||
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() {
|
||||
suite.configuration.Ldap.UsersFilter = "uid={input}"
|
||||
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})")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoesNotContainEnclosingParenthesis() {
|
||||
suite.configuration.Ldap.GroupsFilter = "cn={input}"
|
||||
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})")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlaceholder() {
|
||||
suite.configuration.Ldap.UsersFilter = "(objectClass=person)"
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -289,3 +308,79 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {
|
|||
func TestLdapAuthenticationBackend(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ var validKeys = []string{
|
|||
"authentication_backend.refresh_interval",
|
||||
|
||||
// LDAP Authentication Backend Keys.
|
||||
"authentication_backend.ldap.implementation",
|
||||
"authentication_backend.ldap.url",
|
||||
"authentication_backend.ldap.skip_verify",
|
||||
"authentication_backend.ldap.base_dn",
|
||||
|
|
|
@ -37,6 +37,8 @@ const unableToRegisterSecurityKeyMessage = "Unable to register your security key
|
|||
const unableToResetPasswordMessage = "Unable to reset your password."
|
||||
const mfaValidationFailedMessage = "Authentication failed, please retry later."
|
||||
|
||||
const ldapPasswordComplexityCode = "0000052D"
|
||||
|
||||
const testInactivity = "10"
|
||||
const testRedirectionURL = "http://redirection.local"
|
||||
const testResultAllow = "allow"
|
||||
|
|
|
@ -2,6 +2,7 @@ package handlers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
@ -29,7 +30,13 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
|||
err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
version: '3'
|
||||
services:
|
||||
authelia-backend:
|
||||
volumes:
|
||||
- './ActiveDirectory/configuration.yml:/config/configuration.yml:ro'
|
||||
- './common/ssl:/config/ssl:ro'
|
|
@ -27,9 +27,18 @@ func (wds *WebDriverSession) doSuccessfullyCompletePasswordReset(ctx context.Con
|
|||
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)
|
||||
// then wait for the "email sent notification"
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,16 @@ func waitUntilAutheliaFrontendIsReady(dockerEnvironment *DockerEnvironment) erro
|
|||
[]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...")
|
||||
|
||||
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!")
|
||||
|
||||
return nil
|
||||
|
|
|
@ -65,13 +65,3 @@ sn: Dean
|
|||
uid: james
|
||||
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/
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
<br /> Once first factor is passed, you will need to follow the links to register a secret for the second
|
||||
factor.<br /> Authelia
|
||||
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.
|
||||
|
||||
<ul>
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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())
|
||||
}
|
|
@ -52,7 +52,7 @@ func (s *ResetPasswordScenario) TestShouldResetPassword() {
|
|||
s.verifyIsFirstFactorPage(ctx, s.T())
|
||||
|
||||
// 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
|
||||
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
|
||||
|
@ -65,7 +65,7 @@ func (s *ResetPasswordScenario) TestShouldResetPassword() {
|
|||
s.doLogout(ctx, s.T())
|
||||
|
||||
// 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() {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -25,7 +25,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, bypassAllSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
var dockerSuiteName = "Docker"
|
||||
|
||||
func init() {
|
||||
dockerEnvironment := NewDockerEnvironment([]string{
|
||||
"internal/suites/docker-compose.yml",
|
||||
|
@ -21,7 +23,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, dockerSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
@ -46,7 +48,7 @@ func init() {
|
|||
return dockerEnvironment.Down()
|
||||
}
|
||||
|
||||
GlobalRegistry.Register("Docker", Suite{
|
||||
GlobalRegistry.Register(dockerSuiteName, Suite{
|
||||
SetUp: setup,
|
||||
SetUpTimeout: 5 * time.Minute,
|
||||
OnSetupTimeout: displayAutheliaLogs,
|
||||
|
|
|
@ -23,7 +23,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, duoPushSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -20,13 +20,11 @@ func init() {
|
|||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
err := dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, haproxySuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -28,7 +28,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(haDockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(haDockerEnvironment, highAvailabilitySuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -21,13 +21,11 @@ func init() {
|
|||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
err := dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, ldapSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -25,7 +25,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, mariadbSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -25,7 +25,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, mysqlSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -26,7 +26,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, networkACLSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -22,7 +22,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, oneFactorOnlySuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -20,13 +20,11 @@ func init() {
|
|||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
err := dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, pathPrefixSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -25,7 +25,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, postgresSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -23,7 +23,7 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, shortTimeoutsSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -25,13 +25,11 @@ func init() {
|
|||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
err := dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, standaloneSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -20,13 +20,11 @@ func init() {
|
|||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
err := dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, traefikSuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -20,13 +20,11 @@ func init() {
|
|||
})
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
err := dockerEnvironment.Up()
|
||||
|
||||
if err != nil {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment)
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, traefik2SuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
|
|
|
@ -70,7 +70,11 @@ const ResetPasswordStep2 = function () {
|
|||
setFormDisabled(true);
|
||||
} catch (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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue