[FEATURE] Automatic Profile Refresh - LDAP (#912)
* [FIX] LDAP Not Checking for Updated Groups * refactor handlers verifyFromSessionCookie * refactor authorizer selectMatchingObjectRules * refactor authorizer isDomainMatching * add authorizer URLHasGroupSubjects method * add user provider ProviderType method * update tests * check for new LDAP groups and update session when: * user provider type is LDAP * authorization is forbidden * URL has rule with group subjects * Implement Refresh Interval * add default values for LDAP user provider * add default for refresh interval * add schema validator for refresh interval * add various tests * rename hasUserBeenInactiveLongEnough to hasUserBeenInactiveTooLong * use Authelia ctx clock * add check to determine if user is deleted, if so destroy the * make ldap user not found error a const * implement GetRefreshSettings in mock * Use user not found const with FileProvider * comment exports * use ctx.Clock instead of time pkg * add debug logging * use ptr to reference userSession so we don't have to retrieve it again * add documenation * add check for 0 refresh interval to reduce CPU cost * remove badly copied debug msg * add group change delta message * add SliceStringDelta * refactor ldap refresh to use the new func * improve delta add/remove log message * fix incorrect logic in SliceStringDelta * add tests to SliceStringDelta * add always config option * add tests for always config option * update docs * apply suggestions from code review Co-Authored-By: Amir Zarrinkafsh <nightah@me.com> * complete mocks and fix an old one * show warning when LDAP details failed to update for an unknown reason * golint fix * actually fix existing mocks * use mocks for LDAP refresh testing * use mocks for LDAP refresh testing for both added and removed groups * use test mock to verify disabled refresh behaviour * add information to threat model * add time const for default Unix() value * misc adjustments to mocks * Suggestions from code review * requested changes * update emails * docs updates * test updates * misc * golint fix * set debug for dev testing * misc docs and logging updates * misc grammar/spelling * use built function for VerifyGet * fix reviewdog suggestions * requested changes * Apply suggestions from code review Co-authored-by: Amir Zarrinkafsh <nightah@me.com> Co-authored-by: Clément Michaud <clement.michaud34@gmail.com>pull/975/head
parent
99bb782708
commit
3f374534ab
21
README.md
21
README.md
|
@ -103,15 +103,28 @@ Authelia takes security very seriously. We follow the rule of
|
||||||
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
||||||
encourage the community to as well.
|
encourage the community to as well.
|
||||||
|
|
||||||
|
If you discover a vulnerability in Authelia, please first contact one of the maintainers privately
|
||||||
If you discover a vulnerability in Authelia, please first contact **clems4ever** on
|
either via [Matrix](#matrix) or [email](#email) as described in the [contact options](#contact-options) below.
|
||||||
[Matrix](https://riot.im/app/#/room/#authelia:matrix.org) or by
|
|
||||||
[email](mailto:clement.michaud34@gmail.com).
|
|
||||||
|
|
||||||
For details about security measures implemented in Authelia, please follow
|
For details about security measures implemented in Authelia, please follow
|
||||||
this [link](https://docs.authelia.com/security/measures.html) and for reading about
|
this [link](https://docs.authelia.com/security/measures.html) and for reading about
|
||||||
the threat model follow this [link](https://docs.authelia.com/security/threat-model.html).
|
the threat model follow this [link](https://docs.authelia.com/security/threat-model.html).
|
||||||
|
|
||||||
|
### Contact Options
|
||||||
|
|
||||||
|
#### Matrix
|
||||||
|
|
||||||
|
Join the [Matrix Room](https://riot.im/app/#/room/#authelia:matrix.org) and locate one of the maintainers.
|
||||||
|
You can identify them as they are the room administrators. Alternatively you can just ask for one of the
|
||||||
|
maintainers. Once you've made contact we ask you privately message the maintainer to communicate the vulnerability.
|
||||||
|
|
||||||
|
#### Email
|
||||||
|
|
||||||
|
You can contact any of the maintainers for security vulnerability related issues by emailing
|
||||||
|
[security@authelia.com](mailto:security@authelia.com). This email is strictly reserved for security and vulnerability
|
||||||
|
disclosure related matters. If you need to contact us for another reason please use [Matrix](#matrix) or
|
||||||
|
[team@authelia.com](mailto:team@authelia.com).
|
||||||
|
|
||||||
## Breaking changes
|
## Breaking changes
|
||||||
|
|
||||||
See [BREAKING](./BREAKING.md).
|
See [BREAKING](./BREAKING.md).
|
||||||
|
|
20
SECURITY.md
20
SECURITY.md
|
@ -4,10 +4,24 @@ Authelia takes security very seriously. We follow the rule of
|
||||||
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
||||||
encourage the community to as well.
|
encourage the community to as well.
|
||||||
|
|
||||||
If you discover a vulnerability in Authelia, please first contact **clems4ever** on
|
If you discover a vulnerability in Authelia, please first contact one of the maintainers privately
|
||||||
[Matrix](https://riot.im/app/#/room/#authelia:matrix.org) or by
|
either via [Matrix](#matrix) or [email](#email) as described in the [contact options](#contact-options) below.
|
||||||
[email](mailto:clement.michaud34@gmail.com).
|
|
||||||
|
|
||||||
For details about security measures implemented in Authelia, please follow
|
For details about security measures implemented in Authelia, please follow
|
||||||
this [link](https://docs.authelia.com/security/measures.html) and for reading about
|
this [link](https://docs.authelia.com/security/measures.html) and for reading about
|
||||||
the threat model follow this [link](https://docs.authelia.com/security/threat-model.html).
|
the threat model follow this [link](https://docs.authelia.com/security/threat-model.html).
|
||||||
|
|
||||||
|
## Contact Options
|
||||||
|
|
||||||
|
### Matrix
|
||||||
|
|
||||||
|
Join the [Matrix Room](https://riot.im/app/#/room/#authelia:matrix.org) and locate one of the maintainers.
|
||||||
|
You can identify them as they are the room administrators. Alternatively you can just ask for one of the
|
||||||
|
maintainers. Once you've made contact we ask you privately message the maintainer to communicate the vulnerability.
|
||||||
|
|
||||||
|
### Email
|
||||||
|
|
||||||
|
You can contact any of the maintainers for security vulnerability related issues by emailing
|
||||||
|
[security@authelia.com](mailto:security@authelia.com). This email is strictly reserved for security and vulnerability
|
||||||
|
disclosure related matters. If you need to contact us for another reason please use [Matrix](#matrix) or
|
||||||
|
[team@authelia.com](mailto:security@authelia.com).
|
|
@ -79,6 +79,15 @@ authentication_backend:
|
||||||
# Disable both the HTML element and the API for reset password functionality
|
# Disable both the HTML element and the API for reset password functionality
|
||||||
disable_reset_password: false
|
disable_reset_password: false
|
||||||
|
|
||||||
|
# The amount of time to wait before we refresh data from the authentication backend. Uses duration notation.
|
||||||
|
# To disable this feature set it to 'disable', this will slightly reduce security because for Authelia, users
|
||||||
|
# will always belong to groups they belonged to at the time of login even if they have been removed from them in LDAP.
|
||||||
|
# To force update on every request you can set this to '0' or 'always', this will increase processor demand.
|
||||||
|
# See the below documentation for more information.
|
||||||
|
# Duration Notation docs: https://docs.authelia.com/configuration/index.html#duration-notation-format
|
||||||
|
# Refresh Interval docs: https://docs.authelia.com/configuration/authentication/ldap.html#refresh-interval
|
||||||
|
refresh_interval: 5m
|
||||||
|
|
||||||
# LDAP backend configuration.
|
# LDAP backend configuration.
|
||||||
#
|
#
|
||||||
# This backend allows Authelia to be scaled to more
|
# This backend allows Authelia to be scaled to more
|
||||||
|
|
|
@ -17,7 +17,21 @@ file in the configuration file.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
|
# Disable both the HTML element and the API for reset password functionality
|
||||||
disable_reset_password: false
|
disable_reset_password: false
|
||||||
|
|
||||||
|
# File backend configuration.
|
||||||
|
#
|
||||||
|
# With this backend, the users database is stored in a file
|
||||||
|
# which is updated when users reset their passwords.
|
||||||
|
# Therefore, this backend is meant to be used in a dev environment
|
||||||
|
# and not in production since it prevents Authelia to be scaled to
|
||||||
|
# more than one instance. The options under 'password' have sane
|
||||||
|
# defaults, and as it has security implications it is highly recommended
|
||||||
|
# you leave the default values. Before considering changing these settings
|
||||||
|
# please read the docs page below:
|
||||||
|
# https://docs.authelia.com/configuration/authentication/file.html#password-hash-algorithm-tuning
|
||||||
|
|
||||||
file:
|
file:
|
||||||
path: /var/lib/authelia/users.yml
|
path: /var/lib/authelia/users.yml
|
||||||
password:
|
password:
|
||||||
|
|
|
@ -16,7 +16,18 @@ Configuration of the LDAP backend is done as follows
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
|
# Disable both the HTML element and the API for reset password functionality
|
||||||
disable_reset_password: false
|
disable_reset_password: false
|
||||||
|
|
||||||
|
# The amount of time to wait before we refresh data from the authentication backend. Uses duration notation.
|
||||||
|
# To disable this feature set it to 'disable', this will slightly reduce security because for Authelia, users
|
||||||
|
# will always belong to groups they belonged to at the time of login even if they have been removed from them in LDAP.
|
||||||
|
# To force update on every request you can set this to '0' or 'always', this will increase processor demand.
|
||||||
|
# See the below documentation for more information.
|
||||||
|
# Duration Notation docs: https://docs.authelia.com/configuration/index.html#duration-notation-format
|
||||||
|
# Refresh Interval docs: https://docs.authelia.com/configuration/authentication/ldap.html#refresh-interval
|
||||||
|
refresh_interval: 5m
|
||||||
|
|
||||||
ldap:
|
ldap:
|
||||||
# The url to the ldap server. Scheme can be ldap:// or ldaps://
|
# The url to the ldap server. Scheme can be ldap:// or ldaps://
|
||||||
url: ldap://127.0.0.1
|
url: ldap://127.0.0.1
|
||||||
|
@ -89,6 +100,25 @@ The user must have an email address in order for Authelia to perform
|
||||||
identity verification when a user attempts to reset their password or
|
identity verification when a user attempts to reset their password or
|
||||||
register a second factor device.
|
register a second factor device.
|
||||||
|
|
||||||
|
|
||||||
|
## Refresh Interval
|
||||||
|
|
||||||
|
This setting takes a [duration notation](../index.md#duration-notation-format) that sets the max frequency
|
||||||
|
for how often Authelia contacts the backend to verify the user still exists and that the groups stored
|
||||||
|
in the session are up to date. This allows us to destroy sessions when the user no longer matches the
|
||||||
|
user_filter, or deny access to resources as they are removed from groups.
|
||||||
|
|
||||||
|
In addition to the duration notation, you may provide the value `always` or `disable`. Setting to `always`
|
||||||
|
is the same as setting it to 0 which will refresh on every request, `disable` turns the feature off, which is
|
||||||
|
not recommended. This completely prevents Authelia from refreshing this information, and it would only be
|
||||||
|
refreshed when the user session gets destroyed by other means like inactivity, session expiration or logging
|
||||||
|
out and in.
|
||||||
|
|
||||||
|
This value can be any value including 0, setting it to 0 would automatically refresh the session on
|
||||||
|
every single request. This means Authelia will have to contact the LDAP backend every time an element
|
||||||
|
on a page loads which could be substantially costly. It's a trade-off between load and security that
|
||||||
|
you should adapt according to your own security policy.
|
||||||
|
|
||||||
## Important notes
|
## Important notes
|
||||||
|
|
||||||
Users must be uniquely identified by an attribute, this attribute must obviously contain a single value and
|
Users must be uniquely identified by an attribute, this attribute must obviously contain a single value and
|
||||||
|
@ -102,4 +132,3 @@ unique identifier for your users.
|
||||||
## Loading a password from a secret instead of inside the configuration
|
## Loading a password from a secret instead of inside the configuration
|
||||||
|
|
||||||
Password can also be defined using a [secret](../secrets.md).
|
Password can also be defined using a [secret](../secrets.md).
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,24 @@ Authelia takes security very seriously. We follow the rule of
|
||||||
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
||||||
encourage the community to as well.
|
encourage the community to as well.
|
||||||
|
|
||||||
|
If you discover a vulnerability in Authelia, please first contact one of the maintainers privately
|
||||||
|
either via [Matrix](#matrix) or [email](#email) as described in the [contact options](#contact-options) below.
|
||||||
|
|
||||||
If you discover a vulnerability in Authelia, please first contact **clems4ever** on
|
For details about security measures implemented in Authelia, please follow
|
||||||
[Matrix](https://riot.im/app/#/room/#authelia:matrix.org) or by
|
this [link](https://docs.authelia.com/security/measures.html) and for reading about
|
||||||
[email](mailto:clement.michaud34@gmail.com).
|
the threat model follow this [link](https://docs.authelia.com/security/threat-model.html).
|
||||||
|
|
||||||
|
## Contact Options
|
||||||
|
|
||||||
|
### Matrix
|
||||||
|
|
||||||
|
Join the [Matrix Room](https://riot.im/app/#/room/#authelia:matrix.org) and locate one of the maintainers.
|
||||||
|
You can identify them as they are the room administrators. Alternatively you can just ask for one of the
|
||||||
|
maintainers. Once you've made contact we ask you privately message the maintainer to communicate the vulnerability.
|
||||||
|
|
||||||
|
### Email
|
||||||
|
|
||||||
|
You can contact any of the maintainers for security vulnerability related issues by emailing
|
||||||
|
[security@authelia.com](mailto:security@authelia.com). This email is strictly reserved for security and vulnerability
|
||||||
|
disclosure related matters. If you need to contact us for another reason please use [Matrix](#matrix) or
|
||||||
|
[team@authelia.com](mailto:security@authelia.com).
|
||||||
|
|
|
@ -45,6 +45,16 @@ Lastly Authelia's implementation of Argon2id is highly tunable. You can tune the
|
||||||
used, iterations (time), parallelism, and memory usage. To read more about this please read how to
|
used, iterations (time), parallelism, and memory usage. To read more about this please read how to
|
||||||
[configure](../configuration/authentication/file.md) file authentication.
|
[configure](../configuration/authentication/file.md) file authentication.
|
||||||
|
|
||||||
|
## User profile and group membership always kept up-to-date (LDAP authentication provider)
|
||||||
|
|
||||||
|
Authelia by default refreshes the user's profile and membership every 5 minutes. Additionally, it
|
||||||
|
will invalidate any session where the user could not be retrieved from LDAP based on the user filter, for
|
||||||
|
example if they were deleted or disabled provided the user filter is set correctly. These updates occur when
|
||||||
|
a user accesses a resource protected by Authelia.
|
||||||
|
|
||||||
|
These protections can be [tuned](../configuration/authentication/ldap.md) according to your security policy
|
||||||
|
by changing refresh_interval, however we believe that 5 minutes is a fairly safe interval.
|
||||||
|
|
||||||
## Notifier security measures (SMTP)
|
## Notifier security measures (SMTP)
|
||||||
|
|
||||||
By default the SMTP Notifier implementation does not allow connections that are not secure.
|
By default the SMTP Notifier implementation does not allow connections that are not secure.
|
||||||
|
|
|
@ -28,6 +28,7 @@ If properly configured, Authelia guarantees the following for security of your u
|
||||||
* Prevention against session fixation by regenerating a new session after each privilege elevation.
|
* Prevention against session fixation by regenerating a new session after each privilege elevation.
|
||||||
* Prevention against LDAP injection by following OWASP recommendations regarding valid input characters (https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html).
|
* Prevention against LDAP injection by following OWASP recommendations regarding valid input characters (https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html).
|
||||||
* Connections between Authelia and thirdparty components like mail server, database, cache and LDAP server can be made over TLS to protect against man-in-the-middle attacks from within the infrastructure.
|
* Connections between Authelia and thirdparty components like mail server, database, cache and LDAP server can be made over TLS to protect against man-in-the-middle attacks from within the infrastructure.
|
||||||
|
* Validation of user session group memberships gets refreshed regularly from the authentication backend (LDAP only).
|
||||||
|
|
||||||
## Potential future guarantees
|
## Potential future guarantees
|
||||||
|
|
||||||
|
@ -38,6 +39,8 @@ If properly configured, Authelia guarantees the following for security of your u
|
||||||
* Securely transmit authentication data to backends (OAuth2 with bearer tokens).
|
* Securely transmit authentication data to backends (OAuth2 with bearer tokens).
|
||||||
* Protect secrets stored in DB with encryption to prevent secrets leak by DB exfiltration.
|
* Protect secrets stored in DB with encryption to prevent secrets leak by DB exfiltration.
|
||||||
* Least privilege on LDAP binding operations (currently administrative user is used to bind while it could be anonymous).
|
* Least privilege on LDAP binding operations (currently administrative user is used to bind while it could be anonymous).
|
||||||
|
* Extend the check of user group memberships to authentication backends other than LDAP (File currently).
|
||||||
|
* Invalidate user session after profile or membership has changed in order to drop remaining privileges on the fly.
|
||||||
|
|
||||||
## Trusted environment
|
## Trusted environment
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package authentication
|
package authentication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
// Level is the type representing a level of authentication.
|
// Level is the type representing a level of authentication.
|
||||||
type Level int
|
type Level int
|
||||||
|
|
||||||
|
@ -47,6 +51,9 @@ const (
|
||||||
// HashingPossibleSaltCharacters represents valid hashing runes.
|
// HashingPossibleSaltCharacters represents valid hashing runes.
|
||||||
var HashingPossibleSaltCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/")
|
var HashingPossibleSaltCharacters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/")
|
||||||
|
|
||||||
|
// ErrUserNotFound indicates the user wasn't found in the authentication backend.
|
||||||
|
var ErrUserNotFound = errors.New("user not found")
|
||||||
|
|
||||||
const sha512 = "sha512"
|
const sha512 = "sha512"
|
||||||
|
|
||||||
const testPassword = "my;secure*password"
|
const testPassword = "my;secure*password"
|
||||||
|
|
|
@ -48,7 +48,7 @@ func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnecti
|
||||||
if url.Scheme == "ldaps" {
|
if url.Scheme == "ldaps" {
|
||||||
logging.Logger().Trace("LDAP client starts a TLS session")
|
logging.Logger().Trace("LDAP client starts a TLS session")
|
||||||
conn, err := p.connectionFactory.DialTLS("tcp", url.Host, &tls.Config{
|
conn, err := p.connectionFactory.DialTLS("tcp", url.Host, &tls.Config{
|
||||||
InsecureSkipVerify: p.configuration.SkipVerify, //nolint:gosec
|
InsecureSkipVerify: p.configuration.SkipVerify, //nolint:gosec // This is a configurable option, is desirable in some situations and is off by default
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -150,7 +150,7 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sr.Entries) == 0 {
|
if len(sr.Entries) == 0 {
|
||||||
return nil, fmt.Errorf("No user %s found", inputUsername)
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sr.Entries) > 1 {
|
if len(sr.Entries) > 1 {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
gomock "github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
|
|
@ -67,13 +67,10 @@ func selectMatchingObjectRules(rules []schema.ACLRule, object Object) []schema.A
|
||||||
selectedRules := []schema.ACLRule{}
|
selectedRules := []schema.ACLRule{}
|
||||||
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
for _, domain := range rule.Domains {
|
if isDomainMatching(object.Domain, rule.Domains) && isPathMatching(object.Path, rule.Resources) {
|
||||||
if isDomainMatching(object.Domain, domain) &&
|
|
||||||
isPathMatching(object.Path, rule.Resources) {
|
|
||||||
selectedRules = append(selectedRules, rule)
|
selectedRules = append(selectedRules, rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return selectedRules
|
return selectedRules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,3 +128,18 @@ func (p *Authorizer) GetRequiredLevel(subject Subject, requestURL url.URL) Level
|
||||||
|
|
||||||
return PolicyToLevel(p.configuration.DefaultPolicy)
|
return PolicyToLevel(p.configuration.DefaultPolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsURLMatchingRuleWithGroupSubjects returns true if the request has at least one
|
||||||
|
// matching ACL with a subject of type group attached to it, otherwise false.
|
||||||
|
func (p *Authorizer) IsURLMatchingRuleWithGroupSubjects(requestURL url.URL) (hasGroupSubjects bool) {
|
||||||
|
for _, rule := range p.configuration.Rules {
|
||||||
|
if isDomainMatching(requestURL.Hostname(), rule.Domains) && isPathMatching(requestURL.Path, rule.Resources) {
|
||||||
|
for _, subjectRule := range rule.Subjects {
|
||||||
|
if strings.HasPrefix(subjectRule, groupPrefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@ package authorization
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
func isDomainMatching(domain string, domainRule string) bool {
|
func isDomainMatching(domain string, domainRules []string) bool {
|
||||||
if domain == domainRule { // if domain matches exactly
|
for _, domainRule := range domainRules {
|
||||||
|
if domain == domainRule {
|
||||||
return true
|
return true
|
||||||
} else if strings.HasPrefix(domainRule, "*.") && strings.HasSuffix(domain, domainRule[1:]) {
|
} else if strings.HasPrefix(domainRule, "*.") && strings.HasSuffix(domain, domainRule[1:]) {
|
||||||
// If domain pattern starts with *, it's a multi domain pattern.
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,20 +6,31 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDomainMatcher(t *testing.T) {
|
func TestShouldMatchACLWithSingleDomain(t *testing.T) {
|
||||||
assert.True(t, isDomainMatching("example.com", "example.com"))
|
assert.True(t, isDomainMatching("example.com", []string{"example.com"}))
|
||||||
|
|
||||||
assert.False(t, isDomainMatching("example.com", "*.example.com"))
|
assert.True(t, isDomainMatching("abc.example.com", []string{"*.example.com"}))
|
||||||
assert.True(t, isDomainMatching("abc.example.com", "*.example.com"))
|
assert.True(t, isDomainMatching("abc.def.example.com", []string{"*.example.com"}))
|
||||||
assert.True(t, isDomainMatching("abc.def.example.com", "*.example.com"))
|
}
|
||||||
|
|
||||||
// Character * must be followed by . to be valid.
|
func TestShouldNotMatchACLWithSingleDomain(t *testing.T) {
|
||||||
assert.False(t, isDomainMatching("example.com", "*example.com"))
|
assert.False(t, isDomainMatching("example.com", []string{"*.example.com"}))
|
||||||
|
// Character * must be followed by . to be valid.
|
||||||
assert.False(t, isDomainMatching("example.com", "*.example.com"))
|
assert.False(t, isDomainMatching("example.com", []string{"*example.com"}))
|
||||||
assert.False(t, isDomainMatching("example.com", "*.exampl.com"))
|
|
||||||
|
assert.False(t, isDomainMatching("example.com", []string{"*.exampl.com"}))
|
||||||
assert.False(t, isDomainMatching("example.com", "*.other.net"))
|
|
||||||
assert.False(t, isDomainMatching("example.com", "*other.net"))
|
assert.False(t, isDomainMatching("example.com", []string{"*.other.net"}))
|
||||||
assert.False(t, isDomainMatching("example.com", "other.net"))
|
assert.False(t, isDomainMatching("example.com", []string{"*other.net"}))
|
||||||
|
assert.False(t, isDomainMatching("example.com", []string{"other.net"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldMatchACLWithMultipleDomains(t *testing.T) {
|
||||||
|
assert.True(t, isDomainMatching("example.com", []string{"*.example.com", "example.com"}))
|
||||||
|
assert.True(t, isDomainMatching("apple.example.com", []string{"*.example.com", "example.com"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldNotMatchACLWithMultipleDomains(t *testing.T) {
|
||||||
|
assert.False(t, isDomainMatching("example.com", []string{"*.example.com", "*example.com"}))
|
||||||
|
assert.False(t, isDomainMatching("apple.example.com", []string{"*example.com", "example.com"}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,14 @@ type PasswordConfiguration struct {
|
||||||
Parallelism int `mapstructure:"parallelism"`
|
Parallelism int `mapstructure:"parallelism"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthenticationBackendConfiguration represents the configuration related to the authentication backend.
|
||||||
|
type AuthenticationBackendConfiguration struct {
|
||||||
|
DisableResetPassword bool `mapstructure:"disable_reset_password"`
|
||||||
|
RefreshInterval string `mapstructure:"refresh_interval"`
|
||||||
|
Ldap *LDAPAuthenticationBackendConfiguration `mapstructure:"ldap"`
|
||||||
|
File *FileAuthenticationBackendConfiguration `mapstructure:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultPasswordConfiguration represents the default configuration related to Argon2id hashing.
|
// DefaultPasswordConfiguration represents the default configuration related to Argon2id hashing.
|
||||||
var DefaultPasswordConfiguration = PasswordConfiguration{
|
var DefaultPasswordConfiguration = PasswordConfiguration{
|
||||||
Iterations: 1,
|
Iterations: 1,
|
||||||
|
@ -59,9 +67,8 @@ var DefaultPasswordSHA512Configuration = PasswordConfiguration{
|
||||||
Algorithm: "sha512",
|
Algorithm: "sha512",
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticationBackendConfiguration represents the configuration related to the authentication backend.
|
// DefaultLDAPAuthenticationBackendConfiguration represents the default LDAP config.
|
||||||
type AuthenticationBackendConfiguration struct {
|
var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendConfiguration{
|
||||||
DisableResetPassword bool `mapstructure:"disable_reset_password"`
|
MailAttribute: "mail",
|
||||||
Ldap *LDAPAuthenticationBackendConfiguration `mapstructure:"ldap"`
|
GroupNameAttribute: "cn",
|
||||||
File *FileAuthenticationBackendConfiguration `mapstructure:"file"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
package schema
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
const denyPolicy = "deny"
|
const denyPolicy = "deny"
|
||||||
|
|
||||||
|
// ProfileRefreshDisabled represents a value for refresh_interval that disables the check entirely.
|
||||||
|
const ProfileRefreshDisabled = "disable"
|
||||||
|
|
||||||
|
// ProfileRefreshAlways represents a value for refresh_interval that's the same as 0ms.
|
||||||
|
const ProfileRefreshAlways = "always"
|
||||||
|
|
||||||
|
// RefreshIntervalDefault represents the default value of refresh_interval.
|
||||||
|
const RefreshIntervalDefault = "5m"
|
||||||
|
|
||||||
|
// RefreshIntervalAlways represents the duration value refresh interval should have if set to always.
|
||||||
|
const RefreshIntervalAlways = 0 * time.Millisecond
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting
|
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting
|
||||||
|
@ -147,11 +148,11 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
|
||||||
}
|
}
|
||||||
|
|
||||||
if configuration.GroupNameAttribute == "" {
|
if configuration.GroupNameAttribute == "" {
|
||||||
configuration.GroupNameAttribute = "cn"
|
configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute
|
||||||
}
|
}
|
||||||
|
|
||||||
if configuration.MailAttribute == "" {
|
if configuration.MailAttribute == "" {
|
||||||
configuration.MailAttribute = "mail"
|
configuration.MailAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.MailAttribute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,4 +171,13 @@ func ValidateAuthenticationBackend(configuration *schema.AuthenticationBackendCo
|
||||||
} else if configuration.Ldap != nil {
|
} else if configuration.Ldap != nil {
|
||||||
validateLdapAuthenticationBackend(configuration.Ldap, validator)
|
validateLdapAuthenticationBackend(configuration.Ldap, validator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configuration.RefreshInterval == "" {
|
||||||
|
configuration.RefreshInterval = schema.RefreshIntervalDefault
|
||||||
|
} else {
|
||||||
|
_, err := utils.ParseDurationString(configuration.RefreshInterval)
|
||||||
|
if err != nil && configuration.RefreshInterval != schema.ProfileRefreshDisabled && configuration.RefreshInterval != schema.ProfileRefreshAlways {
|
||||||
|
validator.Push(fmt.Errorf("Auth Backend `refresh_interval` is configured to '%s' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: %s", configuration.RefreshInterval, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,6 +229,13 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsernameAttri
|
||||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a username attribute with `username_attribute`")
|
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a username attribute with `username_attribute`")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() {
|
||||||
|
suite.configuration.RefreshInterval = "blah"
|
||||||
|
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||||
|
require.Len(suite.T(), suite.validator.Errors(), 1)
|
||||||
|
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) TestShouldSetDefaultGroupNameAttribute() {
|
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() {
|
||||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||||
|
@ -241,6 +248,12 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute()
|
||||||
assert.Equal(suite.T(), "mail", suite.configuration.Ldap.MailAttribute)
|
assert.Equal(suite.T(), "mail", suite.configuration.Ldap.MailAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() {
|
||||||
|
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||||
|
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||||
|
assert.Equal(suite.T(), "5m", suite.configuration.RefreshInterval)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() {
|
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() {
|
||||||
suite.configuration.Ldap.UsersFilter = "uid={input}"
|
suite.configuration.Ldap.UsersFilter = "uid={input}"
|
||||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||||
|
|
|
@ -85,6 +85,7 @@ var validKeys = []string{
|
||||||
|
|
||||||
// Authentication Backend Keys.
|
// Authentication Backend Keys.
|
||||||
"authentication_backend.disable_reset_password",
|
"authentication_backend.disable_reset_password",
|
||||||
|
"authentication_backend.refresh_interval",
|
||||||
|
|
||||||
// LDAP Authentication Backend Keys.
|
// LDAP Authentication Backend Keys.
|
||||||
"authentication_backend.ldap.url",
|
"authentication_backend.ldap.url",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// FirstFactorPost is the handler performing the first factory.
|
// FirstFactorPost is the handler performing the first factory.
|
||||||
|
//nolint:gocyclo // TODO: Consider refactoring time permitting.
|
||||||
func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
bodyJSON := firstFactorRequestBody{}
|
bodyJSON := firstFactorRequestBody{}
|
||||||
err := ctx.ParseBody(&bodyJSON)
|
err := ctx.ParseBody(&bodyJSON)
|
||||||
|
@ -104,6 +105,10 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||||
userSession.AuthenticationLevel = authentication.OneFactor
|
userSession.AuthenticationLevel = authentication.OneFactor
|
||||||
userSession.LastActivity = time.Now().Unix()
|
userSession.LastActivity = time.Now().Unix()
|
||||||
userSession.KeepMeLoggedIn = keepMeLoggedIn
|
userSession.KeepMeLoggedIn = keepMeLoggedIn
|
||||||
|
refresh, refreshInterval := getProfileRefreshSettings(ctx.Configuration.AuthenticationBackend)
|
||||||
|
if refresh {
|
||||||
|
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshInterval)
|
||||||
|
}
|
||||||
err = ctx.SaveSession(userSession)
|
err = ctx.SaveSession(userSession)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -342,6 +342,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldRedirectToDefaultURLWhenURLIsUns
|
||||||
"keepMeLoggedIn": false,
|
"keepMeLoggedIn": false,
|
||||||
"targetURL": "http://notsafe.local"
|
"targetURL": "http://notsafe.local"
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
FirstFactorPost(s.mock.Ctx)
|
FirstFactorPost(s.mock.Ctx)
|
||||||
|
|
||||||
// Respond with 200.
|
// Respond with 200.
|
||||||
|
@ -361,6 +362,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenNoTargetURLProvidedA
|
||||||
"password": "hello",
|
"password": "hello",
|
||||||
"keepMeLoggedIn": false
|
"keepMeLoggedIn": false
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
FirstFactorPost(s.mock.Ctx)
|
FirstFactorPost(s.mock.Ctx)
|
||||||
|
|
||||||
// Respond with 200.
|
// Respond with 200.
|
||||||
|
@ -390,6 +392,7 @@ func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenUnsafeTargetURLProvi
|
||||||
"password": "hello",
|
"password": "hello",
|
||||||
"keepMeLoggedIn": false
|
"keepMeLoggedIn": false
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
FirstFactorPost(s.mock.Ctx)
|
FirstFactorPost(s.mock.Ctx)
|
||||||
|
|
||||||
// Respond with 200.
|
// Respond with 200.
|
||||||
|
|
|
@ -6,12 +6,16 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
"github.com/authelia/authelia/internal/authorization"
|
"github.com/authelia/authelia/internal/authorization"
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
|
"github.com/authelia/authelia/internal/session"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isURLUnderProtectedDomain(url *url.URL, domain string) bool {
|
func isURLUnderProtectedDomain(url *url.URL, domain string) bool {
|
||||||
|
@ -34,7 +38,7 @@ func getOriginalURL(ctx *middlewares.AutheliaCtx) (*url.URL, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to parse URL extracted from X-Original-URL header: %v", err)
|
return nil, fmt.Errorf("Unable to parse URL extracted from X-Original-URL header: %v", err)
|
||||||
}
|
}
|
||||||
ctx.Logger.Debug("Using X-Original-URL header content as targeted site URL")
|
ctx.Logger.Trace("Using X-Original-URL header content as targeted site URL")
|
||||||
return url, nil
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +63,7 @@ func getOriginalURL(ctx *middlewares.AutheliaCtx) (*url.URL, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to parse URL %s: %v", requestURI, err)
|
return nil, fmt.Errorf("Unable to parse URL %s: %v", requestURI, err)
|
||||||
}
|
}
|
||||||
ctx.Logger.Debugf("Using X-Fowarded-Proto, X-Forwarded-Host and X-Forwarded-URI headers " +
|
ctx.Logger.Tracef("Using X-Fowarded-Proto, X-Forwarded-Host and X-Forwarded-URI headers " +
|
||||||
"to construct targeted site URL")
|
"to construct targeted site URL")
|
||||||
return url, nil
|
return url, nil
|
||||||
}
|
}
|
||||||
|
@ -151,8 +155,8 @@ func setForwardedHeaders(headers *fasthttp.ResponseHeader, username string, grou
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasUserBeenInactiveLongEnough check whether the user has been inactive for too long.
|
// hasUserBeenInactiveTooLong checks whether the user has been inactive for too long.
|
||||||
func hasUserBeenInactiveLongEnough(ctx *middlewares.AutheliaCtx) (bool, error) { //nolint:unparam
|
func hasUserBeenInactiveTooLong(ctx *middlewares.AutheliaCtx) (bool, error) { //nolint:unparam
|
||||||
maxInactivityPeriod := int64(ctx.Providers.SessionProvider.Inactivity.Seconds())
|
maxInactivityPeriod := int64(ctx.Providers.SessionProvider.Inactivity.Seconds())
|
||||||
if maxInactivityPeriod == 0 {
|
if maxInactivityPeriod == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -171,9 +175,8 @@ func hasUserBeenInactiveLongEnough(ctx *middlewares.AutheliaCtx) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyFromSessionCookie verify if a user identified by a cookie is allowed to access target URL.
|
// verifySessionCookie verifies if a user is identified by a cookie.
|
||||||
func verifyFromSessionCookie(targetURL url.URL, ctx *middlewares.AutheliaCtx) (username string, groups []string, authLevel authentication.Level, err error) { //nolint:unparam
|
func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession, refreshProfile bool, refreshProfileInterval time.Duration) (username string, groups []string, authLevel authentication.Level, err error) { //nolint:unparam
|
||||||
userSession := ctx.GetSession()
|
|
||||||
// No username in the session means the user is anonymous.
|
// No username in the session means the user is anonymous.
|
||||||
isUserAnonymous := userSession.Username == ""
|
isUserAnonymous := userSession.Username == ""
|
||||||
|
|
||||||
|
@ -182,7 +185,7 @@ func verifyFromSessionCookie(targetURL url.URL, ctx *middlewares.AutheliaCtx) (u
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userSession.KeepMeLoggedIn && !isUserAnonymous {
|
if !userSession.KeepMeLoggedIn && !isUserAnonymous {
|
||||||
inactiveLongEnough, err := hasUserBeenInactiveLongEnough(ctx)
|
inactiveLongEnough, err := hasUserBeenInactiveTooLong(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, authentication.NotAuthenticated, fmt.Errorf("Unable to check if user has been inactive for a long time: %s", err)
|
return "", nil, authentication.NotAuthenticated, fmt.Errorf("Unable to check if user has been inactive for a long time: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -197,6 +200,19 @@ func verifyFromSessionCookie(targetURL url.URL, ctx *middlewares.AutheliaCtx) (u
|
||||||
return userSession.Username, userSession.Groups, authentication.NotAuthenticated, fmt.Errorf("User %s has been inactive for too long", userSession.Username)
|
return userSession.Username, userSession.Groups, authentication.NotAuthenticated, fmt.Errorf("User %s has been inactive for too long", userSession.Username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = verifySessionHasUpToDateProfile(ctx, targetURL, userSession, refreshProfile, refreshProfileInterval)
|
||||||
|
if err != nil {
|
||||||
|
if err == authentication.ErrUserNotFound {
|
||||||
|
err = ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger.Error(fmt.Errorf("Unable to destroy user session after provider refresh didn't find the user: %s", err))
|
||||||
|
}
|
||||||
|
return userSession.Username, userSession.Groups, authentication.NotAuthenticated, err
|
||||||
|
}
|
||||||
|
ctx.Logger.Warnf("Error occurred while attempting to update user details from LDAP: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return userSession.Username, userSession.Groups, userSession.AuthenticationLevel, nil
|
return userSession.Username, userSession.Groups, userSession.AuthenticationLevel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,8 +251,107 @@ func updateActivityTimestamp(ctx *middlewares.AutheliaCtx, isBasicAuth bool, use
|
||||||
return ctx.SaveSession(userSession)
|
return ctx.SaveSession(userSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyGet is the handler verifying if a request is allowed to go through.
|
// generateVerifySessionHasUpToDateProfileTraceLogs is used to generate trace logs only when trace logging is enabled.
|
||||||
func VerifyGet(ctx *middlewares.AutheliaCtx) {
|
// The information calculated in this function is completely useless other than trace for now.
|
||||||
|
func generateVerifySessionHasUpToDateProfileTraceLogs(ctx *middlewares.AutheliaCtx, userSession *session.UserSession,
|
||||||
|
details *authentication.UserDetails) {
|
||||||
|
groupsAdded, groupsRemoved := utils.StringSlicesDelta(userSession.Groups, details.Groups)
|
||||||
|
emailsAdded, emailsRemoved := utils.StringSlicesDelta(userSession.Emails, details.Emails)
|
||||||
|
|
||||||
|
// Check Groups.
|
||||||
|
var groupsDelta []string
|
||||||
|
if len(groupsAdded) != 0 {
|
||||||
|
groupsDelta = append(groupsDelta, fmt.Sprintf("Added: %s.", strings.Join(groupsAdded, ", ")))
|
||||||
|
}
|
||||||
|
if len(groupsRemoved) != 0 {
|
||||||
|
groupsDelta = append(groupsDelta, fmt.Sprintf("Removed: %s.", strings.Join(groupsRemoved, ", ")))
|
||||||
|
}
|
||||||
|
if len(groupsDelta) != 0 {
|
||||||
|
ctx.Logger.Tracef("Updated groups detected for %s. %s", userSession.Username, strings.Join(groupsDelta, " "))
|
||||||
|
} else {
|
||||||
|
ctx.Logger.Tracef("No updated groups detected for %s", userSession.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Emails.
|
||||||
|
var emailsDelta []string
|
||||||
|
if len(emailsAdded) != 0 {
|
||||||
|
emailsDelta = append(emailsDelta, fmt.Sprintf("Added: %s.", strings.Join(emailsAdded, ", ")))
|
||||||
|
}
|
||||||
|
if len(emailsRemoved) != 0 {
|
||||||
|
emailsDelta = append(emailsDelta, fmt.Sprintf("Removed: %s.", strings.Join(emailsRemoved, ", ")))
|
||||||
|
}
|
||||||
|
if len(emailsDelta) != 0 {
|
||||||
|
ctx.Logger.Tracef("Updated emails detected for %s. %s", userSession.Username, strings.Join(emailsDelta, " "))
|
||||||
|
} else {
|
||||||
|
ctx.Logger.Tracef("No updated emails detected for %s", userSession.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userSession *session.UserSession,
|
||||||
|
refreshProfile bool, refreshProfileInterval time.Duration) error {
|
||||||
|
// TODO: Add a check for LDAP password changes based on a time format attribute.
|
||||||
|
// See https://docs.authelia.com/security/threat-model.html#potential-future-guarantees
|
||||||
|
|
||||||
|
ctx.Logger.Tracef("Checking if we need check the authentication backend for an updated profile for %s.", userSession.Username)
|
||||||
|
if refreshProfile && userSession.Username != "" && targetURL != nil &&
|
||||||
|
ctx.Providers.Authorizer.IsURLMatchingRuleWithGroupSubjects(*targetURL) &&
|
||||||
|
(refreshProfileInterval == schema.RefreshIntervalAlways || userSession.RefreshTTL.Before(ctx.Clock.Now())) {
|
||||||
|
ctx.Logger.Debugf("Checking the authentication backend for an updated profile for user %s", userSession.Username)
|
||||||
|
details, err := ctx.Providers.UserProvider.GetDetails(userSession.Username)
|
||||||
|
// Only update the session if we could get the new details.
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupsDiff := utils.IsStringSlicesDifferent(userSession.Groups, details.Groups)
|
||||||
|
emailsDiff := utils.IsStringSlicesDifferent(userSession.Emails, details.Emails)
|
||||||
|
if !groupsDiff && !emailsDiff {
|
||||||
|
ctx.Logger.Tracef("Updated profile not detected for %s.", userSession.Username)
|
||||||
|
} else {
|
||||||
|
ctx.Logger.Debugf("Updated profile detected for %s.", userSession.Username)
|
||||||
|
if ctx.Logger.Level.String() == "trace" {
|
||||||
|
generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details)
|
||||||
|
}
|
||||||
|
userSession.Groups = details.Groups
|
||||||
|
userSession.Emails = details.Emails
|
||||||
|
|
||||||
|
// Only update TTL if the user has a interval set.
|
||||||
|
if refreshProfileInterval != schema.RefreshIntervalAlways {
|
||||||
|
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval)
|
||||||
|
}
|
||||||
|
return ctx.SaveSession(*userSession)
|
||||||
|
}
|
||||||
|
// Only update TTL if the user has a interval set.
|
||||||
|
// Also make sure to update the session even if no difference was found.
|
||||||
|
// This is so that we don't check every subsequent request after this one.
|
||||||
|
if refreshProfileInterval != schema.RefreshIntervalAlways {
|
||||||
|
userSession.RefreshTTL = ctx.Clock.Now().Add(refreshProfileInterval)
|
||||||
|
return ctx.SaveSession(*userSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProfileRefreshSettings(cfg schema.AuthenticationBackendConfiguration) (refresh bool, refreshInterval time.Duration) {
|
||||||
|
if cfg.Ldap != nil {
|
||||||
|
if cfg.RefreshInterval != schema.ProfileRefreshDisabled {
|
||||||
|
refresh = true
|
||||||
|
if cfg.RefreshInterval != schema.ProfileRefreshAlways {
|
||||||
|
// Skip Error Check since validator checks it
|
||||||
|
refreshInterval, _ = utils.ParseDurationString(cfg.RefreshInterval)
|
||||||
|
} else {
|
||||||
|
refreshInterval = schema.RefreshIntervalAlways
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return refresh, refreshInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyGet returns the handler verifying if a request is allowed to go through.
|
||||||
|
func VerifyGet(cfg schema.AuthenticationBackendConfiguration) middlewares.RequestHandler {
|
||||||
|
refreshProfile, refreshProfileInterval := getProfileRefreshSettings(cfg)
|
||||||
|
|
||||||
|
return func(ctx *middlewares.AutheliaCtx) {
|
||||||
ctx.Logger.Tracef("Headers=%s", ctx.Request.Header.String())
|
ctx.Logger.Tracef("Headers=%s", ctx.Request.Header.String())
|
||||||
targetURL, err := getOriginalURL(ctx)
|
targetURL, err := getOriginalURL(ctx)
|
||||||
|
|
||||||
|
@ -265,11 +380,13 @@ func VerifyGet(ctx *middlewares.AutheliaCtx) {
|
||||||
|
|
||||||
proxyAuthorization := ctx.Request.Header.Peek(AuthorizationHeader)
|
proxyAuthorization := ctx.Request.Header.Peek(AuthorizationHeader)
|
||||||
isBasicAuth := proxyAuthorization != nil
|
isBasicAuth := proxyAuthorization != nil
|
||||||
|
userSession := ctx.GetSession()
|
||||||
|
|
||||||
if isBasicAuth {
|
if isBasicAuth {
|
||||||
username, groups, authLevel, err = verifyBasicAuth(proxyAuthorization, *targetURL, ctx)
|
username, groups, authLevel, err = verifyBasicAuth(proxyAuthorization, *targetURL, ctx)
|
||||||
} else {
|
} else {
|
||||||
username, groups, authLevel, err = verifyFromSessionCookie(*targetURL, ctx)
|
username, groups, authLevel, err = verifySessionCookie(ctx, targetURL, &userSession,
|
||||||
|
refreshProfile, refreshProfileInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -298,3 +415,4 @@ func VerifyGet(ctx *middlewares.AutheliaCtx) {
|
||||||
ctx.Error(fmt.Errorf("Unable to update last activity: %s", err), operationFailedMessage)
|
ctx.Error(fmt.Errorf("Unable to update last activity: %s", err), operationFailedMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,8 +17,14 @@ import (
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/internal/mocks"
|
"github.com/authelia/authelia/internal/mocks"
|
||||||
"github.com/authelia/authelia/internal/session"
|
"github.com/authelia/authelia/internal/session"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var verifyGetCfg = schema.AuthenticationBackendConfiguration{
|
||||||
|
RefreshInterval: schema.RefreshIntervalDefault,
|
||||||
|
Ldap: &schema.LDAPAuthenticationBackendConfiguration{},
|
||||||
|
}
|
||||||
|
|
||||||
// Test getOriginalURL.
|
// Test getOriginalURL.
|
||||||
func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
|
func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
|
||||||
mock := mocks.NewMockAutheliaCtx(t)
|
mock := mocks.NewMockAutheliaCtx(t)
|
||||||
|
@ -87,24 +93,26 @@ func TestShouldRaiseWhenNoXForwardedHostHeaderProvidedToDetectTargetURL(t *testi
|
||||||
assert.Equal(t, "Missing header X-Fowarded-Host", err.Error())
|
assert.Equal(t, "Missing header X-Fowarded-Host", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldRaiseWhenXForwardedProtoIsNotParseable(t *testing.T) {
|
func TestShouldRaiseWhenXForwardedProtoIsNotParsable(t *testing.T) {
|
||||||
mock := mocks.NewMockAutheliaCtx(t)
|
mock := mocks.NewMockAutheliaCtx(t)
|
||||||
defer mock.Close()
|
defer mock.Close()
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "!:;;:,")
|
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "!:;;:,")
|
||||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
|
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
|
||||||
|
|
||||||
_, err := getOriginalURL(mock.Ctx)
|
_, err := getOriginalURL(mock.Ctx)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "Unable to parse URL !:;;:,://myhost.local: parse !:;;:,://myhost.local: invalid URI for request", err.Error())
|
assert.Equal(t, "Unable to parse URL !:;;:,://myhost.local: parse !:;;:,://myhost.local: invalid URI for request", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldRaiseWhenXForwardedURIIsNotParseable(t *testing.T) {
|
func TestShouldRaiseWhenXForwardedURIIsNotParsable(t *testing.T) {
|
||||||
mock := mocks.NewMockAutheliaCtx(t)
|
mock := mocks.NewMockAutheliaCtx(t)
|
||||||
defer mock.Close()
|
defer mock.Close()
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
|
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "myhost.local")
|
||||||
mock.Ctx.Request.Header.Set("X-Forwarded-URI", "!:;;:,")
|
mock.Ctx.Request.Header.Set("X-Forwarded-URI", "!:;;:,")
|
||||||
|
|
||||||
_, err := getOriginalURL(mock.Ctx)
|
_, err := getOriginalURL(mock.Ctx)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Equal(t, "Unable to parse URL https://myhost.local!:;;:,: parse https://myhost.local!:;;:,: invalid port \":,\" after host", err.Error())
|
assert.Equal(t, "Unable to parse URL https://myhost.local!:;;:,: parse https://myhost.local!:;;:,: invalid port \":,\" after host", err.Error())
|
||||||
|
@ -124,7 +132,7 @@ func TestShouldRaiseWhenCredentialsAreNotInBase64(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) {
|
func TestShouldRaiseWhenCredentialsAreNotInCorrectForm(t *testing.T) {
|
||||||
// the decoded format should be user:password.
|
// The decoded format should be user:password.
|
||||||
_, _, err := parseBasicAuth("Basic am9obiBwYXNzd29yZA==")
|
_, _, err := parseBasicAuth("Basic am9obiBwYXNzd29yZA==")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "Format of Proxy-Authorization header must be user:password", err.Error())
|
assert.Equal(t, "Format of Proxy-Authorization header must be user:password", err.Error())
|
||||||
|
@ -225,7 +233,7 @@ func (s *BasicAuthorizationSuite) TestShouldNotBeAbleToParseBasicAuth() {
|
||||||
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpaaaaaaaaaaaaaaaa")
|
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpaaaaaaaaaaaaaaaa")
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
||||||
}
|
}
|
||||||
|
@ -248,7 +256,7 @@ func (s *BasicAuthorizationSuite) TestShouldApplyDefaultPolicy() {
|
||||||
Groups: []string{"dev", "admins"},
|
Groups: []string{"dev", "admins"},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
|
assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
|
||||||
}
|
}
|
||||||
|
@ -271,7 +279,7 @@ func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfBypassDomain() {
|
||||||
Groups: []string{"dev", "admins"},
|
Groups: []string{"dev", "admins"},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
|
assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
|
||||||
}
|
}
|
||||||
|
@ -294,7 +302,7 @@ func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfOneFactorDomain() {
|
||||||
Groups: []string{"dev", "admins"},
|
Groups: []string{"dev", "admins"},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
|
assert.Equal(s.T(), 200, mock.Ctx.Response.StatusCode())
|
||||||
}
|
}
|
||||||
|
@ -317,7 +325,7 @@ func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfTwoFactorDomain() {
|
||||||
Groups: []string{"dev", "admins"},
|
Groups: []string{"dev", "admins"},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
assert.Equal(s.T(), 401, mock.Ctx.Response.StatusCode())
|
||||||
}
|
}
|
||||||
|
@ -340,7 +348,7 @@ func (s *BasicAuthorizationSuite) TestShouldApplyPolicyOfDenyDomain() {
|
||||||
Groups: []string{"dev", "admins"},
|
Groups: []string{"dev", "admins"},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
|
assert.Equal(s.T(), 403, mock.Ctx.Response.StatusCode())
|
||||||
}
|
}
|
||||||
|
@ -360,7 +368,7 @@ func TestShouldVerifyWrongCredentialsInBasicAuth(t *testing.T) {
|
||||||
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=")
|
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=")
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
|
expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
|
||||||
assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
|
assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
|
||||||
"https://test.example.com", actualStatus, expStatus)
|
"https://test.example.com", actualStatus, expStatus)
|
||||||
|
@ -377,7 +385,7 @@ func TestShouldVerifyFailingPasswordCheckingInBasicAuth(t *testing.T) {
|
||||||
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=")
|
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objp3cm9uZ3Bhc3M=")
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
|
expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
|
||||||
assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
|
assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
|
||||||
"https://test.example.com", actualStatus, expStatus)
|
"https://test.example.com", actualStatus, expStatus)
|
||||||
|
@ -398,7 +406,7 @@ func TestShouldVerifyFailingDetailsFetchingInBasicAuth(t *testing.T) {
|
||||||
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
|
mock.Ctx.Request.Header.Set("Proxy-Authorization", "Basic am9objpwYXNzd29yZA==")
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://test.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
|
expStatus, actualStatus := 401, mock.Ctx.Response.StatusCode()
|
||||||
assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
|
assert.Equal(t, expStatus, actualStatus, "URL=%s -> StatusCode=%d != ExpectedStatusCode=%d",
|
||||||
"https://test.example.com", actualStatus, expStatus)
|
"https://test.example.com", actualStatus, expStatus)
|
||||||
|
@ -450,7 +458,7 @@ func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", testCase.URL)
|
mock.Ctx.Request.Header.Set("X-Original-URL", testCase.URL)
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
expStatus, actualStatus := testCase.ExpectedStatusCode, mock.Ctx.Response.StatusCode()
|
expStatus, actualStatus := testCase.ExpectedStatusCode, mock.Ctx.Response.StatusCode()
|
||||||
assert.Equal(t, expStatus, actualStatus, "URL=%s -> AuthLevel=%d, StatusCode=%d != ExpectedStatusCode=%d",
|
assert.Equal(t, expStatus, actualStatus, "URL=%s -> AuthLevel=%d, StatusCode=%d != ExpectedStatusCode=%d",
|
||||||
testCase.URL, testCase.AuthenticationLevel, actualStatus, expStatus)
|
testCase.URL, testCase.AuthenticationLevel, actualStatus, expStatus)
|
||||||
|
@ -485,7 +493,7 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
// The session has been destroyed.
|
// The session has been destroyed.
|
||||||
newUserSession := mock.Ctx.GetSession()
|
newUserSession := mock.Ctx.GetSession()
|
||||||
|
@ -516,7 +524,7 @@ func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *test
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
// The session has been destroyed.
|
// The session has been destroyed.
|
||||||
newUserSession := mock.Ctx.GetSession()
|
newUserSession := mock.Ctx.GetSession()
|
||||||
|
@ -542,9 +550,9 @@ func TestShouldKeepSessionWhenUserCheckedRememberMeAndIsInactiveForTooLong(t *te
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
// The session has been destroyed.
|
// Check the session is still active.
|
||||||
newUserSession := mock.Ctx.GetSession()
|
newUserSession := mock.Ctx.GetSession()
|
||||||
assert.Equal(t, "john", newUserSession.Username)
|
assert.Equal(t, "john", newUserSession.Username)
|
||||||
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
|
assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel)
|
||||||
|
@ -572,7 +580,7 @@ func TestShouldKeepSessionWhenInactivityTimeoutHasNotBeenExceeded(t *testing.T)
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
// The session has been destroyed.
|
// The session has been destroyed.
|
||||||
newUserSession := mock.Ctx.GetSession()
|
newUserSession := mock.Ctx.GetSession()
|
||||||
|
@ -608,7 +616,7 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin
|
||||||
mock.Ctx.QueryArgs().Add("rd", "https://login.example.com")
|
mock.Ctx.QueryArgs().Add("rd", "https://login.example.com")
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(t, "Found. Redirecting to https://login.example.com?rd=https%3A%2F%2Ftwo-factor.example.com",
|
assert.Equal(t, "Found. Redirecting to https://login.example.com?rd=https%3A%2F%2Ftwo-factor.example.com",
|
||||||
string(mock.Ctx.Response.Body()))
|
string(mock.Ctx.Response.Body()))
|
||||||
|
@ -638,7 +646,7 @@ func TestShouldUpdateInactivityTimestampEvenWhenHittingForbiddenResources(t *tes
|
||||||
|
|
||||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://deny.example.com")
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://deny.example.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
// The resource if forbidden.
|
// The resource if forbidden.
|
||||||
assert.Equal(t, 403, mock.Ctx.Response.StatusCode())
|
assert.Equal(t, 403, mock.Ctx.Response.StatusCode())
|
||||||
|
@ -661,7 +669,7 @@ func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) {
|
||||||
mock.Ctx.Request.SetHost("mydomain.com")
|
mock.Ctx.Request.SetHost("mydomain.com")
|
||||||
mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
|
mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
|
||||||
|
|
||||||
VerifyGet(mock.Ctx)
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
|
||||||
assert.Equal(t, "Found. Redirecting to https://auth.mydomain.com?rd=https%3A%2F%2Ftwo-factor.example.com",
|
assert.Equal(t, "Found. Redirecting to https://auth.mydomain.com?rd=https%3A%2F%2Ftwo-factor.example.com",
|
||||||
string(mock.Ctx.Response.Body()))
|
string(mock.Ctx.Response.Body()))
|
||||||
|
@ -722,3 +730,264 @@ func TestSchemeIsWSS(t *testing.T) {
|
||||||
assert.True(t, isSchemeWSS(
|
assert.True(t, isSchemeWSS(
|
||||||
GetURL("wss://mytest.example.com/abc/?query=abc")))
|
GetURL("wss://mytest.example.com/abc/?query=abc")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldNotRefreshUserGroupsFromBackend(t *testing.T) {
|
||||||
|
mock := mocks.NewMockAutheliaCtx(t)
|
||||||
|
defer mock.Close()
|
||||||
|
|
||||||
|
// Setup pointer to john so we can adjust it during the test.
|
||||||
|
user := &authentication.UserDetails{
|
||||||
|
Username: "john",
|
||||||
|
Groups: []string{
|
||||||
|
"admin",
|
||||||
|
"users",
|
||||||
|
},
|
||||||
|
Emails: []string{
|
||||||
|
"john@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := verifyGetCfg
|
||||||
|
cfg.RefreshInterval = "disable"
|
||||||
|
verifyGet := VerifyGet(cfg)
|
||||||
|
|
||||||
|
mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
|
||||||
|
|
||||||
|
clock := mocks.TestingClock{}
|
||||||
|
clock.Set(time.Now())
|
||||||
|
|
||||||
|
userSession := mock.Ctx.GetSession()
|
||||||
|
userSession.Username = user.Username
|
||||||
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
||||||
|
userSession.LastActivity = clock.Now().Unix()
|
||||||
|
userSession.Groups = user.Groups
|
||||||
|
userSession.Emails = user.Emails
|
||||||
|
userSession.KeepMeLoggedIn = true
|
||||||
|
err := mock.Ctx.SaveSession(userSession)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Check Refresh TTL has not been updated.
|
||||||
|
userSession = mock.Ctx.GetSession()
|
||||||
|
|
||||||
|
// Check user groups are correct.
|
||||||
|
require.Len(t, userSession.Groups, len(user.Groups))
|
||||||
|
assert.Equal(t, utils.RFC3339Zero, userSession.RefreshTTL.Unix())
|
||||||
|
assert.Equal(t, "admin", userSession.Groups[0])
|
||||||
|
assert.Equal(t, "users", userSession.Groups[1])
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Check admin group is not removed from the session.
|
||||||
|
userSession = mock.Ctx.GetSession()
|
||||||
|
assert.Equal(t, utils.RFC3339Zero, userSession.RefreshTTL.Unix())
|
||||||
|
require.Len(t, userSession.Groups, 2)
|
||||||
|
assert.Equal(t, "admin", userSession.Groups[0])
|
||||||
|
assert.Equal(t, "users", userSession.Groups[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldNotRefreshUserGroupsFromBackendWhenNoGroupSubject(t *testing.T) {
|
||||||
|
mock := mocks.NewMockAutheliaCtx(t)
|
||||||
|
defer mock.Close()
|
||||||
|
|
||||||
|
// Setup user john.
|
||||||
|
user := &authentication.UserDetails{
|
||||||
|
Username: "john",
|
||||||
|
Groups: []string{
|
||||||
|
"admin",
|
||||||
|
"users",
|
||||||
|
},
|
||||||
|
Emails: []string{
|
||||||
|
"john@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
|
||||||
|
|
||||||
|
clock := mocks.TestingClock{}
|
||||||
|
clock.Set(time.Now())
|
||||||
|
|
||||||
|
userSession := mock.Ctx.GetSession()
|
||||||
|
userSession.Username = user.Username
|
||||||
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
||||||
|
userSession.LastActivity = clock.Now().Unix()
|
||||||
|
userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute)
|
||||||
|
userSession.Groups = user.Groups
|
||||||
|
userSession.Emails = user.Emails
|
||||||
|
userSession.KeepMeLoggedIn = true
|
||||||
|
err := mock.Ctx.SaveSession(userSession)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Session time should NOT have been updated, it should still have a refresh TTL 1 minute in the past.
|
||||||
|
userSession = mock.Ctx.GetSession()
|
||||||
|
assert.Equal(t, clock.Now().Add(-1*time.Minute).Unix(), userSession.RefreshTTL.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldGetRemovedUserGroupsFromBackend(t *testing.T) {
|
||||||
|
mock := mocks.NewMockAutheliaCtx(t)
|
||||||
|
defer mock.Close()
|
||||||
|
|
||||||
|
// Setup pointer to john so we can adjust it during the test.
|
||||||
|
user := &authentication.UserDetails{
|
||||||
|
Username: "john",
|
||||||
|
Groups: []string{
|
||||||
|
"admin",
|
||||||
|
"users",
|
||||||
|
},
|
||||||
|
Emails: []string{
|
||||||
|
"john@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyGet := VerifyGet(verifyGetCfg)
|
||||||
|
mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(2)
|
||||||
|
|
||||||
|
clock := mocks.TestingClock{}
|
||||||
|
clock.Set(time.Now())
|
||||||
|
|
||||||
|
userSession := mock.Ctx.GetSession()
|
||||||
|
userSession.Username = user.Username
|
||||||
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
||||||
|
userSession.LastActivity = clock.Now().Unix()
|
||||||
|
userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute)
|
||||||
|
userSession.Groups = user.Groups
|
||||||
|
userSession.Emails = user.Emails
|
||||||
|
userSession.KeepMeLoggedIn = true
|
||||||
|
err := mock.Ctx.SaveSession(userSession)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Request should get refresh settings and new user details.
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Check Refresh TTL has been updated since admin.example.com has a group subject and refresh is enabled.
|
||||||
|
userSession = mock.Ctx.GetSession()
|
||||||
|
|
||||||
|
// Check user groups are correct.
|
||||||
|
require.Len(t, userSession.Groups, len(user.Groups))
|
||||||
|
assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
|
||||||
|
assert.Equal(t, "admin", userSession.Groups[0])
|
||||||
|
assert.Equal(t, "users", userSession.Groups[1])
|
||||||
|
|
||||||
|
// Remove the admin group, and force the next request to refresh.
|
||||||
|
user.Groups = []string{"users"}
|
||||||
|
userSession.RefreshTTL = clock.Now().Add(-1 * time.Second)
|
||||||
|
err = mock.Ctx.SaveSession(userSession)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://admin.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 403, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Check admin group is removed from the session.
|
||||||
|
userSession = mock.Ctx.GetSession()
|
||||||
|
assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
|
||||||
|
require.Len(t, userSession.Groups, 1)
|
||||||
|
assert.Equal(t, "users", userSession.Groups[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldGetAddedUserGroupsFromBackend(t *testing.T) {
|
||||||
|
mock := mocks.NewMockAutheliaCtx(t)
|
||||||
|
//defer mock.Close()
|
||||||
|
|
||||||
|
// Setup pointer to john so we can adjust it during the test.
|
||||||
|
user := &authentication.UserDetails{
|
||||||
|
Username: "john",
|
||||||
|
Groups: []string{
|
||||||
|
"admin",
|
||||||
|
"users",
|
||||||
|
},
|
||||||
|
Emails: []string{
|
||||||
|
"john@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
|
||||||
|
|
||||||
|
verifyGet := VerifyGet(verifyGetCfg)
|
||||||
|
|
||||||
|
clock := mocks.TestingClock{}
|
||||||
|
clock.Set(time.Now())
|
||||||
|
|
||||||
|
userSession := mock.Ctx.GetSession()
|
||||||
|
userSession.Username = user.Username
|
||||||
|
userSession.AuthenticationLevel = authentication.TwoFactor
|
||||||
|
userSession.LastActivity = clock.Now().Unix()
|
||||||
|
userSession.RefreshTTL = clock.Now().Add(-1 * time.Minute)
|
||||||
|
userSession.Groups = user.Groups
|
||||||
|
userSession.Emails = user.Emails
|
||||||
|
userSession.KeepMeLoggedIn = true
|
||||||
|
err := mock.Ctx.SaveSession(userSession)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Request should get refresh user profile.
|
||||||
|
mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1)
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://grafana.example.com")
|
||||||
|
verifyGet(mock.Ctx)
|
||||||
|
assert.Equal(t, 403, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Check Refresh TTL has been updated since grafana.example.com has a group subject and refresh is enabled.
|
||||||
|
userSession = mock.Ctx.GetSession()
|
||||||
|
|
||||||
|
// Check user groups are correct.
|
||||||
|
require.Len(t, userSession.Groups, len(user.Groups))
|
||||||
|
assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
|
||||||
|
assert.Equal(t, "admin", userSession.Groups[0])
|
||||||
|
assert.Equal(t, "users", userSession.Groups[1])
|
||||||
|
|
||||||
|
// Add the grafana group, and force the next request to refresh.
|
||||||
|
user.Groups = append(user.Groups, "grafana")
|
||||||
|
userSession.RefreshTTL = clock.Now().Add(-1 * time.Second)
|
||||||
|
err = mock.Ctx.SaveSession(userSession)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Reset otherwise we get the last 403 when we check the Response. Is there a better way to do this?
|
||||||
|
mock.Close()
|
||||||
|
mock = mocks.NewMockAutheliaCtx(t)
|
||||||
|
defer mock.Close()
|
||||||
|
err = mock.Ctx.SaveSession(userSession)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
gomock.InOrder(
|
||||||
|
mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
mock.Ctx.Request.Header.Set("X-Original-URL", "https://grafana.example.com")
|
||||||
|
VerifyGet(verifyGetCfg)(mock.Ctx)
|
||||||
|
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
|
||||||
|
|
||||||
|
// Check admin group is removed from the session.
|
||||||
|
userSession = mock.Ctx.GetSession()
|
||||||
|
assert.Equal(t, true, userSession.KeepMeLoggedIn)
|
||||||
|
assert.Equal(t, authentication.TwoFactor, userSession.AuthenticationLevel)
|
||||||
|
assert.Equal(t, clock.Now().Add(5*time.Minute).Unix(), userSession.RefreshTTL.Unix())
|
||||||
|
require.Len(t, userSession.Groups, 3)
|
||||||
|
assert.Equal(t, "admin", userSession.Groups[0])
|
||||||
|
assert.Equal(t, "users", userSession.Groups[1])
|
||||||
|
assert.Equal(t, "grafana", userSession.Groups[2])
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,14 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||||
}, {
|
}, {
|
||||||
Domains: []string{"deny.example.com"},
|
Domains: []string{"deny.example.com"},
|
||||||
Policy: "deny",
|
Policy: "deny",
|
||||||
|
}, {
|
||||||
|
Domains: []string{"admin.example.com"},
|
||||||
|
Policy: "two_factor",
|
||||||
|
Subjects: []string{"group:admin"},
|
||||||
|
}, {
|
||||||
|
Domains: []string{"grafana.example.com"},
|
||||||
|
Policy: "two_factor",
|
||||||
|
Subjects: []string{"group:grafana"},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
providers := middlewares.Providers{}
|
providers := middlewares.Providers{}
|
||||||
|
|
|
@ -14,30 +14,30 @@ import (
|
||||||
"github.com/authelia/authelia/internal/middlewares"
|
"github.com/authelia/authelia/internal/middlewares"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockAPI is a mock of API interface
|
// MockAPI is a mock of API interface.
|
||||||
type MockAPI struct {
|
type MockAPI struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
recorder *MockAPIMockRecorder
|
recorder *MockAPIMockRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockAPIMockRecorder is the mock recorder for MockAPI
|
// MockAPIMockRecorder is the mock recorder for MockAPI.
|
||||||
type MockAPIMockRecorder struct {
|
type MockAPIMockRecorder struct {
|
||||||
mock *MockAPI
|
mock *MockAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockAPI creates a new mock instance
|
// NewMockAPI creates a new mock instance.
|
||||||
func NewMockAPI(ctrl *gomock.Controller) *MockAPI {
|
func NewMockAPI(ctrl *gomock.Controller) *MockAPI {
|
||||||
mock := &MockAPI{ctrl: ctrl}
|
mock := &MockAPI{ctrl: ctrl}
|
||||||
mock.recorder = &MockAPIMockRecorder{mock}
|
mock.recorder = &MockAPIMockRecorder{mock}
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
|
func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
|
||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call mocks base method
|
// Call mocks base method.
|
||||||
func (m *MockAPI) Call(arg0 url.Values, arg1 *middlewares.AutheliaCtx) (*duo.Response, error) {
|
func (m *MockAPI) Call(arg0 url.Values, arg1 *middlewares.AutheliaCtx) (*duo.Response, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "Call", arg0, arg1)
|
ret := m.ctrl.Call(m, "Call", arg0, arg1)
|
||||||
|
@ -46,7 +46,7 @@ func (m *MockAPI) Call(arg0 url.Values, arg1 *middlewares.AutheliaCtx) (*duo.Res
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call indicates an expected call of Call
|
// Call indicates an expected call of Call.
|
||||||
func (mr *MockAPIMockRecorder) Call(arg0, arg1 interface{}) *gomock.Call {
|
func (mr *MockAPIMockRecorder) Call(arg0, arg1 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockAPI)(nil).Call), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockAPI)(nil).Call), arg0, arg1)
|
||||||
|
|
|
@ -33,11 +33,6 @@ func (m *MockNotifier) EXPECT() *MockNotifierMockRecorder {
|
||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartupCheck mocks base method.
|
|
||||||
func (m *MockNotifier) StartupCheck() (bool, error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send mocks base method.
|
// Send mocks base method.
|
||||||
func (m *MockNotifier) Send(arg0, arg1, arg2 string) error {
|
func (m *MockNotifier) Send(arg0, arg1, arg2 string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -51,3 +46,18 @@ func (mr *MockNotifierMockRecorder) Send(arg0, arg1, arg2 interface{}) *gomock.C
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockNotifier)(nil).Send), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockNotifier)(nil).Send), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartupCheck mocks base method.
|
||||||
|
func (m *MockNotifier) StartupCheck() (bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StartupCheck")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartupCheck indicates an expected call of StartupCheck.
|
||||||
|
func (mr *MockNotifierMockRecorder) StartupCheck() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockNotifier)(nil).StartupCheck))
|
||||||
|
}
|
||||||
|
|
|
@ -5,37 +5,37 @@
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
reflect "reflect"
|
"reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
|
||||||
authentication "github.com/authelia/authelia/internal/authentication"
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockUserProvider is a mock of UserProvider interface
|
// MockUserProvider is a mock of UserProvider interface.
|
||||||
type MockUserProvider struct {
|
type MockUserProvider struct {
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
recorder *MockUserProviderMockRecorder
|
recorder *MockUserProviderMockRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockUserProviderMockRecorder is the mock recorder for MockUserProvider
|
// MockUserProviderMockRecorder is the mock recorder for MockUserProvider.
|
||||||
type MockUserProviderMockRecorder struct {
|
type MockUserProviderMockRecorder struct {
|
||||||
mock *MockUserProvider
|
mock *MockUserProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockUserProvider creates a new mock instance
|
// NewMockUserProvider creates a new mock instance.
|
||||||
func NewMockUserProvider(ctrl *gomock.Controller) *MockUserProvider {
|
func NewMockUserProvider(ctrl *gomock.Controller) *MockUserProvider {
|
||||||
mock := &MockUserProvider{ctrl: ctrl}
|
mock := &MockUserProvider{ctrl: ctrl}
|
||||||
mock.recorder = &MockUserProviderMockRecorder{mock}
|
mock.recorder = &MockUserProviderMockRecorder{mock}
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
func (m *MockUserProvider) EXPECT() *MockUserProviderMockRecorder {
|
func (m *MockUserProvider) EXPECT() *MockUserProviderMockRecorder {
|
||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckUserPassword mocks base method
|
// CheckUserPassword mocks base method.
|
||||||
func (m *MockUserProvider) CheckUserPassword(arg0, arg1 string) (bool, error) {
|
func (m *MockUserProvider) CheckUserPassword(arg0, arg1 string) (bool, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "CheckUserPassword", arg0, arg1)
|
ret := m.ctrl.Call(m, "CheckUserPassword", arg0, arg1)
|
||||||
|
@ -44,13 +44,13 @@ func (m *MockUserProvider) CheckUserPassword(arg0, arg1 string) (bool, error) {
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckUserPassword indicates an expected call of CheckUserPassword
|
// CheckUserPassword indicates an expected call of CheckUserPassword.
|
||||||
func (mr *MockUserProviderMockRecorder) CheckUserPassword(arg0, arg1 interface{}) *gomock.Call {
|
func (mr *MockUserProviderMockRecorder) CheckUserPassword(arg0, arg1 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckUserPassword", reflect.TypeOf((*MockUserProvider)(nil).CheckUserPassword), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckUserPassword", reflect.TypeOf((*MockUserProvider)(nil).CheckUserPassword), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDetails mocks base method
|
// GetDetails mocks base method.
|
||||||
func (m *MockUserProvider) GetDetails(arg0 string) (*authentication.UserDetails, error) {
|
func (m *MockUserProvider) GetDetails(arg0 string) (*authentication.UserDetails, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetDetails", arg0)
|
ret := m.ctrl.Call(m, "GetDetails", arg0)
|
||||||
|
@ -59,7 +59,7 @@ func (m *MockUserProvider) GetDetails(arg0 string) (*authentication.UserDetails,
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDetails indicates an expected call of GetDetails
|
// GetDetails indicates an expected call of GetDetails.
|
||||||
func (mr *MockUserProviderMockRecorder) GetDetails(arg0 interface{}) *gomock.Call {
|
func (mr *MockUserProviderMockRecorder) GetDetails(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDetails", reflect.TypeOf((*MockUserProvider)(nil).GetDetails), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDetails", reflect.TypeOf((*MockUserProvider)(nil).GetDetails), arg0)
|
||||||
|
@ -73,7 +73,7 @@ func (m *MockUserProvider) UpdatePassword(arg0, arg1 string) error {
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePassword indicates an expected call of UpdatePassword
|
// UpdatePassword indicates an expected call of UpdatePassword.
|
||||||
func (mr *MockUserProviderMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.Call {
|
func (mr *MockUserProviderMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockUserProvider)(nil).UpdatePassword), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockUserProvider)(nil).UpdatePassword), arg0, arg1)
|
||||||
|
|
|
@ -38,8 +38,8 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
||||||
router.GET("/api/configuration/extended", autheliaMiddleware(
|
router.GET("/api/configuration/extended", autheliaMiddleware(
|
||||||
middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
|
middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
|
||||||
|
|
||||||
router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet))
|
router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
||||||
router.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet))
|
router.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
||||||
|
|
||||||
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost))
|
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost))
|
||||||
router.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost))
|
router.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost))
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fasthttp/session"
|
"github.com/fasthttp/session"
|
||||||
"github.com/tstranex/u2f"
|
"github.com/tstranex/u2f"
|
||||||
|
|
||||||
|
@ -41,6 +43,8 @@ type UserSession struct {
|
||||||
// This boolean is set to true after identity verification and checked
|
// This boolean is set to true after identity verification and checked
|
||||||
// while doing the query actually updating the password.
|
// while doing the query actually updating the password.
|
||||||
PasswordResetUsername *string
|
PasswordResetUsername *string
|
||||||
|
|
||||||
|
RefreshTTL time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identity identity of the user who is being verified.
|
// Identity identity of the user who is being verified.
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import "github.com/authelia/authelia/internal/authentication"
|
import (
|
||||||
|
"github.com/authelia/authelia/internal/authentication"
|
||||||
|
)
|
||||||
|
|
||||||
// NewDefaultUserSession create a default user session.
|
// NewDefaultUserSession create a default user session.
|
||||||
func NewDefaultUserSession() UserSession {
|
func NewDefaultUserSession() UserSession {
|
||||||
|
|
|
@ -25,4 +25,7 @@ const Year = Day * 365
|
||||||
// Month is an int based representation of the time unit.
|
// Month is an int based representation of the time unit.
|
||||||
const Month = Year / 12
|
const Month = Year / 12
|
||||||
|
|
||||||
|
// RFC3339Zero is the default value for time.Time.Unix().
|
||||||
|
const RFC3339Zero = int64(-62135596800)
|
||||||
|
|
||||||
const testStringInput = "abcdefghijkl"
|
const testStringInput = "abcdefghijkl"
|
||||||
|
|
|
@ -30,6 +30,37 @@ func SliceString(s string, d int) (array []string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsStringSlicesDifferent checks two slices of strings and on the first occurrence of a string item not existing in the
|
||||||
|
// other slice returns true, otherwise returns false.
|
||||||
|
func IsStringSlicesDifferent(a, b []string) (different bool) {
|
||||||
|
for _, s := range a {
|
||||||
|
if !IsStringInSlice(s, b) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range b {
|
||||||
|
if !IsStringInSlice(s, a) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlicesDelta takes a before and after []string and compares them returning a added and removed []string.
|
||||||
|
func StringSlicesDelta(before, after []string) (added, removed []string) {
|
||||||
|
for _, s := range before {
|
||||||
|
if !IsStringInSlice(s, after) {
|
||||||
|
removed = append(removed, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range after {
|
||||||
|
if !IsStringInSlice(s, before) {
|
||||||
|
added = append(added, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added, removed
|
||||||
|
}
|
||||||
|
|
||||||
// RandomString generate a random string of n characters.
|
// RandomString generate a random string of n characters.
|
||||||
func RandomString(n int, characters []rune) (randomString string) {
|
func RandomString(n int, characters []rune) (randomString string) {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldSplitIntoEvenStringsOfFour(t *testing.T) {
|
func TestShouldSplitIntoEvenStringsOfFour(t *testing.T) {
|
||||||
|
@ -35,3 +36,35 @@ func TestShouldSplitIntoUnevenStringsOfFour(t *testing.T) {
|
||||||
assert.Equal(t, "ijkl", arrayOfStrings[2])
|
assert.Equal(t, "ijkl", arrayOfStrings[2])
|
||||||
assert.Equal(t, "m", arrayOfStrings[3])
|
assert.Equal(t, "m", arrayOfStrings[3])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldFindSliceDifferencesDelta(t *testing.T) {
|
||||||
|
before := []string{"abc", "onetwothree"}
|
||||||
|
after := []string{"abc", "xyz"}
|
||||||
|
added, removed := StringSlicesDelta(before, after)
|
||||||
|
require.Len(t, added, 1)
|
||||||
|
require.Len(t, removed, 1)
|
||||||
|
assert.Equal(t, "onetwothree", removed[0])
|
||||||
|
assert.Equal(t, "xyz", added[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldNotFindSliceDifferencesDelta(t *testing.T) {
|
||||||
|
before := []string{"abc", "onetwothree"}
|
||||||
|
after := []string{"abc", "onetwothree"}
|
||||||
|
added, removed := StringSlicesDelta(before, after)
|
||||||
|
require.Len(t, added, 0)
|
||||||
|
require.Len(t, removed, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldFindSliceDifferences(t *testing.T) {
|
||||||
|
a := []string{"abc", "onetwothree"}
|
||||||
|
b := []string{"abc", "xyz"}
|
||||||
|
diff := IsStringSlicesDifferent(a, b)
|
||||||
|
assert.True(t, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldNotFindSliceDifferences(t *testing.T) {
|
||||||
|
a := []string{"abc", "onetwothree"}
|
||||||
|
b := []string{"abc", "onetwothree"}
|
||||||
|
diff := IsStringSlicesDifferent(a, b)
|
||||||
|
assert.False(t, diff)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue