fix(authentication): follow ldap referrals (#3251)
This ensures we are able to follow referrals for LDAP password modify operations when permit_referrals is true. Co-authored-by: Amir Zarrinkafsh <nightah@me.com>pull/3282/head
parent
668ad38f20
commit
c7d992f341
|
@ -313,6 +313,10 @@ authentication_backend:
|
||||||
## 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
|
||||||
|
|
||||||
|
## Follow referrals returned by the server.
|
||||||
|
## This is especially useful for environments where read-only servers exist. Only implemented for write operations.
|
||||||
|
permit_referrals: false
|
||||||
|
|
||||||
## 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
|
||||||
## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||||
|
|
|
@ -32,6 +32,7 @@ authentication_backend:
|
||||||
group_name_attribute: cn
|
group_name_attribute: cn
|
||||||
mail_attribute: mail
|
mail_attribute: mail
|
||||||
display_name_attribute: displayName
|
display_name_attribute: displayName
|
||||||
|
permit_referrals: false
|
||||||
user: CN=admin,DC=example,DC=com
|
user: CN=admin,DC=example,DC=com
|
||||||
password: password
|
password: password
|
||||||
```
|
```
|
||||||
|
@ -95,7 +96,6 @@ Enables use of the LDAP StartTLS process which is not commonly used. You should
|
||||||
it. The initial connection will be over plain text, and Authelia will try to upgrade it with the LDAP server. LDAPS
|
it. The initial connection will be over plain text, and Authelia will try to upgrade it with the LDAP server. LDAPS
|
||||||
URL's are slightly more secure.
|
URL's are slightly more secure.
|
||||||
|
|
||||||
|
|
||||||
### tls
|
### tls
|
||||||
Controls the TLS connection validation process. You can see how to configure the tls
|
Controls the TLS connection validation process. You can see how to configure the tls
|
||||||
section [here](../index.md#tls-configuration).
|
section [here](../index.md#tls-configuration).
|
||||||
|
@ -117,13 +117,14 @@ user searches and [additional_groups_dn](#additional_groups_dn) for groups searc
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
required: no
|
required: yes
|
||||||
{: .label .label-config .label-green }
|
{: .label .label-config .label-red }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
The LDAP attribute that maps to the username in Authelia. The default value is dependent on the [implementation](#implementation),
|
_**Note:** While this option is required, an [implementation](#implementation) may set a default value implicitly
|
||||||
refer to the [attribute defaults](#attribute-defaults) for more information.
|
negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more information._
|
||||||
|
|
||||||
|
The LDAP attribute that maps to the username in Authelia.
|
||||||
|
|
||||||
### additional_users_dn
|
### additional_users_dn
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
|
@ -139,39 +140,103 @@ exactly which OU to get users from for either security or performance reasons. F
|
||||||
`ou=users,ou=people,dc=example,dc=com`. The default value is dependent on the [implementation](#implementation), refer
|
`ou=users,ou=people,dc=example,dc=com`. The default value is dependent on the [implementation](#implementation), refer
|
||||||
to the [attribute defaults](#attribute-defaults) for more information.
|
to the [attribute defaults](#attribute-defaults) for more information.
|
||||||
|
|
||||||
|
|
||||||
### users_filter
|
### users_filter
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: string
|
type: string
|
||||||
{: .label .label-config .label-purple }
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_**Note:** While this option is required, an [implementation](#implementation) may set a default value implicitly
|
||||||
|
negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more information._
|
||||||
|
|
||||||
|
The LDAP filter to narrow down which users are valid. This is important to set correctly as to exclude disabled users.
|
||||||
|
|
||||||
|
### group_name_attribute
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_**Note:** While this option is required, an [implementation](#implementation) may set a default value implicitly
|
||||||
|
negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more information._
|
||||||
|
|
||||||
|
The LDAP attribute that is used by Authelia to determine the group name.
|
||||||
|
|
||||||
|
### additional_groups_dn
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
required: no
|
required: no
|
||||||
{: .label .label-config .label-green }
|
{: .label .label-config .label-green }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
The LDAP filter to narrow down which users are valid. This is important to set correctly as to exclude disabled users.
|
|
||||||
The default value is dependent on the [implementation](#implementation), refer to the
|
|
||||||
[attribute defaults](#attribute-defaults) for more information.
|
|
||||||
|
|
||||||
### additional_groups_dn
|
|
||||||
Similar to [additional_users_dn](#additional_users_dn) but it applies to group searches.
|
Similar to [additional_users_dn](#additional_users_dn) but it applies to group searches.
|
||||||
|
|
||||||
### groups_filter
|
### groups_filter
|
||||||
Similar to [users_filter](#users_filter) but it applies to group searches. In order to include groups the memeber is not
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_**Note:** While this option is required, an [implementation](#implementation) may set a default value implicitly
|
||||||
|
negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more information._
|
||||||
|
|
||||||
|
Similar to [users_filter](#users_filter) but it applies to group searches. In order to include groups the member is not
|
||||||
a direct member of, but is a member of another group that is a member of those (i.e. recursive groups), you may try
|
a direct member of, but is a member of another group that is a member of those (i.e. recursive groups), you may try
|
||||||
using the following filter which is currently only tested against Microsoft Active Directory:
|
using the following filter which is currently only tested against Microsoft Active Directory:
|
||||||
|
|
||||||
`(&(member:1.2.840.113556.1.4.1941:={dn})(objectClass=group)(objectCategory=group))`
|
`(&(member:1.2.840.113556.1.4.1941:={dn})(objectClass=group)(objectCategory=group))`
|
||||||
|
|
||||||
### mail_attribute
|
### mail_attribute
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_**Note:** While this option is required, an [implementation](#implementation) may set a default value implicitly
|
||||||
|
negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more information._
|
||||||
|
|
||||||
The attribute to retrieve which contains the users email addresses. This is important for the device registration and
|
The attribute to retrieve which contains the users email addresses. This is important for the device registration and
|
||||||
password reset processes.
|
password reset processes.
|
||||||
The user must have an email address in order for Authelia to perform
|
|
||||||
identity verification when a user attempts to reset their password or
|
The user must have an email address in order for Authelia to perform identity verification when a user attempts to reset
|
||||||
register a second factor device.
|
their password or register a second factor device.
|
||||||
|
|
||||||
### display_name_attribute
|
### display_name_attribute
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: yes
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_**Note:** While this option is required, an [implementation](#implementation) may set a default value implicitly
|
||||||
|
negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more information._
|
||||||
|
|
||||||
The attribute to retrieve which is shown on the Web UI to the user when they log in.
|
The attribute to retrieve which is shown on the Web UI to the user when they log in.
|
||||||
|
|
||||||
|
### permit_referrals
|
||||||
|
<div markdown="1">
|
||||||
|
type: boolean
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: false
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-red }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Permits following referrals. This is useful if you have read-only servers in your architecture and thus require
|
||||||
|
referrals to be followed when performing write operations. This is only implemented for password modifications, if you
|
||||||
|
need this for searches please open a GitHub issue or contact us.
|
||||||
|
|
||||||
### user
|
### user
|
||||||
The distinguished name of the user paired with the password to bind with for lookup and password change operations.
|
The distinguished name of the user paired with the password to bind with for lookup and password change operations.
|
||||||
|
|
||||||
|
@ -191,20 +256,20 @@ search.
|
||||||
|
|
||||||
#### Users filter replacements
|
#### Users filter replacements
|
||||||
|
|
||||||
|Placeholder |Phase |Replacement |
|
| Placeholder | Phase | Replacement |
|
||||||
|:----------------------:|:-----:|:--------------------------------------------------------------:|
|
|:------------------------:|:-------:|:-------------------------------------:|
|
||||||
|{username_attribute} |startup|The configured username attribute |
|
| {username_attribute} | startup | The configured username attribute |
|
||||||
|{mail_attribute} |startup|The configured mail attribute |
|
| {mail_attribute} | startup | The configured mail attribute |
|
||||||
|{display_name_attribute}|startup|The configured display name attribute |
|
| {display_name_attribute} | startup | The configured display name attribute |
|
||||||
|{input} |search |The input into the username field |
|
| {input} | search | The input into the username field |
|
||||||
|
|
||||||
#### Groups filter replacements
|
#### Groups filter replacements
|
||||||
|
|
||||||
|Placeholder |Phase |Replacement |
|
| Placeholder | Phase | Replacement |
|
||||||
|:----------------------:|:-----:|:-------------------------------------------------------------------------:|
|
|:-----------:|:------:|:-------------------------------------------------------------------------:|
|
||||||
|{input} |search |The input into the username field |
|
| {input} | search | The input into the username field |
|
||||||
|{username} |search |The username from the profile lookup obtained from the username attribute |
|
| {username} | search | The username from the profile lookup obtained from the username attribute |
|
||||||
|{dn} |search |The distinguished name from the profile lookup |
|
| {dn} | search | The distinguished name from the profile lookup |
|
||||||
|
|
||||||
### Defaults
|
### Defaults
|
||||||
The below tables describes the current attribute defaults for each implementation.
|
The below tables describes the current attribute defaults for each implementation.
|
||||||
|
@ -213,10 +278,10 @@ The below tables describes the current attribute defaults for each implementatio
|
||||||
This table describes the attribute defaults for each implementation. i.e. the username_attribute is
|
This table describes the attribute defaults for each implementation. i.e. the username_attribute is
|
||||||
described by the Username column.
|
described by the Username column.
|
||||||
|
|
||||||
|Implementation |Username |Display Name|Mail |Group Name|
|
| Implementation | Username | Display Name | Mail | Group Name |
|
||||||
|:-------------:|:------------:|:----------:|:---:|:--------:|
|
|:---------------:|:--------------:|:------------:|:----:|:----------:|
|
||||||
|custom |n/a |displayName |mail |cn |
|
| custom | n/a | displayName | mail | cn |
|
||||||
|activedirectory|sAMAccountName|displayName |mail |cn |
|
| activedirectory | sAMAccountName | displayName | mail | cn |
|
||||||
|
|
||||||
#### Filter defaults
|
#### Filter defaults
|
||||||
The filters are probably the most important part to get correct when setting up LDAP.
|
The filters are probably the most important part to get correct when setting up LDAP.
|
||||||
|
@ -225,14 +290,15 @@ filters that accomplish this as an example (more examples would be appreciated).
|
||||||
userAccountControl filter checks that the account is not disabled and the pwdLastSet
|
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.
|
makes sure that value is not 0 which means the password requires changing at the next login.
|
||||||
|
|
||||||
|Implementation |Users Filter |Groups Filter|
|
| Implementation | Users Filter | Groups Filter |
|
||||||
|:-------------:|:------------:|:-----------:|
|
|:---------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------:|
|
||||||
|custom |n/a |n/a |
|
| custom | n/a | n/a |
|
||||||
|activedirectory|(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0)))|(&(member={dn})(objectClass=group)(objectCategory=group))|
|
| activedirectory | (&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))) | (&(member={dn})(objectClass=group)(objectCategory=group)) |
|
||||||
|
|
||||||
_**Note:**_ The Active Directory filter `(sAMAccountType=805306368)` is exactly the same as
|
_**Note:**_ The Active Directory filter `(sAMAccountType=805306368)` is exactly the same as
|
||||||
`(&(objectCategory=person)(objectClass=user))` except that the former is more performant, you can read more about this
|
`(&(objectCategory=person)(objectClass=user))` except that the former is more performant, you can read more about this
|
||||||
and other Active Directory filters on the [TechNet wiki](https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx).
|
and other Active Directory filters on the
|
||||||
|
[TechNet wiki](https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx).
|
||||||
|
|
||||||
## Refresh Interval
|
## Refresh Interval
|
||||||
This setting takes a [duration notation](../index.md#duration-notation-format) that sets the max frequency
|
This setting takes a [duration notation](../index.md#duration-notation-format) that sets the max frequency
|
||||||
|
|
|
@ -21,6 +21,11 @@ const (
|
||||||
ldapOIDPasswdModifyExtension = "1.3.6.1.4.1.4203.1.11.1" // http://oidref.com/1.3.6.1.4.1.4203.1.11.1
|
ldapOIDPasswdModifyExtension = "1.3.6.1.4.1.4203.1.11.1" // http://oidref.com/1.3.6.1.4.1.4203.1.11.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ldapAttributeUnicodePwd = "unicodePwd"
|
||||||
|
ldapAttributeUserPassword = "userPassword"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ldapPlaceholderInput = "{input}"
|
ldapPlaceholderInput = "{input}"
|
||||||
ldapPlaceholderDistinguishedName = "{dn}"
|
ldapPlaceholderDistinguishedName = "{dn}"
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
package authentication
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LDAPConnection interface representing a connection to the ldap.
|
|
||||||
type LDAPConnection interface {
|
|
||||||
Bind(username, password string) error
|
|
||||||
Close()
|
|
||||||
|
|
||||||
Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error)
|
|
||||||
Modify(modifyRequest *ldap.ModifyRequest) error
|
|
||||||
PasswordModify(pwdModifyRequest *ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error)
|
|
||||||
StartTLS(config *tls.Config) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// LDAPConnectionImpl the production implementation of an ldap connection.
|
|
||||||
type LDAPConnectionImpl struct {
|
|
||||||
conn *ldap.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLDAPConnectionImpl create a new ldap connection.
|
|
||||||
func NewLDAPConnectionImpl(conn *ldap.Conn) *LDAPConnectionImpl {
|
|
||||||
return &LDAPConnectionImpl{conn}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind binds ldap connection to a username/password.
|
|
||||||
func (lc *LDAPConnectionImpl) Bind(username, password string) error {
|
|
||||||
return lc.conn.Bind(username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes a ldap connection.
|
|
||||||
func (lc *LDAPConnectionImpl) Close() {
|
|
||||||
lc.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search searches a ldap server.
|
|
||||||
func (lc *LDAPConnectionImpl) Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
|
||||||
return lc.conn.Search(searchRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify modifies an ldap object.
|
|
||||||
func (lc *LDAPConnectionImpl) Modify(modifyRequest *ldap.ModifyRequest) error {
|
|
||||||
return lc.conn.Modify(modifyRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PasswordModify modifies an ldap objects password.
|
|
||||||
func (lc *LDAPConnectionImpl) PasswordModify(pwdModifyRequest *ldap.PasswordModifyRequest) error {
|
|
||||||
_, err := lc.conn.PasswordModify(pwdModifyRequest)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartTLS requests the LDAP server upgrades to TLS encryption.
|
|
||||||
func (lc *LDAPConnectionImpl) StartTLS(config *tls.Config) error {
|
|
||||||
return lc.conn.StartTLS(config)
|
|
||||||
}
|
|
|
@ -4,25 +4,15 @@ import (
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LDAPConnectionFactory an interface of factory of ldap connections.
|
// ProductionLDAPConnectionFactory the production implementation of an ldap connection factory.
|
||||||
type LDAPConnectionFactory interface {
|
type ProductionLDAPConnectionFactory struct{}
|
||||||
DialURL(addr string, opts ...ldap.DialOpt) (LDAPConnection, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LDAPConnectionFactoryImpl the production implementation of an ldap connection factory.
|
// NewProductionLDAPConnectionFactory create a concrete ldap connection factory.
|
||||||
type LDAPConnectionFactoryImpl struct{}
|
func NewProductionLDAPConnectionFactory() *ProductionLDAPConnectionFactory {
|
||||||
|
return &ProductionLDAPConnectionFactory{}
|
||||||
// NewLDAPConnectionFactoryImpl create a concrete ldap connection factory.
|
|
||||||
func NewLDAPConnectionFactoryImpl() *LDAPConnectionFactoryImpl {
|
|
||||||
return &LDAPConnectionFactoryImpl{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialURL creates a connection from an LDAP URL when successful.
|
// DialURL creates a connection from an LDAP URL when successful.
|
||||||
func (lcf *LDAPConnectionFactoryImpl) DialURL(addr string, opts ...ldap.DialOpt) (LDAPConnection, error) {
|
func (f *ProductionLDAPConnectionFactory) DialURL(addr string, opts ...ldap.DialOpt) (conn LDAPConnection, err error) {
|
||||||
conn, err := ldap.DialURL(addr, opts...)
|
return ldap.DialURL(addr, opts...)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/text/encoding/unicode"
|
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/v4/internal/logging"
|
"github.com/authelia/authelia/v4/internal/logging"
|
||||||
|
@ -18,11 +17,11 @@ import (
|
||||||
|
|
||||||
// LDAPUserProvider is a UserProvider that connects to LDAP servers like ActiveDirectory, OpenLDAP, OpenDJ, FreeIPA, etc.
|
// LDAPUserProvider is a UserProvider that connects to LDAP servers like ActiveDirectory, OpenLDAP, OpenDJ, FreeIPA, etc.
|
||||||
type LDAPUserProvider struct {
|
type LDAPUserProvider struct {
|
||||||
configuration schema.LDAPAuthenticationBackendConfiguration
|
config schema.LDAPAuthenticationBackendConfiguration
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
dialOpts []ldap.DialOpt
|
dialOpts []ldap.DialOpt
|
||||||
log *logrus.Logger
|
log *logrus.Logger
|
||||||
connectionFactory LDAPConnectionFactory
|
factory LDAPConnectionFactory
|
||||||
|
|
||||||
disableResetPassword bool
|
disableResetPassword bool
|
||||||
|
|
||||||
|
@ -43,21 +42,21 @@ type LDAPUserProvider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
|
// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
|
||||||
func NewLDAPUserProvider(configuration schema.AuthenticationBackendConfiguration, certPool *x509.CertPool) (provider *LDAPUserProvider) {
|
func NewLDAPUserProvider(config schema.AuthenticationBackendConfiguration, certPool *x509.CertPool) (provider *LDAPUserProvider) {
|
||||||
provider = newLDAPUserProvider(*configuration.LDAP, configuration.DisableResetPassword, certPool, nil)
|
provider = newLDAPUserProvider(*config.LDAP, config.DisableResetPassword, certPool, nil)
|
||||||
|
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration, disableResetPassword bool, certPool *x509.CertPool, factory LDAPConnectionFactory) (provider *LDAPUserProvider) {
|
func newLDAPUserProvider(config schema.LDAPAuthenticationBackendConfiguration, disableResetPassword bool, certPool *x509.CertPool, factory LDAPConnectionFactory) (provider *LDAPUserProvider) {
|
||||||
if configuration.TLS == nil {
|
if config.TLS == nil {
|
||||||
configuration.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
|
config.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConfig := utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool)
|
tlsConfig := utils.NewTLSConfig(config.TLS, tls.VersionTLS12, certPool)
|
||||||
|
|
||||||
var dialOpts = []ldap.DialOpt{
|
var dialOpts = []ldap.DialOpt{
|
||||||
ldap.DialWithDialer(&net.Dialer{Timeout: configuration.Timeout}),
|
ldap.DialWithDialer(&net.Dialer{Timeout: config.Timeout}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if tlsConfig != nil {
|
if tlsConfig != nil {
|
||||||
|
@ -65,15 +64,15 @@ func newLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfigura
|
||||||
}
|
}
|
||||||
|
|
||||||
if factory == nil {
|
if factory == nil {
|
||||||
factory = NewLDAPConnectionFactoryImpl()
|
factory = NewProductionLDAPConnectionFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
provider = &LDAPUserProvider{
|
provider = &LDAPUserProvider{
|
||||||
configuration: configuration,
|
config: config,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
dialOpts: dialOpts,
|
dialOpts: dialOpts,
|
||||||
log: logging.Logger(),
|
log: logging.Logger(),
|
||||||
connectionFactory: factory,
|
factory: factory,
|
||||||
disableResetPassword: disableResetPassword,
|
disableResetPassword: disableResetPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,77 +82,224 @@ func newLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfigura
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnection, error) {
|
// CheckUserPassword checks if provided password matches for the given user.
|
||||||
conn, err := p.connectionFactory.DialURL(p.configuration.URL, p.dialOpts...)
|
func (p *LDAPUserProvider) CheckUserPassword(inputUsername string, password string) (valid bool, err error) {
|
||||||
|
var (
|
||||||
|
conn, connUser LDAPConnection
|
||||||
|
profile *ldapUserProfile
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn, err = p.connect(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if profile, err = p.getUserProfile(conn, inputUsername); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if connUser, err = p.connectCustom(p.config.URL, profile.DN, password, p.config.StartTLS, p.dialOpts...); err != nil {
|
||||||
|
return false, fmt.Errorf("authentication failed. Cause: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer connUser.Close()
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDetails retrieve the groups a user belongs to.
|
||||||
|
func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, err error) {
|
||||||
|
var (
|
||||||
|
conn LDAPConnection
|
||||||
|
profile *ldapUserProfile
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn, err = p.connect(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if profile, err = p.getUserProfile(conn, username); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
filter string
|
||||||
|
searchRequest *ldap.SearchRequest
|
||||||
|
searchResult *ldap.SearchResult
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter, err = p.resolveGroupsFilter(username, profile); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create group filter for user '%s'. Cause: %w", username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for the users groups.
|
||||||
|
searchRequest = ldap.NewSearchRequest(
|
||||||
|
p.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||||
|
0, 0, false, filter, p.groupsAttributes, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if searchResult, err = p.search(conn, searchRequest); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]string, 0)
|
||||||
|
|
||||||
|
for _, res := range searchResult.Entries {
|
||||||
|
if len(res.Attributes) == 0 {
|
||||||
|
p.log.Warningf("No groups retrieved from LDAP for user %s", username)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append all values of the document. Normally there should be only one per document.
|
||||||
|
groups = append(groups, res.Attributes[0].Values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserDetails{
|
||||||
|
Username: profile.Username,
|
||||||
|
DisplayName: profile.DisplayName,
|
||||||
|
Emails: profile.Emails,
|
||||||
|
Groups: groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePassword update the password of the given user.
|
||||||
|
func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error) {
|
||||||
|
var (
|
||||||
|
conn LDAPConnection
|
||||||
|
profile *ldapUserProfile
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn, err = p.connect(); err != nil {
|
||||||
|
return fmt.Errorf("unable to update password. Cause: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if profile, err = p.getUserProfile(conn, username); err != nil {
|
||||||
|
return fmt.Errorf("unable to update password. Cause: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var controls []ldap.Control
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case p.supportExtensionPasswdModify:
|
||||||
|
pwdModifyRequest := ldap.NewPasswordModifyRequest(
|
||||||
|
profile.DN,
|
||||||
|
"",
|
||||||
|
password,
|
||||||
|
)
|
||||||
|
|
||||||
|
err = p.pwdModify(conn, pwdModifyRequest)
|
||||||
|
case p.config.Implementation == schema.LDAPImplementationActiveDirectory:
|
||||||
|
modifyRequest := ldap.NewModifyRequest(profile.DN, controls)
|
||||||
|
// 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, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", password))
|
||||||
|
modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded})
|
||||||
|
|
||||||
|
err = p.modify(conn, modifyRequest)
|
||||||
|
default:
|
||||||
|
modifyRequest := ldap.NewModifyRequest(profile.DN, controls)
|
||||||
|
modifyRequest.Replace(ldapAttributeUserPassword, []string{password})
|
||||||
|
|
||||||
|
err = p.modify(conn, modifyRequest)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("unable to update password. Cause: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.configuration.StartTLS {
|
return nil
|
||||||
if err := conn.StartTLS(p.tlsConfig); err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
|
func (p *LDAPUserProvider) connect() (LDAPConnection, error) {
|
||||||
|
return p.connectCustom(p.config.URL, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LDAPUserProvider) connectCustom(url, userDN, password string, startTLS bool, opts ...ldap.DialOpt) (conn LDAPConnection, err error) {
|
||||||
|
if conn, err = p.factory.DialURL(url, opts...); err != nil {
|
||||||
|
return nil, fmt.Errorf("dial failed with error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if startTLS {
|
||||||
|
if err = conn.StartTLS(p.tlsConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("starttls failed with error: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.Bind(userDN, password); err != nil {
|
if err = conn.Bind(userDN, password); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("bind failed with error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckUserPassword checks if provided password matches for the given user.
|
func (p *LDAPUserProvider) search(conn LDAPConnection, searchRequest *ldap.SearchRequest) (searchResult *ldap.SearchResult, err error) {
|
||||||
func (p *LDAPUserProvider) CheckUserPassword(inputUsername string, password string) (bool, error) {
|
searchResult, err = conn.Search(searchRequest)
|
||||||
conn, err := p.connect(p.configuration.User, p.configuration.Password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
if referral, ok := p.getReferral(err); ok {
|
||||||
|
if errReferral := p.searchReferral(referral, searchRequest, searchResult); errReferral != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return searchResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.config.PermitReferrals || len(searchResult.Referrals) == 0 {
|
||||||
|
return searchResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.searchReferrals(searchRequest, searchResult)
|
||||||
|
|
||||||
|
return searchResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LDAPUserProvider) searchReferral(referral string, searchRequest *ldap.SearchRequest, searchResult *ldap.SearchResult) (err error) {
|
||||||
|
var (
|
||||||
|
conn LDAPConnection
|
||||||
|
result *ldap.SearchResult
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn, err = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); err != nil {
|
||||||
|
p.log.Errorf("Failed to connect during referred search request (referred to %s): %v", referral, err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
profile, err := p.getUserProfile(conn, inputUsername)
|
if result, err = conn.Search(searchRequest); err != nil {
|
||||||
if err != nil {
|
p.log.Errorf("Failed to perform search operation during referred search request (referred to %s): %v", referral, err)
|
||||||
return false, err
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
userConn, err := p.connect(profile.DN, password)
|
if len(result.Entries) == 0 {
|
||||||
if err != nil {
|
return err
|
||||||
return false, fmt.Errorf("authentication failed. Cause: %w", err)
|
|
||||||
}
|
|
||||||
defer userConn.Close()
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LDAPUserProvider) ldapEscape(inputUsername string) string {
|
|
||||||
inputUsername = ldap.EscapeFilter(inputUsername)
|
|
||||||
for _, c := range specialLDAPRunes {
|
|
||||||
inputUsername = strings.ReplaceAll(inputUsername, string(c), fmt.Sprintf("\\%c", c))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return inputUsername
|
for i := 0; i < len(result.Entries); i++ {
|
||||||
}
|
if !ldapEntriesContainsEntry(result.Entries[i], searchResult.Entries) {
|
||||||
|
searchResult.Entries = append(searchResult.Entries, result.Entries[i])
|
||||||
type ldapUserProfile struct {
|
}
|
||||||
DN string
|
|
||||||
Emails []string
|
|
||||||
DisplayName string
|
|
||||||
Username string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *LDAPUserProvider) resolveUsersFilter(inputUsername string) (filter string) {
|
|
||||||
filter = p.configuration.UsersFilter
|
|
||||||
|
|
||||||
if p.usersFilterReplacementInput {
|
|
||||||
// The {input} placeholder is replaced by the users username input.
|
|
||||||
filter = strings.ReplaceAll(filter, ldapPlaceholderInput, p.ldapEscape(inputUsername))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.log.Tracef("Computed user filter is %s", filter)
|
return nil
|
||||||
|
|
||||||
return filter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername string) (*ldapUserProfile, error) {
|
func (p *LDAPUserProvider) searchReferrals(searchRequest *ldap.SearchRequest, searchResult *ldap.SearchResult) {
|
||||||
|
for i := 0; i < len(searchResult.Referrals); i++ {
|
||||||
|
_ = p.searchReferral(searchResult.Referrals[i], searchRequest, searchResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername string) (profile *ldapUserProfile, err error) {
|
||||||
userFilter := p.resolveUsersFilter(inputUsername)
|
userFilter := p.resolveUsersFilter(inputUsername)
|
||||||
|
|
||||||
// Search for the given username.
|
// Search for the given username.
|
||||||
|
@ -162,36 +308,37 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str
|
||||||
1, 0, false, userFilter, p.usersAttributes, nil,
|
1, 0, false, userFilter, p.usersAttributes, nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
sr, err := conn.Search(searchRequest)
|
var searchResult *ldap.SearchResult
|
||||||
if err != nil {
|
|
||||||
|
if searchResult, err = p.search(conn, searchRequest); err != nil {
|
||||||
return nil, fmt.Errorf("cannot find user DN of user '%s'. Cause: %w", inputUsername, err)
|
return nil, fmt.Errorf("cannot find user DN of user '%s'. Cause: %w", inputUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sr.Entries) == 0 {
|
if len(searchResult.Entries) == 0 {
|
||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sr.Entries) > 1 {
|
if len(searchResult.Entries) > 1 {
|
||||||
return nil, fmt.Errorf("multiple users %s found", inputUsername)
|
return nil, fmt.Errorf("multiple users %s found", inputUsername)
|
||||||
}
|
}
|
||||||
|
|
||||||
userProfile := ldapUserProfile{
|
userProfile := ldapUserProfile{
|
||||||
DN: sr.Entries[0].DN,
|
DN: searchResult.Entries[0].DN,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, attr := range sr.Entries[0].Attributes {
|
for _, attr := range searchResult.Entries[0].Attributes {
|
||||||
if attr.Name == p.configuration.DisplayNameAttribute {
|
if attr.Name == p.config.DisplayNameAttribute {
|
||||||
userProfile.DisplayName = attr.Values[0]
|
userProfile.DisplayName = attr.Values[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr.Name == p.configuration.MailAttribute {
|
if attr.Name == p.config.MailAttribute {
|
||||||
userProfile.Emails = attr.Values
|
userProfile.Emails = attr.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr.Name == p.configuration.UsernameAttribute {
|
if attr.Name == p.config.UsernameAttribute {
|
||||||
if len(attr.Values) != 1 {
|
if len(attr.Values) != 1 {
|
||||||
return nil, fmt.Errorf("user '%s' cannot have multiple value for attribute '%s'",
|
return nil, fmt.Errorf("user '%s' cannot have multiple value for attribute '%s'",
|
||||||
inputUsername, p.configuration.UsernameAttribute)
|
inputUsername, p.config.UsernameAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
userProfile.Username = attr.Values[0]
|
userProfile.Username = attr.Values[0]
|
||||||
|
@ -205,12 +352,25 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str
|
||||||
return &userProfile, nil
|
return &userProfile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *LDAPUserProvider) resolveUsersFilter(inputUsername string) (filter string) {
|
||||||
|
filter = p.config.UsersFilter
|
||||||
|
|
||||||
|
if p.usersFilterReplacementInput {
|
||||||
|
// The {input} placeholder is replaced by the username input.
|
||||||
|
filter = strings.ReplaceAll(filter, ldapPlaceholderInput, ldapEscape(inputUsername))
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log.Tracef("Detected user filter is %s", filter)
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ldapUserProfile) (filter string, err error) { //nolint:unparam
|
func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ldapUserProfile) (filter string, err error) { //nolint:unparam
|
||||||
filter = p.configuration.GroupsFilter
|
filter = p.config.GroupsFilter
|
||||||
|
|
||||||
if p.groupsFilterReplacementInput {
|
if p.groupsFilterReplacementInput {
|
||||||
// The {input} placeholder is replaced by the users username input.
|
// The {input} placeholder is replaced by the users username input.
|
||||||
filter = strings.ReplaceAll(p.configuration.GroupsFilter, ldapPlaceholderInput, p.ldapEscape(inputUsername))
|
filter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderInput, ldapEscape(inputUsername))
|
||||||
}
|
}
|
||||||
|
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
|
@ -228,98 +388,78 @@ func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ld
|
||||||
return filter, nil
|
return filter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDetails retrieve the groups a user belongs to.
|
func (p *LDAPUserProvider) modify(conn LDAPConnection, modifyRequest *ldap.ModifyRequest) (err error) {
|
||||||
func (p *LDAPUserProvider) GetDetails(inputUsername string) (*UserDetails, error) {
|
if err = conn.Modify(modifyRequest); err != nil {
|
||||||
conn, err := p.connect(p.configuration.User, p.configuration.Password)
|
var (
|
||||||
if err != nil {
|
referral string
|
||||||
return nil, err
|
ok bool
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
profile, err := p.getUserProfile(conn, inputUsername)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupsFilter, err := p.resolveGroupsFilter(inputUsername, profile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to create group filter for user '%s'. Cause: %w", inputUsername, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for the given username.
|
|
||||||
searchGroupRequest := ldap.NewSearchRequest(
|
|
||||||
p.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
|
||||||
0, 0, false, groupsFilter, p.groupsAttributes, nil,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sr, err := conn.Search(searchGroupRequest)
|
if referral, ok = p.getReferral(err); !ok {
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", inputUsername, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := make([]string, 0)
|
p.log.Debugf("Attempting Modify on referred URL %s", referral)
|
||||||
|
|
||||||
for _, res := range sr.Entries {
|
var (
|
||||||
if len(res.Attributes) == 0 {
|
connReferral LDAPConnection
|
||||||
p.log.Warningf("No groups retrieved from LDAP for user %s", inputUsername)
|
errReferral error
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append all values of the document. Normally there should be only one per document.
|
|
||||||
groups = append(groups, res.Attributes[0].Values...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UserDetails{
|
|
||||||
Username: profile.Username,
|
|
||||||
DisplayName: profile.DisplayName,
|
|
||||||
Emails: profile.Emails,
|
|
||||||
Groups: groups,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePassword update the password of the given user.
|
|
||||||
func (p *LDAPUserProvider) UpdatePassword(inputUsername string, newPassword string) error {
|
|
||||||
conn, err := p.connect(p.configuration.User, p.configuration.Password)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to update password. Cause: %w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
profile, err := p.getUserProfile(conn, inputUsername)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to update password. Cause: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case p.supportExtensionPasswdModify:
|
|
||||||
modifyRequest := ldap.NewPasswordModifyRequest(
|
|
||||||
profile.DN,
|
|
||||||
"",
|
|
||||||
newPassword,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err = conn.PasswordModify(modifyRequest)
|
if connReferral, errReferral = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); errReferral != nil {
|
||||||
case p.configuration.Implementation == schema.LDAPImplementationActiveDirectory:
|
p.log.Errorf("Failed to connect during referred modify request (referred to %s): %v", referral, errReferral)
|
||||||
modifyRequest := ldap.NewModifyRequest(profile.DN, nil)
|
|
||||||
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})
|
|
||||||
|
|
||||||
err = conn.Modify(modifyRequest)
|
return err
|
||||||
default:
|
|
||||||
modifyRequest := ldap.NewModifyRequest(profile.DN, nil)
|
|
||||||
modifyRequest.Replace("userPassword", []string{newPassword})
|
|
||||||
|
|
||||||
err = conn.Modify(modifyRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
defer connReferral.Close()
|
||||||
return fmt.Errorf("unable to update password. Cause: %w", err)
|
|
||||||
|
if errReferral = connReferral.Modify(modifyRequest); errReferral != nil {
|
||||||
|
p.log.Errorf("Failed to perform modify operation during referred modify request (referred to %s): %v", referral, errReferral)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LDAPUserProvider) pwdModify(conn LDAPConnection, pwdModifyRequest *ldap.PasswordModifyRequest) (err error) {
|
||||||
|
if _, err = conn.PasswordModify(pwdModifyRequest); err != nil {
|
||||||
|
var (
|
||||||
|
referral string
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if referral, ok = p.getReferral(err); !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log.Debugf("Attempting PwdModify ExOp (1.3.6.1.4.1.4203.1.11.1) on referred URL %s", referral)
|
||||||
|
|
||||||
|
var (
|
||||||
|
connReferral LDAPConnection
|
||||||
|
errReferral error
|
||||||
|
)
|
||||||
|
|
||||||
|
if connReferral, errReferral = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); errReferral != nil {
|
||||||
|
p.log.Errorf("Failed to connect during referred password modify request (referred to %s): %v", referral, errReferral)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer connReferral.Close()
|
||||||
|
|
||||||
|
if _, errReferral = connReferral.PasswordModify(pwdModifyRequest); errReferral != nil {
|
||||||
|
p.log.Errorf("Failed to perform modify operation during referred modify request (referred to %s): %v", referral, errReferral)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LDAPUserProvider) getReferral(err error) (referral string, ok bool) {
|
||||||
|
if !p.config.PermitReferrals {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ldapGetReferral(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,12 @@ import (
|
||||||
|
|
||||||
// StartupCheck implements the startup check provider interface.
|
// StartupCheck implements the startup check provider interface.
|
||||||
func (p *LDAPUserProvider) StartupCheck() (err error) {
|
func (p *LDAPUserProvider) StartupCheck() (err error) {
|
||||||
conn, err := p.connect(p.configuration.User, p.configuration.Password)
|
var (
|
||||||
if err != nil {
|
conn LDAPConnection
|
||||||
|
searchResult *ldap.SearchResult
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn, err = p.connect(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,17 +24,16 @@ func (p *LDAPUserProvider) StartupCheck() (err error) {
|
||||||
searchRequest := ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases,
|
searchRequest := ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases,
|
||||||
1, 0, false, "(objectClass=*)", []string{ldapSupportedExtensionAttribute}, nil)
|
1, 0, false, "(objectClass=*)", []string{ldapSupportedExtensionAttribute}, nil)
|
||||||
|
|
||||||
sr, err := conn.Search(searchRequest)
|
if searchResult, err = conn.Search(searchRequest); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sr.Entries) != 1 {
|
if len(searchResult.Entries) != 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate the attribute values to see what the server supports.
|
// Iterate the attribute values to see what the server supports.
|
||||||
for _, attr := range sr.Entries[0].Attributes {
|
for _, attr := range searchResult.Entries[0].Attributes {
|
||||||
if attr.Name == ldapSupportedExtensionAttribute {
|
if attr.Name == ldapSupportedExtensionAttribute {
|
||||||
p.log.Tracef("LDAP Supported Extension OIDs: %s", strings.Join(attr.Values, ", "))
|
p.log.Tracef("LDAP Supported Extension OIDs: %s", strings.Join(attr.Values, ", "))
|
||||||
|
|
||||||
|
@ -40,13 +43,11 @@ func (p *LDAPUserProvider) StartupCheck() (err error) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.supportExtensionPasswdModify && !p.disableResetPassword &&
|
if !p.supportExtensionPasswdModify && !p.disableResetPassword &&
|
||||||
p.configuration.Implementation != schema.LDAPImplementationActiveDirectory {
|
p.config.Implementation != schema.LDAPImplementationActiveDirectory {
|
||||||
p.log.Warn("Your LDAP server implementation may not support a method for password hashing " +
|
p.log.Warn("Your LDAP server implementation may not support a method for password hashing " +
|
||||||
"known to Authelia, it's strongly recommended you ensure your directory server hashes the password " +
|
"known to Authelia, it's strongly recommended you ensure your directory server hashes the password " +
|
||||||
"attribute when users reset their password via Authelia.")
|
"attribute when users reset their password via Authelia.")
|
||||||
|
@ -56,27 +57,27 @@ func (p *LDAPUserProvider) StartupCheck() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LDAPUserProvider) parseDynamicUsersConfiguration() {
|
func (p *LDAPUserProvider) parseDynamicUsersConfiguration() {
|
||||||
p.configuration.UsersFilter = strings.ReplaceAll(p.configuration.UsersFilter, "{username_attribute}", p.configuration.UsernameAttribute)
|
p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{username_attribute}", p.config.UsernameAttribute)
|
||||||
p.configuration.UsersFilter = strings.ReplaceAll(p.configuration.UsersFilter, "{mail_attribute}", p.configuration.MailAttribute)
|
p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{mail_attribute}", p.config.MailAttribute)
|
||||||
p.configuration.UsersFilter = strings.ReplaceAll(p.configuration.UsersFilter, "{display_name_attribute}", p.configuration.DisplayNameAttribute)
|
p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{display_name_attribute}", p.config.DisplayNameAttribute)
|
||||||
|
|
||||||
p.log.Tracef("Dynamically generated users filter is %s", p.configuration.UsersFilter)
|
p.log.Tracef("Dynamically generated users filter is %s", p.config.UsersFilter)
|
||||||
|
|
||||||
p.usersAttributes = []string{
|
p.usersAttributes = []string{
|
||||||
p.configuration.DisplayNameAttribute,
|
p.config.DisplayNameAttribute,
|
||||||
p.configuration.MailAttribute,
|
p.config.MailAttribute,
|
||||||
p.configuration.UsernameAttribute,
|
p.config.UsernameAttribute,
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.configuration.AdditionalUsersDN != "" {
|
if p.config.AdditionalUsersDN != "" {
|
||||||
p.usersBaseDN = p.configuration.AdditionalUsersDN + "," + p.configuration.BaseDN
|
p.usersBaseDN = p.config.AdditionalUsersDN + "," + p.config.BaseDN
|
||||||
} else {
|
} else {
|
||||||
p.usersBaseDN = p.configuration.BaseDN
|
p.usersBaseDN = p.config.BaseDN
|
||||||
}
|
}
|
||||||
|
|
||||||
p.log.Tracef("Dynamically generated users BaseDN is %s", p.usersBaseDN)
|
p.log.Tracef("Dynamically generated users BaseDN is %s", p.usersBaseDN)
|
||||||
|
|
||||||
if strings.Contains(p.configuration.UsersFilter, ldapPlaceholderInput) {
|
if strings.Contains(p.config.UsersFilter, ldapPlaceholderInput) {
|
||||||
p.usersFilterReplacementInput = true
|
p.usersFilterReplacementInput = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,26 +87,26 @@ func (p *LDAPUserProvider) parseDynamicUsersConfiguration() {
|
||||||
|
|
||||||
func (p *LDAPUserProvider) parseDynamicGroupsConfiguration() {
|
func (p *LDAPUserProvider) parseDynamicGroupsConfiguration() {
|
||||||
p.groupsAttributes = []string{
|
p.groupsAttributes = []string{
|
||||||
p.configuration.GroupNameAttribute,
|
p.config.GroupNameAttribute,
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.configuration.AdditionalGroupsDN != "" {
|
if p.config.AdditionalGroupsDN != "" {
|
||||||
p.groupsBaseDN = ldap.EscapeFilter(p.configuration.AdditionalGroupsDN + "," + p.configuration.BaseDN)
|
p.groupsBaseDN = ldap.EscapeFilter(p.config.AdditionalGroupsDN + "," + p.config.BaseDN)
|
||||||
} else {
|
} else {
|
||||||
p.groupsBaseDN = p.configuration.BaseDN
|
p.groupsBaseDN = p.config.BaseDN
|
||||||
}
|
}
|
||||||
|
|
||||||
p.log.Tracef("Dynamically generated groups BaseDN is %s", p.groupsBaseDN)
|
p.log.Tracef("Dynamically generated groups BaseDN is %s", p.groupsBaseDN)
|
||||||
|
|
||||||
if strings.Contains(p.configuration.GroupsFilter, ldapPlaceholderInput) {
|
if strings.Contains(p.config.GroupsFilter, ldapPlaceholderInput) {
|
||||||
p.groupsFilterReplacementInput = true
|
p.groupsFilterReplacementInput = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(p.configuration.GroupsFilter, ldapPlaceholderUsername) {
|
if strings.Contains(p.config.GroupsFilter, ldapPlaceholderUsername) {
|
||||||
p.groupsFilterReplacementUsername = true
|
p.groupsFilterReplacementUsername = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(p.configuration.GroupsFilter, ldapPlaceholderDistinguishedName) {
|
if strings.Contains(p.config.GroupsFilter, ldapPlaceholderDistinguishedName) {
|
||||||
p.groupsFilterReplacementDN = true
|
p.groupsFilterReplacementDN = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,8 @@ func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) {
|
||||||
ldapClient := newLDAPUserProvider(
|
ldapClient := newLDAPUserProvider(
|
||||||
schema.LDAPAuthenticationBackendConfiguration{
|
schema.LDAPAuthenticationBackendConfiguration{
|
||||||
URL: "ldap://127.0.0.1:389",
|
URL: "ldap://127.0.0.1:389",
|
||||||
|
User: "cn=admin,dc=example,dc=com",
|
||||||
|
Password: "password",
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
|
@ -40,7 +42,7 @@ func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) {
|
||||||
|
|
||||||
gomock.InOrder(dialURL, connBind)
|
gomock.InOrder(dialURL, connBind)
|
||||||
|
|
||||||
_, err := ldapClient.connect("cn=admin,dc=example,dc=com", "password")
|
_, err := ldapClient.connect()
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,8 @@ func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) {
|
||||||
ldapClient := newLDAPUserProvider(
|
ldapClient := newLDAPUserProvider(
|
||||||
schema.LDAPAuthenticationBackendConfiguration{
|
schema.LDAPAuthenticationBackendConfiguration{
|
||||||
URL: "ldaps://127.0.0.1:389",
|
URL: "ldaps://127.0.0.1:389",
|
||||||
|
User: "cn=admin,dc=example,dc=com",
|
||||||
|
Password: "password",
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
|
@ -70,41 +74,28 @@ func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) {
|
||||||
|
|
||||||
gomock.InOrder(dialURL, connBind)
|
gomock.InOrder(dialURL, connBind)
|
||||||
|
|
||||||
_, err := ldapClient.connect("cn=admin,dc=example,dc=com", "password")
|
_, err := ldapClient.connect()
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeSpecialCharsFromUserInput(t *testing.T) {
|
func TestEscapeSpecialCharsFromUserInput(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
mockFactory := NewMockLDAPConnectionFactory(ctrl)
|
|
||||||
|
|
||||||
ldapClient := newLDAPUserProvider(
|
|
||||||
schema.LDAPAuthenticationBackendConfiguration{
|
|
||||||
URL: "ldaps://127.0.0.1:389",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
mockFactory)
|
|
||||||
|
|
||||||
// No escape.
|
// No escape.
|
||||||
assert.Equal(t, "xyz", ldapClient.ldapEscape("xyz"))
|
assert.Equal(t, "xyz", ldapEscape("xyz"))
|
||||||
|
|
||||||
// Escape.
|
// Escape.
|
||||||
assert.Equal(t, "test\\,abc", ldapClient.ldapEscape("test,abc"))
|
assert.Equal(t, "test\\,abc", ldapEscape("test,abc"))
|
||||||
assert.Equal(t, "test\\5cabc", ldapClient.ldapEscape("test\\abc"))
|
assert.Equal(t, "test\\5cabc", ldapEscape("test\\abc"))
|
||||||
assert.Equal(t, "test\\2aabc", ldapClient.ldapEscape("test*abc"))
|
assert.Equal(t, "test\\2aabc", ldapEscape("test*abc"))
|
||||||
assert.Equal(t, "test \\28abc\\29", ldapClient.ldapEscape("test (abc)"))
|
assert.Equal(t, "test \\28abc\\29", ldapEscape("test (abc)"))
|
||||||
assert.Equal(t, "test\\#abc", ldapClient.ldapEscape("test#abc"))
|
assert.Equal(t, "test\\#abc", ldapEscape("test#abc"))
|
||||||
assert.Equal(t, "test\\+abc", ldapClient.ldapEscape("test+abc"))
|
assert.Equal(t, "test\\+abc", ldapEscape("test+abc"))
|
||||||
assert.Equal(t, "test\\<abc", ldapClient.ldapEscape("test<abc"))
|
assert.Equal(t, "test\\<abc", ldapEscape("test<abc"))
|
||||||
assert.Equal(t, "test\\>abc", ldapClient.ldapEscape("test>abc"))
|
assert.Equal(t, "test\\>abc", ldapEscape("test>abc"))
|
||||||
assert.Equal(t, "test\\;abc", ldapClient.ldapEscape("test;abc"))
|
assert.Equal(t, "test\\;abc", ldapEscape("test;abc"))
|
||||||
assert.Equal(t, "test\\\"abc", ldapClient.ldapEscape("test\"abc"))
|
assert.Equal(t, "test\\\"abc", ldapEscape("test\"abc"))
|
||||||
assert.Equal(t, "test\\=abc", ldapClient.ldapEscape("test=abc"))
|
assert.Equal(t, "test\\=abc", ldapEscape("test=abc"))
|
||||||
assert.Equal(t, "test\\,\\5c\\28abc\\29", ldapClient.ldapEscape("test,\\(abc)"))
|
assert.Equal(t, "test\\,\\5c\\28abc\\29", ldapEscape("test,\\(abc)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) {
|
func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) {
|
||||||
|
@ -306,7 +297,7 @@ func TestShouldReturnCheckServerConnectError(t *testing.T) {
|
||||||
Return(mockConn, errors.New("could not connect"))
|
Return(mockConn, errors.New("could not connect"))
|
||||||
|
|
||||||
err := ldapClient.StartupCheck()
|
err := ldapClient.StartupCheck()
|
||||||
assert.EqualError(t, err, "could not connect")
|
assert.EqualError(t, err, "dial failed with error: could not connect")
|
||||||
|
|
||||||
assert.False(t, ldapClient.supportExtensionPasswdModify)
|
assert.False(t, ldapClient.supportExtensionPasswdModify)
|
||||||
}
|
}
|
||||||
|
@ -1105,7 +1096,7 @@ func TestShouldCheckInvalidUserPassword(t *testing.T) {
|
||||||
valid, err := ldapClient.CheckUserPassword("john", "password")
|
valid, err := ldapClient.CheckUserPassword("john", "password")
|
||||||
|
|
||||||
assert.False(t, valid)
|
assert.False(t, valid)
|
||||||
require.EqualError(t, err, "authentication failed. Cause: invalid username or password")
|
require.EqualError(t, err, "authentication failed. Cause: bind failed with error: invalid username or password")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldCallStartTLSWhenEnabled(t *testing.T) {
|
func TestShouldCallStartTLSWhenEnabled(t *testing.T) {
|
||||||
|
@ -1215,8 +1206,8 @@ func TestShouldParseDynamicConfiguration(t *testing.T) {
|
||||||
|
|
||||||
assert.True(t, ldapClient.usersFilterReplacementInput)
|
assert.True(t, ldapClient.usersFilterReplacementInput)
|
||||||
|
|
||||||
assert.Equal(t, "(&(|(uid={input})(mail={input})(displayName={input}))(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!pwdLastSet=0))", ldapClient.configuration.UsersFilter)
|
assert.Equal(t, "(&(|(uid={input})(mail={input})(displayName={input}))(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!pwdLastSet=0))", ldapClient.config.UsersFilter)
|
||||||
assert.Equal(t, "(&(|(member={dn})(member={input})(member={username}))(objectClass=group))", ldapClient.configuration.GroupsFilter)
|
assert.Equal(t, "(&(|(member={dn})(member={input})(member={username}))(objectClass=group))", ldapClient.config.GroupsFilter)
|
||||||
assert.Equal(t, "ou=users,dc=example,dc=com", ldapClient.usersBaseDN)
|
assert.Equal(t, "ou=users,dc=example,dc=com", ldapClient.usersBaseDN)
|
||||||
assert.Equal(t, "ou=groups,dc=example,dc=com", ldapClient.groupsBaseDN)
|
assert.Equal(t, "ou=groups,dc=example,dc=com", ldapClient.groupsBaseDN)
|
||||||
}
|
}
|
||||||
|
@ -1342,5 +1333,5 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) {
|
||||||
gomock.InOrder(dialURL, connStartTLS)
|
gomock.InOrder(dialURL, connStartTLS)
|
||||||
|
|
||||||
_, err := ldapClient.GetDetails("john")
|
_, err := ldapClient.GetDetails("john")
|
||||||
assert.EqualError(t, err, "LDAP Result Code 200 \"Network Error\": ldap: already encrypted")
|
assert.EqualError(t, err, "starttls failed with error: LDAP Result Code 200 \"Network Error\": ldap: already encrypted")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package authentication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
ber "github.com/go-asn1-ber/asn1-ber"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ldapEntriesContainsEntry(needle *ldap.Entry, haystack []*ldap.Entry) bool {
|
||||||
|
for i := 0; i < len(haystack); i++ {
|
||||||
|
if haystack[i].DN == needle.DN {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ldapEscape(inputUsername string) string {
|
||||||
|
inputUsername = ldap.EscapeFilter(inputUsername)
|
||||||
|
for _, c := range specialLDAPRunes {
|
||||||
|
inputUsername = strings.ReplaceAll(inputUsername, string(c), fmt.Sprintf("\\%c", c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
func ldapGetReferral(err error) (referral string, ok bool) {
|
||||||
|
if !ldap.IsErrorWithCode(err, ldap.LDAPResultReferral) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := err.(type) {
|
||||||
|
case *ldap.Error:
|
||||||
|
if len(e.Packet.Children) < 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(e.Packet.Children[1].Children); i++ {
|
||||||
|
if e.Packet.Children[1].Children[i].Tag != ber.TagBitString || len(e.Packet.Children[1].Children[i].Children) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
referral, ok = e.Packet.Children[1].Children[i].Children[0].Value.(string)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return referral, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,29 @@
|
||||||
package authentication
|
package authentication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LDAPConnectionFactory an interface of factory of ldap connections.
|
||||||
|
type LDAPConnectionFactory interface {
|
||||||
|
DialURL(addr string, opts ...ldap.DialOpt) (LDAPConnection, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAPConnection interface representing a connection to the ldap.
|
||||||
|
type LDAPConnection interface {
|
||||||
|
Bind(username, password string) (err error)
|
||||||
|
Close()
|
||||||
|
StartTLS(config *tls.Config) (err error)
|
||||||
|
|
||||||
|
Search(searchRequest *ldap.SearchRequest) (searchResult *ldap.SearchResult, err error)
|
||||||
|
|
||||||
|
Modify(modifyRequest *ldap.ModifyRequest) (err error)
|
||||||
|
PasswordModify(pwdModifyRequest *ldap.PasswordModifyRequest) (result *ldap.PasswordModifyResult, err error)
|
||||||
|
}
|
||||||
|
|
||||||
// UserDetails represent the details retrieved for a given user.
|
// UserDetails represent the details retrieved for a given user.
|
||||||
type UserDetails struct {
|
type UserDetails struct {
|
||||||
Username string
|
Username string
|
||||||
|
@ -7,3 +31,12 @@ type UserDetails struct {
|
||||||
Emails []string
|
Emails []string
|
||||||
Groups []string
|
Groups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ldapUserProfile struct {
|
||||||
|
DN string
|
||||||
|
Emails []string
|
||||||
|
DisplayName string
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
var utf16LittleEndian = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
|
||||||
|
|
|
@ -313,6 +313,10 @@ authentication_backend:
|
||||||
## 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
|
||||||
|
|
||||||
|
## Follow referrals returned by the server.
|
||||||
|
## This is especially useful for environments where read-only servers exist. Only implemented for write operations.
|
||||||
|
permit_referrals: false
|
||||||
|
|
||||||
## 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
|
||||||
## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
|
||||||
|
|
|
@ -26,6 +26,8 @@ type LDAPAuthenticationBackendConfiguration struct {
|
||||||
MailAttribute string `koanf:"mail_attribute"`
|
MailAttribute string `koanf:"mail_attribute"`
|
||||||
DisplayNameAttribute string `koanf:"display_name_attribute"`
|
DisplayNameAttribute string `koanf:"display_name_attribute"`
|
||||||
|
|
||||||
|
PermitReferrals bool `koanf:"permit_referrals"`
|
||||||
|
|
||||||
User string `koanf:"user"`
|
User string `koanf:"user"`
|
||||||
Password string `koanf:"password"`
|
Password string `koanf:"password"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ var Keys = []string{
|
||||||
"authentication_backend.ldap.username_attribute",
|
"authentication_backend.ldap.username_attribute",
|
||||||
"authentication_backend.ldap.mail_attribute",
|
"authentication_backend.ldap.mail_attribute",
|
||||||
"authentication_backend.ldap.display_name_attribute",
|
"authentication_backend.ldap.display_name_attribute",
|
||||||
|
"authentication_backend.ldap.permit_referrals",
|
||||||
"authentication_backend.ldap.user",
|
"authentication_backend.ldap.user",
|
||||||
"authentication_backend.ldap.password",
|
"authentication_backend.ldap.password",
|
||||||
"authentication_backend.file.path",
|
"authentication_backend.file.path",
|
||||||
|
|
|
@ -124,9 +124,7 @@ func validateLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendC
|
||||||
|
|
||||||
if config.TLS == nil {
|
if config.TLS == nil {
|
||||||
config.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
|
config.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
|
||||||
}
|
} else if config.TLS.MinimumVersion == "" {
|
||||||
|
|
||||||
if config.TLS.MinimumVersion == "" {
|
|
||||||
config.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion
|
config.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue