feat(session): add redis sentinel provider (#1768)

* feat(session): add redis sentinel provider

* refactor(session): use int for ports as per go standards

* refactor(configuration): adjust tests and validation

* refactor(configuration): add err format consts

* refactor(configuration): explicitly map redis structs

* refactor(session): merge redis/redis sentinel providers

* refactor(session): add additional checks to redis providers

* feat(session): add redis cluster provider

* fix: update config for new values

* fix: provide nil certpool to affected tests/mocks

* test: add additional tests to cover uncovered code

* docs: expand explanation of host and nodes relation for redis

* ci: add redis-sentinel to suite highavailability, add redis-sentinel quorum

* fix(session): sentinel password

* test: use redis alpine library image for redis sentinel, use expose instead of ports, use redis ip, adjust redis ip range, adjust redis config

* test: make entrypoint.sh executable, fix entrypoint.sh if/elif

* test: add redis failover tests

* test: defer docker start, adjust sleep, attempt logout before login, attempt visit before login and tune timeouts, add additional logging

* test: add sentinel integration test

* test: add secondary node failure to tests, fix password usage, bump test timeout, add sleep

* feat: use sentinel failover cluster

* fix: renamed addrs to sentineladdrs upstream

* test(session): sentinel failover

* test: add redis standard back into testing

* test: move redis standalone test to traefik2

* fix/docs: apply suggestions from code review
pull/1793/head
James Elliott 2021-03-10 10:03:05 +11:00 committed by GitHub
parent 073c558296
commit e041143f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 5510 additions and 136 deletions

View File

@ -22,6 +22,7 @@ type HostEntry struct {
var hostEntries = []HostEntry{ var hostEntries = []HostEntry{
// For authelia backend. // For authelia backend.
{Domain: "authelia.example.com", IP: "192.168.240.50"}, {Domain: "authelia.example.com", IP: "192.168.240.50"},
// For common tests. // For common tests.
{Domain: "login.example.com", IP: "192.168.240.100"}, {Domain: "login.example.com", IP: "192.168.240.100"},
{Domain: "admin.example.com", IP: "192.168.240.100"}, {Domain: "admin.example.com", IP: "192.168.240.100"},
@ -34,14 +35,28 @@ var hostEntries = []HostEntry{
{Domain: "secure.example.com", IP: "192.168.240.100"}, {Domain: "secure.example.com", IP: "192.168.240.100"},
{Domain: "mail.example.com", IP: "192.168.240.100"}, {Domain: "mail.example.com", IP: "192.168.240.100"},
{Domain: "duo.example.com", IP: "192.168.240.100"}, {Domain: "duo.example.com", IP: "192.168.240.100"},
// For Traefik suite. // For Traefik suite.
{Domain: "traefik.example.com", IP: "192.168.240.100"}, {Domain: "traefik.example.com", IP: "192.168.240.100"},
// For HAProxy suite. // For HAProxy suite.
{Domain: "haproxy.example.com", IP: "192.168.240.100"}, {Domain: "haproxy.example.com", IP: "192.168.240.100"},
// For testing network ACLs. // For testing network ACLs.
{Domain: "proxy-client1.example.com", IP: "192.168.240.201"}, {Domain: "proxy-client1.example.com", IP: "192.168.240.201"},
{Domain: "proxy-client2.example.com", IP: "192.168.240.202"}, {Domain: "proxy-client2.example.com", IP: "192.168.240.202"},
{Domain: "proxy-client3.example.com", IP: "192.168.240.203"}, {Domain: "proxy-client3.example.com", IP: "192.168.240.203"},
// Redis Replicas
{Domain: "redis-node-0.example.com", IP: "192.168.240.110"},
{Domain: "redis-node-1.example.com", IP: "192.168.240.111"},
{Domain: "redis-node-2.example.com", IP: "192.168.240.112"},
// Redis Sentinel Replicas
{Domain: "redis-sentinel-0.example.com", IP: "192.168.240.120"},
{Domain: "redis-sentinel-1.example.com", IP: "192.168.240.121"},
{Domain: "redis-sentinel-2.example.com", IP: "192.168.240.122"},
// Kubernetes dashboard. // Kubernetes dashboard.
{Domain: "kubernetes.example.com", IP: "192.168.240.110"}, {Domain: "kubernetes.example.com", IP: "192.168.240.110"},
} }

View File

@ -115,7 +115,7 @@ func startServer() {
clock := utils.RealClock{} clock := utils.RealClock{}
authorizer := authorization.NewAuthorizer(config.AccessControl) authorizer := authorization.NewAuthorizer(config.AccessControl)
sessionProvider := session.NewProvider(config.Session) sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock) regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
providers := middlewares.Providers{ providers := middlewares.Providers{

View File

@ -8,6 +8,11 @@ port: 9091
# tls_key: /config/ssl/key.pem # tls_key: /config/ssl/key.pem
# tls_cert: /config/ssl/cert.pem # tls_cert: /config/ssl/cert.pem
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
## the system certificates store.
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
# certificates_directory: /config/certificates
# The theme to display: light, dark, grey # The theme to display: light, dark, grey
theme: light theme: light
@ -109,7 +114,7 @@ authentication_backend:
# The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>]. # The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>].
url: ldap://127.0.0.1 url: ldap://127.0.0.1
# Use StartTLS with the LDAP connection. # Use StartTLS with the LDAP connection.
start_tls: false start_tls: false
@ -118,6 +123,8 @@ authentication_backend:
# server_name: ldap.example.com # server_name: ldap.example.com
# Skip verifying the server certificate (to allow a self-signed certificate). # Skip verifying the server certificate (to allow a self-signed certificate).
## In preference to setting this we strongly recommend you add the public portion of the certificate to the
## certificates directory which is defined by the `certificates_directory` option at the top of the config.
skip_verify: false skip_verify: false
# Minimum TLS version for either Secure LDAP or LDAP StartTLS. # Minimum TLS version for either Secure LDAP or LDAP StartTLS.
@ -125,7 +132,7 @@ authentication_backend:
# The base dn for every entries. # The base dn for every entries.
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
# The attribute holding the username of the user. This attribute is used to populate # The attribute holding the username of the user. This attribute is used to populate
# the username in the session information. It was introduced due to #561 to handle case # the username in the session information. It was introduced due to #561 to handle case
# insensitive search queries. # insensitive search queries.
@ -138,13 +145,13 @@ authentication_backend:
# them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow # them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow
# https://www.ietf.org/rfc/rfc2307.txt. # https://www.ietf.org/rfc/rfc2307.txt.
# username_attribute: uid # username_attribute: uid
# An additional dn to define the scope to all users. # An additional dn to define the scope to all users.
additional_users_dn: ou=users additional_users_dn: ou=users
# The users filter used in search queries to find the user profile based on input filled in login form. # The users filter used in search queries to find the user profile based on input filled in login form.
# Various placeholders are available to represent the user input and back reference other options of the configuration: # Various placeholders are available to represent the user input and back reference other options of the configuration:
# - {input} is a placeholder replaced by what the user inputs in the login form. # - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username_attribute} is a mandatory placeholder replaced by what is configured in `username_attribute`. # - {username_attribute} is a mandatory placeholder replaced by what is configured in `username_attribute`.
# - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`. # - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`.
# - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it. # - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it.
@ -159,7 +166,7 @@ authentication_backend:
# An additional dn to define the scope of groups. # An additional dn to define the scope of groups.
additional_groups_dn: ou=groups additional_groups_dn: ou=groups
# The groups filter used in search queries to find the groups of the user. # The groups filter used in search queries to find the groups of the user.
# - {input} is a placeholder replaced by what the user inputs in the login form. # - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username} is a placeholder replace by the username stored in LDAP (based on `username_attribute`). # - {username} is a placeholder replace by the username stored in LDAP (based on `username_attribute`).
@ -270,8 +277,8 @@ access_control:
- 10.0.0.1 - 10.0.0.1
- domain: - domain:
- secure.example.com - secure.example.com
- private.example.com - private.example.com
policy: two_factor policy: two_factor
- domain: singlefactor.example.com - domain: singlefactor.example.com
@ -326,7 +333,7 @@ session:
# The name of the session cookie. (default: authelia_session). # The name of the session cookie. (default: authelia_session).
name: authelia_session name: authelia_session
# The secret to encrypt the session data. This is only used with Redis. # The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
# Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html # Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
secret: insecure_session_secret secret: insecure_session_secret
@ -348,19 +355,65 @@ session:
# is restricted to the subdomain of the issuer. # is restricted to the subdomain of the issuer.
domain: example.com domain: example.com
# The redis connection details ## The redis connection details
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
# Use a unix socket instead ## Use a unix socket instead
# host: /var/run/redis/redis.sock # host: /var/run/redis/redis.sock
## Optional username to be used with authentication.
username: authelia
# Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html ## Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
password: authelia password: authelia
# This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc).
## This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc).
database_index: 0 database_index: 0
## The maximum number of concurrent active connections to Redis.
maximum_active_connections: 8
## The target number of idle connections to have open ready for work. Useful when opening connections is slow.
minimum_idle_connections: 0
## The Redis TLS configuration. If defined will require a TLS connection to the Redis instance(s).
# tls:
## Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option).
# server_name: myredis.example.com
## Skip verifying the server certificate (to allow a self-signed certificate).
## In preference to setting this we strongly recommend you add the public portion of the certificate to the
## certificates directory which is defined by the `certificates_directory` option at the top of the config.
# skip_verify: false
## Minimum TLS version for the connection.
# minimum_version: TLS1.2
## The Redis HA configuration options.
## This provides specific options to Redis Sentinel, sentinel_name must be defined (Master Name).
# high_availability:
## Sentinel Name / Master Name
# sentinel_name: mysentinel
## Specific password for Redis Sentinel. The node username and password is configured above.
# sentinel_password: sentinel_specific_pass
## The additional nodes to pre-seed the redis provider with (for sentinel).
## If the host in the above section is defined, it will be combined with this list to connect to sentinel.
## For high availability to be used you must have either defined; the host above or at least one node below.
# nodes:
# - host: sentinel-node1
# port: 6379
# - host: sentinel-node2
# port: 6379
## Choose the host with the lowest latency.
# route_by_latency: false
## Choose the host randomly.
# route_randomly: false
# Configuration of the authentication regulation mechanism. # Configuration of the authentication regulation mechanism.
# #
# This mechanism prevents attackers from brute forcing the first factor. # This mechanism prevents attackers from brute forcing the first factor.
@ -446,7 +499,9 @@ notifier:
# Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option). # Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option).
# server_name: smtp.example.com # server_name: smtp.example.com
# Skip verifying the server certificate (to allow a self-signed certificate). ## Skip verifying the server certificate (to allow a self-signed certificate).
## In preference to setting this we strongly recommend you add the public portion of the certificate to the
## certificates directory which is defined by the `certificates_directory` option at the top of the config.
skip_verify: false skip_verify: false
# Minimum TLS version for either StartTLS or SMTPS. # Minimum TLS version for either StartTLS or SMTPS.

View File

@ -48,11 +48,60 @@ session:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
# Use a unix socket instead ## Use a unix socket instead
# host: /var/run/redis/redis.sock # host: /var/run/redis/redis.sock
# Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html ## Optional username to be used with authentication.
username: authelia
## Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
password: authelia password: authelia
## This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc).
database_index: 0
## The maximum number of concurrent active connections to Redis.
maximum_active_connections: 8
## The target number of idle connections to have open ready for work. Useful when opening connections is slow.
minimum_idle_connections: 0
## The Redis TLS configuration. If defined will require a TLS connection to the Redis instance(s).
tls:
## Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option).
server_name: myredis.example.com
## Skip verifying the server certificate (to allow a self-signed certificate).
## In preference to setting this we strongly recommend you add the public portion of the certificate to the
## certificates directory which is defined by the `certificates_directory` option at the top of the config.
skip_verify: false
## Minimum TLS version for the connection.
minimum_version: TLS1.2
## The Redis HA configuration options.
## This provides specific options to Redis Sentinel, sentinel_name must be defined (Master Name).
high_availability:
## Sentinel Name / Master Name
sentinel_name: mysentinel
## Specific password for Redis Sentinel. The node username and password is configured above.
sentinel_password: sentinel_specific_pass
## The additional nodes to pre-seed the redis provider with (for sentinel).
## If the host in the above section is defined, it will be combined with this list to connect to sentinel.
## For high availability to be used you must have either defined; the host above or at least one node below.
nodes:
- host: sentinel-node1
port: 6379
- host: sentinel-node2
port: 6379
## Choose the host with the lowest latency.
route_by_latency: false
## Choose the host randomly.
route_randomly: false
``` ```
### Security ### Security
@ -74,4 +123,9 @@ host: "[fd00:1111:2222:3333::1]"
## 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).
## Redis Sentinel
When using Redis Sentinel, the host specified in the main redis section is added (it will be the first node) to the
nodes in the high availability section. This however is optional.

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/Workiva/go-datastructures v1.0.52 github.com/Workiva/go-datastructures v1.0.52
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/authelia/session/v2 v2.4.1 github.com/authelia/session/v2 v2.5.7
github.com/deckarep/golang-set v1.7.1 github.com/deckarep/golang-set v1.7.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/duosecurity/duo_api_golang v0.0.0-20201112143038-0e07e9f869e3 github.com/duosecurity/duo_api_golang v0.0.0-20201112143038-0e07e9f869e3

12
go.sum
View File

@ -39,8 +39,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/authelia/session/v2 v2.4.1 h1:/c/imfsdr380VfZ3Do1nFma+wvfZlrYQuR299UEyZBs= github.com/authelia/session/v2 v2.5.7 h1:cdF7cod8Lgw7KavtyQstP511Sov10FafIuGnx+w3a/M=
github.com/authelia/session/v2 v2.4.1/go.mod h1:YzuxG4Aj5aFVxO49g9Upvye4BNaTNFjg6rIEgZJ3BQI= github.com/authelia/session/v2 v2.5.7/go.mod h1:0bZpmr+V7hL2DVPyutiC+1lcNjdYVVmx3vbZUdigD6c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
@ -323,9 +323,9 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/savsgio/dictpool v0.0.0-20210105101557-9da1bc2fbfce h1:PRDREQ3VGiocUySEdKYQpwdyxDx+e4uKdjVaQvwIR5I= github.com/savsgio/dictpool v0.0.0-20210217113430-85d3b37fb239 h1:aTxmMsYGLUZfj0EsWaJ1s0HnctxCgjRw3A+TFoO1Tsc=
github.com/savsgio/dictpool v0.0.0-20210105101557-9da1bc2fbfce/go.mod h1:TNr2IIMnYd9/KYEpTVHVrnfmjizlKPTSgkWUbjyof+A= github.com/savsgio/dictpool v0.0.0-20210217113430-85d3b37fb239/go.mod h1:CfPSewBwpXF/05Izyk9s379O1ysmtUajFVr1nOD83Fs=
github.com/savsgio/gotils v0.0.0-20210105085219-0567298fdcac/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8= github.com/savsgio/gotils v0.0.0-20210217112953-d4a072536008/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8=
github.com/savsgio/gotils v0.0.0-20210225112730-595c7e5a8a7a h1:9AQ3IfP72fCdbYAJNNwovzXrarhaWtxosEuN1fpent0= github.com/savsgio/gotils v0.0.0-20210225112730-595c7e5a8a7a h1:9AQ3IfP72fCdbYAJNNwovzXrarhaWtxosEuN1fpent0=
github.com/savsgio/gotils v0.0.0-20210225112730-595c7e5a8a7a/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8= github.com/savsgio/gotils v0.0.0-20210225112730-595c7e5a8a7a/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
@ -383,7 +383,7 @@ github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGB
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.19.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= github.com/valyala/fasthttp v1.21.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A=
github.com/valyala/fasthttp v1.22.0 h1:OpwH5KDOJ9cS2bq8fD+KfT4IrksK0llvkHf4MZx42jQ= github.com/valyala/fasthttp v1.22.0 h1:OpwH5KDOJ9cS2bq8fD+KfT4IrksK0llvkHf4MZx42jQ=
github.com/valyala/fasthttp v1.22.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU= github.com/valyala/fasthttp v1.22.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=

View File

@ -8,6 +8,11 @@ port: 9091
# tls_key: /config/ssl/key.pem # tls_key: /config/ssl/key.pem
# tls_cert: /config/ssl/cert.pem # tls_cert: /config/ssl/cert.pem
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
## the system certificates store.
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
# certificates_directory: /config/certificates
# The theme to display: light, dark, grey # The theme to display: light, dark, grey
theme: light theme: light
@ -109,7 +114,7 @@ authentication_backend:
# The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>]. # The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>].
url: ldap://127.0.0.1 url: ldap://127.0.0.1
# Use StartTLS with the LDAP connection. # Use StartTLS with the LDAP connection.
start_tls: false start_tls: false
@ -118,6 +123,8 @@ authentication_backend:
# server_name: ldap.example.com # server_name: ldap.example.com
# Skip verifying the server certificate (to allow a self-signed certificate). # Skip verifying the server certificate (to allow a self-signed certificate).
## In preference to setting this we strongly recommend you add the public portion of the certificate to the
## certificates directory which is defined by the `certificates_directory` option at the top of the config.
skip_verify: false skip_verify: false
# Minimum TLS version for either Secure LDAP or LDAP StartTLS. # Minimum TLS version for either Secure LDAP or LDAP StartTLS.
@ -125,7 +132,7 @@ authentication_backend:
# The base dn for every entries. # The base dn for every entries.
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
# The attribute holding the username of the user. This attribute is used to populate # The attribute holding the username of the user. This attribute is used to populate
# the username in the session information. It was introduced due to #561 to handle case # the username in the session information. It was introduced due to #561 to handle case
# insensitive search queries. # insensitive search queries.
@ -138,13 +145,13 @@ authentication_backend:
# them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow # them, we instead advise to use the attributes mentioned above (sAMAccountName and uid) to follow
# https://www.ietf.org/rfc/rfc2307.txt. # https://www.ietf.org/rfc/rfc2307.txt.
# username_attribute: uid # username_attribute: uid
# An additional dn to define the scope to all users. # An additional dn to define the scope to all users.
additional_users_dn: ou=users additional_users_dn: ou=users
# The users filter used in search queries to find the user profile based on input filled in login form. # The users filter used in search queries to find the user profile based on input filled in login form.
# Various placeholders are available to represent the user input and back reference other options of the configuration: # Various placeholders are available to represent the user input and back reference other options of the configuration:
# - {input} is a placeholder replaced by what the user inputs in the login form. # - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username_attribute} is a mandatory placeholder replaced by what is configured in `username_attribute`. # - {username_attribute} is a mandatory placeholder replaced by what is configured in `username_attribute`.
# - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`. # - {mail_attribute} is a placeholder replaced by what is configured in `mail_attribute`.
# - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it. # - DON'T USE - {0} is an alias for {input} supported for backward compatibility but it will be deprecated in later versions, so please don't use it.
@ -159,7 +166,7 @@ authentication_backend:
# An additional dn to define the scope of groups. # An additional dn to define the scope of groups.
additional_groups_dn: ou=groups additional_groups_dn: ou=groups
# The groups filter used in search queries to find the groups of the user. # The groups filter used in search queries to find the groups of the user.
# - {input} is a placeholder replaced by what the user inputs in the login form. # - {input} is a placeholder replaced by what the user inputs in the login form.
# - {username} is a placeholder replace by the username stored in LDAP (based on `username_attribute`). # - {username} is a placeholder replace by the username stored in LDAP (based on `username_attribute`).
@ -270,8 +277,8 @@ access_control:
- 10.0.0.1 - 10.0.0.1
- domain: - domain:
- secure.example.com - secure.example.com
- private.example.com - private.example.com
policy: two_factor policy: two_factor
- domain: singlefactor.example.com - domain: singlefactor.example.com
@ -326,7 +333,7 @@ session:
# The name of the session cookie. (default: authelia_session). # The name of the session cookie. (default: authelia_session).
name: authelia_session name: authelia_session
# The secret to encrypt the session data. This is only used with Redis. # The secret to encrypt the session data. This is only used with Redis / Redis Sentinel.
# Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html # Secret can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
secret: insecure_session_secret secret: insecure_session_secret
@ -348,19 +355,65 @@ session:
# is restricted to the subdomain of the issuer. # is restricted to the subdomain of the issuer.
domain: example.com domain: example.com
# The redis connection details ## The redis connection details
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
# Use a unix socket instead ## Use a unix socket instead
# host: /var/run/redis/redis.sock # host: /var/run/redis/redis.sock
## Optional username to be used with authentication.
username: authelia
# Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html ## Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html
password: authelia password: authelia
# This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc).
## This is the Redis DB Index https://redis.io/commands/select (sometimes referred to as database number, DB, etc).
database_index: 0 database_index: 0
## The maximum number of concurrent active connections to Redis.
maximum_active_connections: 8
## The target number of idle connections to have open ready for work. Useful when opening connections is slow.
minimum_idle_connections: 0
## The Redis TLS configuration. If defined will require a TLS connection to the Redis instance(s).
# tls:
## Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option).
# server_name: myredis.example.com
## Skip verifying the server certificate (to allow a self-signed certificate).
## In preference to setting this we strongly recommend you add the public portion of the certificate to the
## certificates directory which is defined by the `certificates_directory` option at the top of the config.
# skip_verify: false
## Minimum TLS version for the connection.
# minimum_version: TLS1.2
## The Redis HA configuration options.
## This provides specific options to Redis Sentinel, sentinel_name must be defined (Master Name).
# high_availability:
## Sentinel Name / Master Name
# sentinel_name: mysentinel
## Specific password for Redis Sentinel. The node username and password is configured above.
# sentinel_password: sentinel_specific_pass
## The additional nodes to pre-seed the redis provider with (for sentinel).
## If the host in the above section is defined, it will be combined with this list to connect to sentinel.
## For high availability to be used you must have either defined; the host above or at least one node below.
# nodes:
# - host: sentinel-node1
# port: 6379
# - host: sentinel-node2
# port: 6379
## Choose the host with the lowest latency.
# route_by_latency: false
## Choose the host randomly.
# route_randomly: false
# Configuration of the authentication regulation mechanism. # Configuration of the authentication regulation mechanism.
# #
# This mechanism prevents attackers from brute forcing the first factor. # This mechanism prevents attackers from brute forcing the first factor.
@ -446,7 +499,9 @@ notifier:
# Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option). # Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option).
# server_name: smtp.example.com # server_name: smtp.example.com
# Skip verifying the server certificate (to allow a self-signed certificate). ## Skip verifying the server certificate (to allow a self-signed certificate).
## In preference to setting this we strongly recommend you add the public portion of the certificate to the
## certificates directory which is defined by the `certificates_directory` option at the top of the config.
skip_verify: false skip_verify: false
# Minimum TLS version for either StartTLS or SMTPS. # Minimum TLS version for either StartTLS or SMTPS.

View File

@ -1,11 +1,31 @@
package schema package schema
// RedisNode Represents a Node.
type RedisNode struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
// RedisHighAvailabilityConfiguration holds configuration variables for Redis Cluster/Sentinel.
type RedisHighAvailabilityConfiguration struct {
SentinelName string `mapstructure:"sentinel_name"`
SentinelPassword string `mapstructure:"sentinel_password"`
Nodes []RedisNode `mapstructure:"nodes"`
RouteByLatency bool `mapstructure:"route_by_latency"`
RouteRandomly bool `mapstructure:"route_randomly"`
}
// RedisSessionConfiguration represents the configuration related to redis session store. // RedisSessionConfiguration represents the configuration related to redis session store.
type RedisSessionConfiguration struct { type RedisSessionConfiguration struct {
Host string `mapstructure:"host"` Host string `mapstructure:"host"`
Port int64 `mapstructure:"port"` Port int `mapstructure:"port"`
Password string `mapstructure:"password"` Username string `mapstructure:"username"`
DatabaseIndex int `mapstructure:"database_index"` Password string `mapstructure:"password"`
DatabaseIndex int `mapstructure:"database_index"`
MaximumActiveConnections int `mapstructure:"maximum_active_connections"`
MinimumIdleConnections int `mapstructure:"minimum_idle_connections"`
TLS *TLSConfig `mapstructure:"tls"`
HighAvailability *RedisHighAvailabilityConfiguration `mapstructure:"high_availability"`
} }
// SessionConfiguration represents the configuration related to user sessions. // SessionConfiguration represents the configuration related to user sessions.

View File

@ -42,8 +42,24 @@ var validKeys = []string{
// Redis Session Keys. // Redis Session Keys.
"session.redis.host", "session.redis.host",
"session.redis.port", "session.redis.port",
"session.redis.username",
"session.redis.password", "session.redis.password",
"session.redis.database_index", "session.redis.database_index",
"session.redis.maximum_active_connections",
"session.redis.minimum_idle_connections",
"session.redis.tls.minimum_version",
"session.redis.tls.skip_verify",
"session.redis.tls.server_name",
"session.redis.high_availability.sentinel_name",
"session.redis.high_availability.sentinel_password",
"session.redis.high_availability.nodes",
"session.redis.high_availability.route_by_latency",
"session.redis.high_availability.route_randomly",
"session.redis.timeouts.dial",
"session.redis.timeouts.idle",
"session.redis.timeouts.pool",
"session.redis.timeouts.read",
"session.redis.timeouts.write",
// Local Storage Keys. // Local Storage Keys.
"storage.local.path", "storage.local.path",
@ -171,6 +187,11 @@ var specificErrorKeys = map[string]string{
"authentication_backend.file.hashing.parallelism": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", "authentication_backend.file.hashing.parallelism": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password",
} }
const errFmtSessionSecretRedisProvider = "The session secret must be set when using the %s session provider"
const errFmtSessionRedisPortRange = "The port must be between 1 and 65535 for the %s session provider"
const errFmtSessionRedisHostRequired = "The host must be provided when using the %s session provider"
const errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
const denyPolicy = "deny" const denyPolicy = "deny"
const bypassPolicy = "bypass" const bypassPolicy = "bypass"

View File

@ -16,15 +16,21 @@ func ValidateSession(configuration *schema.SessionConfiguration, validator *sche
} }
if configuration.Redis != nil { if configuration.Redis != nil {
if configuration.Secret == "" { if configuration.Redis.HighAvailability != nil {
validator.Push(errors.New("Set secret of the session object")) if configuration.Redis.HighAvailability.SentinelName != "" {
} validateRedisSentinel(configuration, validator)
} else {
if !strings.HasPrefix(configuration.Redis.Host, "/") && configuration.Redis.Port == 0 { validator.Push(fmt.Errorf("Session provider redis is configured for high availability but doesn't have a sentinel_name which is required"))
validator.Push(errors.New("A redis port different than 0 must be provided")) }
} else {
validateRedis(configuration, validator)
} }
} }
validateSession(configuration, validator)
}
func validateSession(configuration *schema.SessionConfiguration, validator *schema.StructValidator) {
if configuration.Expiration == "" { if configuration.Expiration == "" {
configuration.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour configuration.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour
} else if _, err := utils.ParseDurationString(configuration.Expiration); err != nil { } else if _, err := utils.ParseDurationString(configuration.Expiration); err != nil {
@ -51,3 +57,56 @@ func ValidateSession(configuration *schema.SessionConfiguration, validator *sche
validator.Push(errors.New("The domain of the session must be the root domain you're protecting instead of a wildcard domain")) validator.Push(errors.New("The domain of the session must be the root domain you're protecting instead of a wildcard domain"))
} }
} }
func validateRedis(configuration *schema.SessionConfiguration, validator *schema.StructValidator) {
if configuration.Redis.Host == "" {
validator.Push(fmt.Errorf(errFmtSessionRedisHostRequired, "redis"))
}
if configuration.Secret == "" {
validator.Push(fmt.Errorf(errFmtSessionSecretRedisProvider, "redis"))
}
if !strings.HasPrefix(configuration.Redis.Host, "/") && configuration.Redis.Port == 0 {
validator.Push(errors.New("A redis port different than 0 must be provided"))
} else if configuration.Redis.Port < 0 || configuration.Redis.Port > 65535 {
validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, "redis"))
}
if configuration.Redis.MaximumActiveConnections <= 0 {
configuration.Redis.MaximumActiveConnections = 8
}
}
func validateRedisSentinel(configuration *schema.SessionConfiguration, validator *schema.StructValidator) {
if configuration.Redis.Port == 0 {
configuration.Redis.Port = 26379
} else if configuration.Redis.Port < 0 || configuration.Redis.Port > 65535 {
validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, "redis sentinel"))
}
validateHighAvailability(configuration, validator, "redis sentinel")
}
func validateHighAvailability(configuration *schema.SessionConfiguration, validator *schema.StructValidator, provider string) {
if configuration.Redis.Host == "" && len(configuration.Redis.HighAvailability.Nodes) == 0 {
validator.Push(fmt.Errorf(errFmtSessionRedisHostOrNodesRequired, provider))
}
if configuration.Secret == "" {
validator.Push(fmt.Errorf(errFmtSessionSecretRedisProvider, provider))
}
for i, node := range configuration.Redis.HighAvailability.Nodes {
if node.Host == "" {
validator.Push(fmt.Errorf("The %s nodes require a host set but you have not set the host for one or more nodes", provider))
break
}
if node.Port == 0 {
if provider == "redis sentinel" {
configuration.Redis.HighAvailability.Nodes[i].Port = 26379
}
}
}
}

View File

@ -1,9 +1,11 @@
package validator package validator
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
) )
@ -22,7 +24,8 @@ func TestShouldSetDefaultSessionName(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name) assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name)
} }
@ -32,7 +35,8 @@ func TestShouldSetDefaultSessionInactivity(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
} }
@ -42,7 +46,8 @@ func TestShouldSetDefaultSessionExpiration(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration)
} }
@ -64,10 +69,47 @@ func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, 8, config.Redis.MaximumActiveConnections)
} }
func TestShouldRaiseErrorWhenRedisIsUsedAndPasswordNotSet(t *testing.T) { func TestShouldRaiseErrorWithInvalidRedisPortLow(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Host: "authelia-port-1",
Port: -1,
}
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis"))
}
func TestShouldRaiseErrorWithInvalidRedisPortHigh(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Host: "authelia-port-1",
Port: 65536,
}
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis"))
}
func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
config.Secret = "" config.Secret = ""
@ -85,8 +127,9 @@ func TestShouldRaiseErrorWhenRedisIsUsedAndPasswordNotSet(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Set secret of the session object") assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis"))
} }
func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) { func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) {
@ -106,10 +149,214 @@ func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "A redis port different than 0 must be provided") assert.EqualError(t, validator.Errors()[0], "A redis port different than 0 must be provided")
} }
func TestShouldRaiseOneErrorWhenRedisHighAvailabilityHasNodesWithNoHost(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Host: "redis",
Port: 6379,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelName: "authelia-sentinel",
SentinelPassword: "abc123",
Nodes: []schema.RedisNode{
{
Port: 26379,
},
{
Port: 26379,
},
},
},
}
ValidateSession(&config, validator)
errors := validator.Errors()
assert.False(t, validator.HasWarnings())
require.Len(t, errors, 1)
assert.EqualError(t, errors[0], "The redis sentinel nodes require a host set but you have not set the host for one or more nodes")
}
func TestShouldRaiseOneErrorWhenRedisHighAvailabilityDoesNotHaveSentinelName(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Host: "redis",
Port: 6379,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelPassword: "abc123",
},
}
ValidateSession(&config, validator)
errors := validator.Errors()
assert.False(t, validator.HasWarnings())
require.Len(t, errors, 1)
assert.EqualError(t, errors[0], "Session provider redis is configured for high availability but doesn't have a sentinel_name which is required")
}
func TestShouldUpdateDefaultPortWhenRedisSentinelHasNodes(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Host: "redis",
Port: 6379,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelName: "authelia-sentinel",
SentinelPassword: "abc123",
Nodes: []schema.RedisNode{
{
Host: "node-1",
Port: 333,
},
{
Host: "node-2",
},
{
Host: "node-3",
},
},
},
}
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, 333, config.Redis.HighAvailability.Nodes[0].Port)
assert.Equal(t, 26379, config.Redis.HighAvailability.Nodes[1].Port)
assert.Equal(t, 26379, config.Redis.HighAvailability.Nodes[2].Port)
}
func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Secret = ""
config.Redis = &schema.RedisSessionConfiguration{
Port: 65536,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelName: "sentinel",
SentinelPassword: "abc123",
Nodes: []schema.RedisNode{
{
Host: "node1",
Port: 26379,
},
},
RouteByLatency: true,
RouteRandomly: true,
},
}
ValidateSession(&config, validator)
errors := validator.Errors()
assert.False(t, validator.HasWarnings())
require.Len(t, errors, 2)
assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis sentinel"))
assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis sentinel"))
validator.Clear()
config.Redis.Port = -1
ValidateSession(&config, validator)
errors = validator.Errors()
assert.False(t, validator.HasWarnings())
require.Len(t, errors, 2)
assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis sentinel"))
assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis sentinel"))
}
func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisSentinelPortBlank(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Host: "mysentinelHost",
Port: 0,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelName: "sentinel",
SentinelPassword: "abc123",
Nodes: []schema.RedisNode{
{
Host: "node1",
Port: 26379,
},
},
RouteByLatency: true,
RouteRandomly: true,
},
}
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, 26379, config.Redis.Port)
}
func TestShouldRaiseErrorWhenRedisHostAndHighAvailabilityNodesEmpty(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Port: 26379,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelName: "sentinel",
SentinelPassword: "abc123",
RouteByLatency: true,
RouteRandomly: true,
},
}
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisHostOrNodesRequired, "redis sentinel"))
}
func TestShouldRaiseErrorsWhenRedisHostNotSet(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
config.Redis = &schema.RedisSessionConfiguration{
Port: 6379,
}
ValidateSession(&config, validator)
errors := validator.Errors()
assert.False(t, validator.HasWarnings())
require.Len(t, errors, 1)
assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisHostRequired, "redis"))
}
func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) { func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
@ -117,6 +364,7 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Set domain of the session object") assert.EqualError(t, validator.Errors()[0], "Set domain of the session object")
} }
@ -128,6 +376,7 @@ func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "The domain of the session must be the root domain you're protecting instead of a wildcard domain") assert.EqualError(t, validator.Errors()[0], "The domain of the session must be the root domain you're protecting instead of a wildcard domain")
} }
@ -140,6 +389,7 @@ func TestShouldRaiseErrorWhenBadInactivityAndExpirationSet(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 2) assert.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "Error occurred parsing session expiration string: Could not convert the input string of -1 into a duration") assert.EqualError(t, validator.Errors()[0], "Error occurred parsing session expiration string: Could not convert the input string of -1 into a duration")
assert.EqualError(t, validator.Errors()[1], "Error occurred parsing session inactivity string: Could not convert the input string of -1 into a duration") assert.EqualError(t, validator.Errors()[1], "Error occurred parsing session inactivity string: Could not convert the input string of -1 into a duration")
@ -152,6 +402,7 @@ func TestShouldRaiseErrorWhenBadRememberMeDurationSet(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Error occurred parsing session remember_me_duration string: Could not convert the input string of 1 year into a duration") assert.EqualError(t, validator.Errors()[0], "Error occurred parsing session remember_me_duration string: Could not convert the input string of 1 year into a duration")
} }
@ -162,6 +413,7 @@ func TestShouldSetDefaultRememberMeDuration(t *testing.T) {
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, config.RememberMeDuration, schema.DefaultSessionConfiguration.RememberMeDuration) assert.Equal(t, config.RememberMeDuration, schema.DefaultSessionConfiguration.RememberMeDuration)
} }

View File

@ -617,7 +617,7 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
mock.Ctx.Configuration.Session.Inactivity = testInactivity mock.Ctx.Configuration.Session.Inactivity = testInactivity
// Reload the session provider since the configuration is indirect. // Reload the session provider since the configuration is indirect.
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session) mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
@ -650,7 +650,7 @@ func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *test
mock.Ctx.Configuration.Session.Inactivity = "10s" mock.Ctx.Configuration.Session.Inactivity = "10s"
// Reload the session provider since the configuration is indirect. // Reload the session provider since the configuration is indirect.
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session) mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
@ -747,7 +747,7 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin
mock.Ctx.Configuration.Session.Inactivity = testInactivity mock.Ctx.Configuration.Session.Inactivity = testInactivity
// Reload the session provider since the configuration is indirect. // Reload the session provider since the configuration is indirect.
mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session) mock.Ctx.Providers.SessionProvider = session.NewProvider(mock.Ctx.Configuration.Session, nil)
assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity) assert.Equal(t, time.Second*10, mock.Ctx.Providers.SessionProvider.Inactivity)
past := clock.Now().Add(-1 * time.Hour) past := clock.Now().Add(-1 * time.Hour)

View File

@ -18,7 +18,7 @@ func TestShouldCallNextWithAutheliaCtx(t *testing.T) {
ctx := &fasthttp.RequestCtx{} ctx := &fasthttp.RequestCtx{}
configuration := schema.Configuration{} configuration := schema.Configuration{}
userProvider := mocks.NewMockUserProvider(ctrl) userProvider := mocks.NewMockUserProvider(ctrl)
sessionProvider := session.NewProvider(configuration.Session) sessionProvider := session.NewProvider(configuration.Session, nil)
providers := middlewares.Providers{ providers := middlewares.Providers{
UserProvider: userProvider, UserProvider: userProvider,
SessionProvider: sessionProvider, SessionProvider: sessionProvider,

View File

@ -108,7 +108,7 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
configuration.AccessControl) configuration.AccessControl)
providers.SessionProvider = session.NewProvider( providers.SessionProvider = session.NewProvider(
configuration.Session) configuration.Session, nil)
providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider, &mockAuthelia.Clock) providers.Regulator = regulation.NewRegulator(configuration.Regulation, providers.StorageProvider, &mockAuthelia.Clock)

View File

@ -1,6 +1,7 @@
package session package session
import ( import (
"crypto/x509"
"encoding/json" "encoding/json"
"time" "time"
@ -10,6 +11,7 @@ import (
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/logging"
"github.com/authelia/authelia/internal/utils" "github.com/authelia/authelia/internal/utils"
) )
@ -21,42 +23,51 @@ type Provider struct {
} }
// NewProvider instantiate a session provider given a configuration. // NewProvider instantiate a session provider given a configuration.
func NewProvider(configuration schema.SessionConfiguration) *Provider { func NewProvider(configuration schema.SessionConfiguration, certPool *x509.CertPool) *Provider {
providerConfig := NewProviderConfig(configuration) providerConfig := NewProviderConfig(configuration, certPool)
provider := new(Provider) provider := new(Provider)
provider.sessionHolder = fasthttpsession.New(providerConfig.config) provider.sessionHolder = fasthttpsession.New(providerConfig.config)
logger := logging.Logger()
duration, err := utils.ParseDurationString(configuration.RememberMeDuration) duration, err := utils.ParseDurationString(configuration.RememberMeDuration)
if err != nil { if err != nil {
panic(err) logger.Fatal(err)
} }
provider.RememberMe = duration provider.RememberMe = duration
duration, err = utils.ParseDurationString(configuration.Inactivity) duration, err = utils.ParseDurationString(configuration.Inactivity)
if err != nil { if err != nil {
panic(err) logger.Fatal(err)
} }
provider.Inactivity = duration provider.Inactivity = duration
var providerImpl fasthttpsession.Provider var providerImpl fasthttpsession.Provider
if providerConfig.redisConfig != nil {
switch {
case providerConfig.redisConfig != nil:
providerImpl, err = redis.New(*providerConfig.redisConfig) providerImpl, err = redis.New(*providerConfig.redisConfig)
if err != nil { if err != nil {
panic(err) logger.Fatal(err)
} }
} else { case providerConfig.redisSentinelConfig != nil:
providerImpl, err = redis.NewFailoverCluster(*providerConfig.redisSentinelConfig)
if err != nil {
logger.Fatal(err)
}
default:
providerImpl, err = memory.New(memory.Config{}) providerImpl, err = memory.New(memory.Config{})
if err != nil { if err != nil {
panic(err) logger.Fatal(err)
} }
} }
err = provider.sessionHolder.SetProvider(providerImpl) err = provider.sessionHolder.SetProvider(providerImpl)
if err != nil { if err != nil {
panic(err) logger.Fatal(err)
} }
return provider return provider

View File

@ -1,11 +1,13 @@
package session package session
import ( import (
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"strings"
"github.com/authelia/session/v2" "github.com/authelia/session/v2"
"github.com/authelia/session/v2/providers/redis" "github.com/authelia/session/v2/providers/redis"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
@ -13,7 +15,7 @@ import (
) )
// NewProviderConfig creates a configuration for creating the session provider. // NewProviderConfig creates a configuration for creating the session provider.
func NewProviderConfig(configuration schema.SessionConfiguration) ProviderConfig { func NewProviderConfig(configuration schema.SessionConfiguration, certPool *x509.CertPool) ProviderConfig {
config := session.NewDefaultConfig() config := session.NewDefaultConfig()
// Override the cookie name. // Override the cookie name.
@ -35,42 +37,88 @@ func NewProviderConfig(configuration schema.SessionConfiguration) ProviderConfig
var redisConfig *redis.Config var redisConfig *redis.Config
var redisSentinelConfig *redis.FailoverConfig
var providerName string var providerName string
// If redis configuration is provided, then use the redis provider. // If redis configuration is provided, then use the redis provider.
if configuration.Redis != nil { switch {
providerName = "redis" case configuration.Redis != nil:
serializer := NewEncryptingSerializer(configuration.Secret) serializer := NewEncryptingSerializer(configuration.Secret)
network := "tcp"
var addr string var tlsConfig *tls.Config
if configuration.Redis.Port == 0 { if configuration.Redis.TLS != nil {
network = "unix" tlsConfig = utils.NewTLSConfig(configuration.Redis.TLS, tls.VersionTLS12, certPool)
addr = configuration.Redis.Host }
if configuration.Redis.HighAvailability != nil && configuration.Redis.HighAvailability.SentinelName != "" {
addrs := make([]string, 0)
if configuration.Redis.Host != "" {
addrs = append(addrs, fmt.Sprintf("%s:%d", strings.ToLower(configuration.Redis.Host), configuration.Redis.Port))
}
for _, node := range configuration.Redis.HighAvailability.Nodes {
addr := fmt.Sprintf("%s:%d", strings.ToLower(node.Host), node.Port)
if !utils.IsStringInSlice(addr, addrs) {
addrs = append(addrs, addr)
}
}
providerName = "redis-sentinel"
redisSentinelConfig = &redis.FailoverConfig{
MasterName: configuration.Redis.HighAvailability.SentinelName,
SentinelAddrs: addrs,
SentinelPassword: configuration.Redis.HighAvailability.SentinelPassword,
RouteByLatency: configuration.Redis.HighAvailability.RouteByLatency,
RouteRandomly: configuration.Redis.HighAvailability.RouteRandomly,
Username: configuration.Redis.Username,
Password: configuration.Redis.Password,
DB: configuration.Redis.DatabaseIndex, // DB is the fasthttp/session property for the Redis DB Index.
PoolSize: configuration.Redis.MaximumActiveConnections,
MinIdleConns: configuration.Redis.MinimumIdleConnections,
IdleTimeout: 300,
TLSConfig: tlsConfig,
KeyPrefix: "authelia-session",
}
} else { } else {
addr = fmt.Sprintf("%s:%d", configuration.Redis.Host, configuration.Redis.Port) providerName = "redis"
network := "tcp"
var addr string
if configuration.Redis.Port == 0 {
network = "unix"
addr = configuration.Redis.Host
} else {
addr = fmt.Sprintf("%s:%d", configuration.Redis.Host, configuration.Redis.Port)
}
redisConfig = &redis.Config{
Network: network,
Addr: addr,
Username: configuration.Redis.Username,
Password: configuration.Redis.Password,
DB: configuration.Redis.DatabaseIndex, // DB is the fasthttp/session property for the Redis DB Index.
PoolSize: configuration.Redis.MaximumActiveConnections,
MinIdleConns: configuration.Redis.MinimumIdleConnections,
IdleTimeout: 300,
TLSConfig: tlsConfig,
KeyPrefix: "authelia-session",
}
} }
redisConfig = &redis.Config{
Network: network,
Addr: addr,
Password: configuration.Redis.Password,
// DB is the fasthttp/session property for the Redis DB Index.
DB: configuration.Redis.DatabaseIndex,
PoolSize: 8,
IdleTimeout: 300,
KeyPrefix: "authelia-session",
}
config.EncodeFunc = serializer.Encode config.EncodeFunc = serializer.Encode
config.DecodeFunc = serializer.Decode config.DecodeFunc = serializer.Decode
} else { // if no option is provided, use the memory provider. default:
providerName = "memory" providerName = "memory"
} }
return ProviderConfig{ return ProviderConfig{
config: config, config,
redisConfig: redisConfig, redisConfig,
providerName: providerName, redisSentinelConfig,
providerName,
} }
} }

View File

@ -2,11 +2,11 @@ package session
import ( import (
"crypto/sha256" "crypto/sha256"
"crypto/tls"
"testing" "testing"
"time" "time"
"github.com/authelia/session/v2" "github.com/authelia/session/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -20,7 +20,7 @@ func TestShouldCreateInMemorySessionProvider(t *testing.T) {
configuration.Domain = testDomain configuration.Domain = testDomain
configuration.Name = testName configuration.Name = testName
configuration.Expiration = testExpiration configuration.Expiration = testExpiration
providerConfig := NewProviderConfig(configuration) providerConfig := NewProviderConfig(configuration, nil)
assert.Equal(t, "my_session", providerConfig.config.CookieName) assert.Equal(t, "my_session", providerConfig.config.CookieName)
assert.Equal(t, testDomain, providerConfig.config.Domain) assert.Equal(t, testDomain, providerConfig.config.Domain)
@ -31,8 +31,7 @@ func TestShouldCreateInMemorySessionProvider(t *testing.T) {
assert.Equal(t, "memory", providerConfig.providerName) assert.Equal(t, "memory", providerConfig.providerName)
} }
func TestShouldCreateRedisSessionProvider(t *testing.T) { func TestShouldCreateRedisSessionProviderTLS(t *testing.T) {
// The redis configuration is not provided so we create a in-memory provider.
configuration := schema.SessionConfiguration{} configuration := schema.SessionConfiguration{}
configuration.Domain = testDomain configuration.Domain = testDomain
configuration.Name = testName configuration.Name = testName
@ -41,9 +40,14 @@ func TestShouldCreateRedisSessionProvider(t *testing.T) {
Host: "redis.example.com", Host: "redis.example.com",
Port: 6379, Port: 6379,
Password: "pass", Password: "pass",
TLS: &schema.TLSConfig{
ServerName: "redis.fqdn.example.com",
MinimumVersion: "TLS1.3",
},
} }
providerConfig := NewProviderConfig(configuration) providerConfig := NewProviderConfig(configuration, nil)
assert.Nil(t, providerConfig.redisSentinelConfig)
assert.Equal(t, "my_session", providerConfig.config.CookieName) assert.Equal(t, "my_session", providerConfig.config.CookieName)
assert.Equal(t, testDomain, providerConfig.config.Domain) assert.Equal(t, testDomain, providerConfig.config.Domain)
assert.Equal(t, true, providerConfig.config.Secure) assert.Equal(t, true, providerConfig.config.Secure)
@ -57,10 +61,131 @@ func TestShouldCreateRedisSessionProvider(t *testing.T) {
assert.Equal(t, "pass", pConfig.Password) assert.Equal(t, "pass", pConfig.Password)
// DbNumber is the fasthttp/session property for the Redis DB Index // DbNumber is the fasthttp/session property for the Redis DB Index
assert.Equal(t, 0, pConfig.DB) assert.Equal(t, 0, pConfig.DB)
assert.Equal(t, 0, pConfig.PoolSize)
assert.Equal(t, 0, pConfig.MinIdleConns)
require.NotNil(t, pConfig.TLSConfig)
require.Equal(t, uint16(tls.VersionTLS13), pConfig.TLSConfig.MinVersion)
require.Equal(t, "redis.fqdn.example.com", pConfig.TLSConfig.ServerName)
require.False(t, pConfig.TLSConfig.InsecureSkipVerify)
}
func TestShouldCreateRedisSessionProvider(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = testDomain
configuration.Name = testName
configuration.Expiration = testExpiration
configuration.Redis = &schema.RedisSessionConfiguration{
Host: "redis.example.com",
Port: 6379,
Password: "pass",
}
providerConfig := NewProviderConfig(configuration, nil)
assert.Nil(t, providerConfig.redisSentinelConfig)
assert.Equal(t, "my_session", providerConfig.config.CookieName)
assert.Equal(t, testDomain, providerConfig.config.Domain)
assert.Equal(t, true, providerConfig.config.Secure)
assert.Equal(t, time.Duration(40)*time.Second, providerConfig.config.Expiration)
assert.True(t, providerConfig.config.IsSecureFunc(nil))
assert.Equal(t, "redis", providerConfig.providerName)
pConfig := providerConfig.redisConfig
assert.Equal(t, "redis.example.com:6379", pConfig.Addr)
assert.Equal(t, "pass", pConfig.Password)
// DbNumber is the fasthttp/session property for the Redis DB Index
assert.Equal(t, 0, pConfig.DB)
assert.Equal(t, 0, pConfig.PoolSize)
assert.Equal(t, 0, pConfig.MinIdleConns)
assert.Nil(t, pConfig.TLSConfig)
}
func TestShouldCreateRedisSentinelSessionProviderWithoutDuplicateHosts(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = testDomain
configuration.Name = testName
configuration.Expiration = testExpiration
configuration.Redis = &schema.RedisSessionConfiguration{
Host: "REDIS.example.com",
Port: 26379,
Password: "pass",
MaximumActiveConnections: 8,
MinimumIdleConnections: 2,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelName: "mysent",
SentinelPassword: "mypass",
Nodes: []schema.RedisNode{
{
Host: "redis2.example.com",
Port: 26379,
},
{
Host: "redis.example.com",
Port: 26379,
},
},
},
}
providerConfig := NewProviderConfig(configuration, nil)
assert.Len(t, providerConfig.redisSentinelConfig.SentinelAddrs, 2)
assert.Equal(t, providerConfig.redisSentinelConfig.SentinelAddrs[0], "redis.example.com:26379")
assert.Equal(t, providerConfig.redisSentinelConfig.SentinelAddrs[1], "redis2.example.com:26379")
}
func TestShouldCreateRedisSentinelSessionProvider(t *testing.T) {
configuration := schema.SessionConfiguration{}
configuration.Domain = testDomain
configuration.Name = testName
configuration.Expiration = testExpiration
configuration.Redis = &schema.RedisSessionConfiguration{
Host: "redis.example.com",
Port: 26379,
Password: "pass",
MaximumActiveConnections: 8,
MinimumIdleConnections: 2,
HighAvailability: &schema.RedisHighAvailabilityConfiguration{
SentinelName: "mysent",
SentinelPassword: "mypass",
Nodes: []schema.RedisNode{
{
Host: "redis2.example.com",
Port: 26379,
},
},
},
}
providerConfig := NewProviderConfig(configuration, nil)
assert.Nil(t, providerConfig.redisConfig)
assert.Equal(t, "my_session", providerConfig.config.CookieName)
assert.Equal(t, testDomain, providerConfig.config.Domain)
assert.Equal(t, true, providerConfig.config.Secure)
assert.Equal(t, time.Duration(40)*time.Second, providerConfig.config.Expiration)
assert.True(t, providerConfig.config.IsSecureFunc(nil))
assert.Equal(t, "redis-sentinel", providerConfig.providerName)
pConfig := providerConfig.redisSentinelConfig
assert.Equal(t, "redis.example.com:26379", pConfig.SentinelAddrs[0])
assert.Equal(t, "redis2.example.com:26379", pConfig.SentinelAddrs[1])
assert.Equal(t, "pass", pConfig.Password)
assert.Equal(t, "mysent", pConfig.MasterName)
assert.Equal(t, "mypass", pConfig.SentinelPassword)
assert.False(t, pConfig.RouteRandomly)
assert.False(t, pConfig.RouteByLatency)
assert.Equal(t, 8, pConfig.PoolSize)
assert.Equal(t, 2, pConfig.MinIdleConns)
// DbNumber is the fasthttp/session property for the Redis DB Index
assert.Equal(t, 0, pConfig.DB)
assert.Nil(t, pConfig.TLSConfig)
} }
func TestShouldCreateRedisSessionProviderWithUnixSocket(t *testing.T) { func TestShouldCreateRedisSessionProviderWithUnixSocket(t *testing.T) {
// The redis configuration is not provided so we create a in-memory provider.
configuration := schema.SessionConfiguration{} configuration := schema.SessionConfiguration{}
configuration.Domain = testDomain configuration.Domain = testDomain
configuration.Name = testName configuration.Name = testName
@ -70,7 +195,10 @@ func TestShouldCreateRedisSessionProviderWithUnixSocket(t *testing.T) {
Port: 0, Port: 0,
Password: "pass", Password: "pass",
} }
providerConfig := NewProviderConfig(configuration)
providerConfig := NewProviderConfig(configuration, nil)
assert.Nil(t, providerConfig.redisSentinelConfig)
assert.Equal(t, "my_session", providerConfig.config.CookieName) assert.Equal(t, "my_session", providerConfig.config.CookieName)
assert.Equal(t, testDomain, providerConfig.config.Domain) assert.Equal(t, testDomain, providerConfig.config.Domain)
@ -85,6 +213,7 @@ func TestShouldCreateRedisSessionProviderWithUnixSocket(t *testing.T) {
assert.Equal(t, "pass", pConfig.Password) assert.Equal(t, "pass", pConfig.Password)
// DbNumber is the fasthttp/session property for the Redis DB Index // DbNumber is the fasthttp/session property for the Redis DB Index
assert.Equal(t, 0, pConfig.DB) assert.Equal(t, 0, pConfig.DB)
assert.Nil(t, pConfig.TLSConfig)
} }
func TestShouldSetDbNumber(t *testing.T) { func TestShouldSetDbNumber(t *testing.T) {
@ -98,7 +227,11 @@ func TestShouldSetDbNumber(t *testing.T) {
Password: "pass", Password: "pass",
DatabaseIndex: 5, DatabaseIndex: 5,
} }
providerConfig := NewProviderConfig(configuration)
providerConfig := NewProviderConfig(configuration, nil)
assert.Nil(t, providerConfig.redisSentinelConfig)
assert.Equal(t, "redis", providerConfig.providerName) assert.Equal(t, "redis", providerConfig.providerName)
pConfig := providerConfig.redisConfig pConfig := providerConfig.redisConfig
// DbNumber is the fasthttp/session property for the Redis DB Index // DbNumber is the fasthttp/session property for the Redis DB Index
@ -114,7 +247,7 @@ func TestShouldUseEncryptingSerializerWithRedis(t *testing.T) {
Password: "pass", Password: "pass",
DatabaseIndex: 5, DatabaseIndex: 5,
} }
providerConfig := NewProviderConfig(configuration) providerConfig := NewProviderConfig(configuration, nil)
payload := session.Dict{} payload := session.Dict{}
payload.Set("key", "value") payload.Set("key", "value")

View File

@ -18,7 +18,7 @@ func TestShouldInitializerSession(t *testing.T) {
configuration.Name = testName configuration.Name = testName
configuration.Expiration = testExpiration configuration.Expiration = testExpiration
provider := NewProvider(configuration) provider := NewProvider(configuration, nil)
session, err := provider.GetSession(ctx) session, err := provider.GetSession(ctx)
require.NoError(t, err) require.NoError(t, err)
@ -32,7 +32,7 @@ func TestShouldUpdateSession(t *testing.T) {
configuration.Name = testName configuration.Name = testName
configuration.Expiration = testExpiration configuration.Expiration = testExpiration
provider := NewProvider(configuration) provider := NewProvider(configuration, nil)
session, _ := provider.GetSession(ctx) session, _ := provider.GetSession(ctx)
session.Username = testUsername session.Username = testUsername
@ -57,7 +57,7 @@ func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
configuration.Name = testName configuration.Name = testName
configuration.Expiration = testExpiration configuration.Expiration = testExpiration
provider := NewProvider(configuration) provider := NewProvider(configuration, nil)
session, err := provider.GetSession(ctx) session, err := provider.GetSession(ctx)
require.NoError(t, err) require.NoError(t, err)

View File

@ -12,9 +12,10 @@ import (
// ProviderConfig is the configuration used to create the session provider. // ProviderConfig is the configuration used to create the session provider.
type ProviderConfig struct { type ProviderConfig struct {
config session.Config config session.Config
redisConfig *redis.Config redisConfig *redis.Config
providerName string redisSentinelConfig *redis.FailoverConfig
providerName string
} }
// U2FRegistration is a serializable version of a U2F registration. // U2FRegistration is a serializable version of a U2F registration.

View File

@ -85,9 +85,19 @@ session:
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
domain: example.com domain: example.com
redis: redis:
host: redis username: authelia
port: 6379 password: redis-user-password
password: authelia high_availability:
sentinel_name: authelia
sentinel_password: sentinel-server-password
nodes:
- host: redis-sentinel-0
port: 26379
- host: redis-sentinel-1
port: 26379
- host: redis-sentinel-2
port: 26379
remember_me_duration: 1y remember_me_duration: 1y
regulation: regulation:

View File

@ -20,6 +20,11 @@ session:
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
remember_me_duration: 1y remember_me_duration: 1y
redis:
host: redis
port: 6379
username: authelia
password: redis-user-password
storage: storage:
local: local:

View File

@ -55,6 +55,16 @@ func (de *DockerEnvironment) Restart(service string) error {
return de.createCommandWithStdout(fmt.Sprintf("restart %s", service)).Run() return de.createCommandWithStdout(fmt.Sprintf("restart %s", service)).Run()
} }
// Stop a docker service.
func (de *DockerEnvironment) Stop(service string) error {
return de.createCommandWithStdout(fmt.Sprintf("stop %s", service)).Run()
}
// Start a docker service.
func (de *DockerEnvironment) Start(service string) error {
return de.createCommandWithStdout(fmt.Sprintf("start %s", service)).Run()
}
// Down destroy a docker environment. // Down destroy a docker environment.
func (de *DockerEnvironment) Down() error { func (de *DockerEnvironment) Down() error {
return de.createCommandWithStdout("down -v").Run() return de.createCommandWithStdout("down -v").Run()

View File

@ -0,0 +1,96 @@
version: '3'
services:
redis-node-0:
image: redis:6.2-alpine
command: /entrypoint.sh master
expose:
- "6379"
volumes:
- ./example/compose/redis/templates:/templates
- ./example/compose/redis/users.acl:/data/users.acl
- ./example/compose/redis/entrypoint.sh:/entrypoint.sh
networks:
authelianet:
aliases:
- redis-node-0.example.com
ipv4_address: 192.168.240.110
redis-node-1:
image: redis:6.2-alpine
command: /entrypoint.sh slave
depends_on:
- redis-node-0
expose:
- "6379"
volumes:
- ./example/compose/redis/templates:/templates
- ./example/compose/redis/users.acl:/data/users.acl
- ./example/compose/redis/entrypoint.sh:/entrypoint.sh
networks:
authelianet:
aliases:
- redis-node-1.example.com
ipv4_address: 192.168.240.111
redis-node-2:
image: redis:6.2-alpine
command: /entrypoint.sh slave
depends_on:
- redis-node-0
expose:
- "6379"
volumes:
- ./example/compose/redis/templates:/templates
- ./example/compose/redis/users.acl:/data/users.acl
- ./example/compose/redis/entrypoint.sh:/entrypoint.sh
networks:
authelianet:
aliases:
- redis-node-2.example.com
ipv4_address: 192.168.240.112
redis-sentinel-0:
image: redis:6.2-alpine
command: /entrypoint.sh sentinel
depends_on:
- redis-node-1
- redis-node-2
expose:
- "26379"
volumes:
- ./example/compose/redis/templates:/templates
- ./example/compose/redis/entrypoint.sh:/entrypoint.sh
networks:
authelianet:
aliases:
- redis-sentinel-0.example.com
ipv4_address: 192.168.240.120
redis-sentinel-1:
image: redis:6.2-alpine
command: /entrypoint.sh sentinel
depends_on:
- redis-node-1
- redis-node-2
expose:
- "26379"
volumes:
- ./example/compose/redis/templates:/templates
- ./example/compose/redis/entrypoint.sh:/entrypoint.sh
networks:
authelianet:
aliases:
- redis-sentinel-1.example.com
ipv4_address: 192.168.240.121
redis-sentinel-2:
image: redis:6.2-alpine
command: /entrypoint.sh sentinel
depends_on:
- redis-node-1
- redis-node-2
expose:
- "26379"
volumes:
- ./example/compose/redis/templates:/templates
- ./example/compose/redis/entrypoint.sh:/entrypoint.sh
networks:
authelianet:
aliases:
- redis-sentinel-2.example.com
ipv4_address: 192.168.240.122

View File

@ -1,9 +1,13 @@
version: '3' version: '3'
services: services:
redis: redis:
image: redis:4.0-alpine image: redis:6.2-alpine
command: redis-server --requirepass authelia command: /entrypoint.sh master
ports: expose:
- "6379:6379" - "6379"
volumes:
- ./example/compose/redis/templates:/templates
- ./example/compose/redis/users.acl:/data/users.acl
- ./example/compose/redis/entrypoint.sh:/entrypoint.sh
networks: networks:
- authelianet - authelianet

View File

@ -0,0 +1,15 @@
#!/bin/sh
MODE=$1
cp /templates/${MODE}.conf /data/redis.conf
chown -R redis:redis /data
if [ "${MODE}" == "master" ] || [ "${MODE}" == "slave" ]; then
redis-server /data/redis.conf
elif [ "${MODE}" == "sentinel" ]; then
redis-server /data/redis.conf --sentinel
else
echo "invalid argument: entrypoint.sh [master|slave|sentinel]"
exit 1
fi

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,346 @@
###########################################################################
# Source: https://raw.githubusercontent.com/redis/redis/6.2/sentinel.conf #
###########################################################################
# Example sentinel.conf
# *** IMPORTANT ***
#
# By default Sentinel will not be reachable from interfaces different than
# localhost, either use the 'bind' directive to bind to a list of network
# interfaces, or disable protected mode with "protected-mode no" by
# adding it to this configuration file.
#
# Before doing that MAKE SURE the instance is protected from the outside
# world via firewalling or other means.
#
# For example you may use one of the following:
#
# bind 127.0.0.1 192.168.1.1
#
protected-mode no
bind 0.0.0.0
# port <sentinel-port>
# The port that this sentinel instance will run on
port 26379
# By default Redis Sentinel does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis-sentinel.pid when
# daemonized.
daemonize no
# When running daemonized, Redis Sentinel writes a pid file in
# /var/run/redis-sentinel.pid by default. You can specify a custom pid file
# location here.
pidfile /var/run/redis-sentinel.pid
# Specify the log file name. Also the empty string can be used to force
# Sentinel to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile ""
# sentinel announce-ip <ip>
# sentinel announce-port <port>
#
# The above two configuration directives are useful in environments where,
# because of NAT, Sentinel is reachable from outside via a non-local address.
#
# When announce-ip is provided, the Sentinel will claim the specified IP address
# in HELLO messages used to gossip its presence, instead of auto-detecting the
# local address as it usually does.
#
# Similarly when announce-port is provided and is valid and non-zero, Sentinel
# will announce the specified TCP port.
#
# The two options don't need to be used together, if only announce-ip is
# provided, the Sentinel will announce the specified IP and the server port
# as specified by the "port" option. If only announce-port is provided, the
# Sentinel will announce the auto-detected local IP and the specified port.
#
# Example:
#
# sentinel announce-ip 1.2.3.4
# dir <working-directory>
# Every long running process should have a well-defined working directory.
# For Redis Sentinel to chdir to /tmp at startup is the simplest thing
# for the process to don't interfere with administrative tasks such as
# unmounting filesystems.
dir /tmp
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
#
# Tells Sentinel to monitor this master, and to consider it in O_DOWN
# (Objectively Down) state only if at least <quorum> sentinels agree.
#
# Note that whatever is the ODOWN quorum, a Sentinel will require to
# be elected by the majority of the known Sentinels in order to
# start a failover, so no failover can be performed in minority.
#
# Replicas are auto-discovered, so you don't need to specify replicas in
# any way. Sentinel itself will rewrite this configuration file adding
# the replicas using additional configuration options.
# Also note that the configuration file is rewritten when a
# replica is promoted to master.
#
# Note: master name should not include special characters or spaces.
# The valid charset is A-z 0-9 and the three characters ".-_".
sentinel monitor authelia 192.168.240.110 6379 2
# sentinel auth-pass <master-name> <password>
#
# Set the password to use to authenticate with the master and replicas.
# Useful if there is a password set in the Redis instances to monitor.
#
# Note that the master password is also used for replicas, so it is not
# possible to set a different password in masters and replicas instances
# if you want to be able to monitor these instances with Sentinel.
#
# However you can have Redis instances without the authentication enabled
# mixed with Redis instances requiring the authentication (as long as the
# password set is the same for all the instances requiring the password) as
# the AUTH command will have no effect in Redis instances with authentication
# switched off.
#
# Example:
#
sentinel auth-pass authelia sentinel-client-password
sentinel auth-user authelia sentinel
#
# This is useful in order to authenticate to instances having ACL capabilities,
# that is, running Redis 6.0 or greater. When just auth-pass is provided the
# Sentinel instance will authenticate to Redis using the old "AUTH <pass>"
# method. When also an username is provided, it will use "AUTH <user> <pass>".
# In the Redis servers side, the ACL to provide just minimal access to
# Sentinel instances, should be configured along the following lines:
#
# user sentinel-user >somepassword +client +subscribe +publish \
# +ping +info +multi +slaveof +config +client +exec on
# sentinel down-after-milliseconds <master-name> <milliseconds>
#
# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
sentinel down-after-milliseconds authelia 1000
# IMPORTANT NOTE: starting with Redis 6.2 ACL capability is supported for
# Sentinel mode, please refer to the Redis website https://redis.io/topics/acl
# for more details.
# Sentinel's ACL users are defined in the following format:
#
# user <username> ... acl rules ...
#
# For example:
#
# user worker +@admin +@connection ~* on >ffa9203c493aa99
#
# For more information about ACL configuration please refer to the Redis
# website at https://redis.io/topics/acl and redis server configuration
# template redis.conf.
# ACL LOG
#
# The ACL Log tracks failed commands and authentication events associated
# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked
# by ACLs. The ACL Log is stored in memory. You can reclaim memory with
# ACL LOG RESET. Define the maximum entry length of the ACL Log below.
acllog-max-len 128
# Using an external ACL file
#
# Instead of configuring users here in this file, it is possible to use
# a stand-alone file just listing users. The two methods cannot be mixed:
# if you configure users here and at the same time you activate the external
# ACL file, the server will refuse to start.
#
# The format of the external ACL user file is exactly the same as the
# format that is used inside redis.conf to describe users.
#
# aclfile /data/sentinel-users.acl
requirepass sentinel-server-password
#
# You can configure Sentinel itself to require a password, however when doing
# so Sentinel will try to authenticate with the same password to all the
# other Sentinels. So you need to configure all your Sentinels in a given
# group with the same "requirepass" password. Check the following documentation
# for more info: https://redis.io/topics/sentinel
#
# IMPORTANT NOTE: starting with Redis 6.2 "requirepass" is a compatibility
# layer on top of the ACL system. The option effect will be just setting
# the password for the default user. Clients will still authenticate using
# AUTH <password> as usually, or more explicitly with AUTH default <password>
# if they follow the new protocol: both will work.
#
# New config files are advised to use separate authentication control for
# incoming connections (via ACL), and for outgoing connections (via
# sentinel-user and sentinel-pass)
#
# The requirepass is not compatable with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.
# sentinel sentinel-user sentinel
#
# You can configure Sentinel to authenticate with other Sentinels with specific
# user name.
sentinel sentinel-pass sentinel-server-password
#
# The password for Sentinel to authenticate with other Sentinels. If sentinel-user
# is not configured, Sentinel will use 'default' user with sentinel-pass to authenticate.
# sentinel parallel-syncs <master-name> <numreplicas>
#
# How many replicas we can reconfigure to point to the new replica simultaneously
# during the failover. Use a low number if you use the replicas to serve query
# to avoid that all the replicas will be unreachable at about the same
# time while performing the synchronization with the master.
sentinel parallel-syncs authelia 1
# sentinel failover-timeout <master-name> <milliseconds>
#
# Specifies the failover timeout in milliseconds. It is used in many ways:
#
# - The time needed to re-start a failover after a previous failover was
# already tried against the same master by a given Sentinel, is two
# times the failover timeout.
#
# - The time needed for a replica replicating to a wrong master according
# to a Sentinel current configuration, to be forced to replicate
# with the right master, is exactly the failover timeout (counting since
# the moment a Sentinel detected the misconfiguration).
#
# - The time needed to cancel a failover that is already in progress but
# did not produced any configuration change (SLAVEOF NO ONE yet not
# acknowledged by the promoted replica).
#
# - The maximum time a failover in progress waits for all the replicas to be
# reconfigured as replicas of the new master. However even after this time
# the replicas will be reconfigured by the Sentinels anyway, but not with
# the exact parallel-syncs progression as specified.
#
# Default is 3 minutes.
sentinel failover-timeout authelia 2000
# SCRIPTS EXECUTION
#
# sentinel notification-script and sentinel reconfig-script are used in order
# to configure scripts that are called to notify the system administrator
# or to reconfigure clients after a failover. The scripts are executed
# with the following rules for error handling:
#
# If script exits with "1" the execution is retried later (up to a maximum
# number of times currently set to 10).
#
# If script exits with "2" (or an higher value) the script execution is
# not retried.
#
# If script terminates because it receives a signal the behavior is the same
# as exit code 1.
#
# A script has a maximum running time of 60 seconds. After this limit is
# reached the script is terminated with a SIGKILL and the execution retried.
# NOTIFICATION SCRIPT
#
# sentinel notification-script <master-name> <script-path>
#
# Call the specified notification script for any sentinel event that is
# generated in the WARNING level (for instance -sdown, -odown, and so forth).
# This script should notify the system administrator via email, SMS, or any
# other messaging system, that there is something wrong with the monitored
# Redis systems.
#
# The script is called with just two arguments: the first is the event type
# and the second the event description.
#
# The script must exist and be executable in order for sentinel to start if
# this option is provided.
#
# Example:
#
# sentinel notification-script mymaster /var/redis/notify.sh
# CLIENTS RECONFIGURATION SCRIPT
#
# sentinel client-reconfig-script <master-name> <script-path>
#
# When the master changed because of a failover a script can be called in
# order to perform application-specific tasks to notify the clients that the
# configuration has changed and the master is at a different address.
#
# The following arguments are passed to the script:
#
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#
# <state> is currently always "failover"
# <role> is either "leader" or "observer"
#
# The arguments from-ip, from-port, to-ip, to-port are used to communicate
# the old address of the master and the new address of the elected replica
# (now a master).
#
# This script should be resistant to multiple invocations.
#
# Example:
#
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
# SECURITY
#
# By default SENTINEL SET will not be able to change the notification-script
# and client-reconfig-script at runtime. This avoids a trivial security issue
# where clients can set the script to anything and trigger a failover in order
# to get the program executed.
sentinel deny-scripts-reconfig yes
# REDIS COMMANDS RENAMING
#
# Sometimes the Redis server has certain commands, that are needed for Sentinel
# to work correctly, renamed to unguessable strings. This is often the case
# of CONFIG and SLAVEOF in the context of providers that provide Redis as
# a service, and don't want the customers to reconfigure the instances outside
# of the administration console.
#
# In such case it is possible to tell Sentinel to use different command names
# instead of the normal ones. For example if the master "mymaster", and the
# associated replicas, have "CONFIG" all renamed to "GUESSME", I could use:
#
# SENTINEL rename-command mymaster CONFIG GUESSME
#
# After such configuration is set, every time Sentinel would use CONFIG it will
# use GUESSME instead. Note that there is no actual need to respect the command
# case, so writing "config guessme" is the same in the example above.
#
# SENTINEL SET can also be used in order to perform this configuration at runtime.
#
# In order to set a command back to its original name (undo the renaming), it
# is possible to just rename a command to itself:
#
# SENTINEL rename-command mymaster CONFIG CONFIG
# HOSTNAMES SUPPORT
#
# Normally Sentinel uses only IP addresses and requires SENTINEL MONITOR
# to specify an IP address. Also, it requires the Redis replica-announce-ip
# keyword to specify only IP addresses.
#
# You may enable hostnames support by enabling resolve-hostnames. Note
# that you must make sure your DNS is configured properly and that DNS
# resolution does not introduce very long delays.
#
SENTINEL resolve-hostnames no
# When resolve-hostnames is enabled, Sentinel still uses IP addresses
# when exposing instances to users, configuration files, etc. If you want
# to retain the hostnames when announced, enable announce-hostnames below.
#
SENTINEL announce-hostnames no

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
user authelia +@admin +@all ~* &* on >redis-user-password
user repl +@admin +@all ~* &* on >repl-password
user sentinel +@admin +@all ~* &* on >sentinel-client-password

View File

@ -13,7 +13,7 @@ var haDockerEnvironment = NewDockerEnvironment([]string{
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml", "internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
"internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml", "internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
"internal/suites/example/compose/mariadb/docker-compose.yml", "internal/suites/example/compose/mariadb/docker-compose.yml",
"internal/suites/example/compose/redis/docker-compose.yml", "internal/suites/example/compose/redis-sentinel/docker-compose.yml",
"internal/suites/example/compose/nginx/backend/docker-compose.yml", "internal/suites/example/compose/nginx/backend/docker-compose.yml",
"internal/suites/example/compose/nginx/portal/docker-compose.yml", "internal/suites/example/compose/nginx/portal/docker-compose.yml",
"internal/suites/example/compose/smtp/docker-compose.yml", "internal/suites/example/compose/smtp/docker-compose.yml",
@ -57,7 +57,7 @@ func init() {
SetUp: setup, SetUp: setup,
SetUpTimeout: 5 * time.Minute, SetUpTimeout: 5 * time.Minute,
OnSetupTimeout: displayAutheliaLogs, OnSetupTimeout: displayAutheliaLogs,
TestTimeout: 5 * time.Minute, TestTimeout: 6 * time.Minute,
TearDown: teardown, TearDown: teardown,
TearDownTimeout: 2 * time.Minute, TearDownTimeout: 2 * time.Minute,
OnError: displayAutheliaLogs, OnError: displayAutheliaLogs,

View File

@ -47,6 +47,92 @@ func (s *HighAvailabilityWebDriverSuite) SetupTest() {
s.verifyIsHome(ctx, s.T()) s.verifyIsHome(ctx, s.T())
} }
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserSessionActive() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
defer cancel()
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
err := haDockerEnvironment.Restart("redis-node-0")
s.Require().NoError(err)
time.Sleep(5 * time.Second)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
s.verifyIsSecondFactorPage(ctx, s.T())
}
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserSessionActiveWithPrimaryRedisNodeFailure() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
defer cancel()
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
s.verifyIsSecondFactorPage(ctx, s.T())
err := haDockerEnvironment.Stop("redis-node-0")
s.Require().NoError(err)
defer func() {
err = haDockerEnvironment.Start("redis-node-0")
s.Require().NoError(err)
}()
// Allow fail over to occur.
time.Sleep(3 * time.Second)
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
// Verify the user is still authenticated
s.doVisit(s.T(), GetLoginBaseURL())
s.verifyIsSecondFactorPage(ctx, s.T())
// Then logout and login again to check we can see the secret.
s.doLogout(ctx, s.T())
s.verifyIsFirstFactorPage(ctx, s.T())
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, fmt.Sprintf("%s/secret.html", SecureBaseURL))
s.verifySecretAuthorized(ctx, s.T())
}
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserSessionActiveWithPrimaryRedisSentinelFailureAndSecondaryRedisNodeFailure() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
defer cancel()
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
s.verifyIsSecondFactorPage(ctx, s.T())
err := haDockerEnvironment.Stop("redis-sentinel-0")
s.Require().NoError(err)
defer func() {
err = haDockerEnvironment.Start("redis-sentinel-0")
s.Require().NoError(err)
}()
err = haDockerEnvironment.Stop("redis-node-2")
s.Require().NoError(err)
defer func() {
err = haDockerEnvironment.Start("redis-node-2")
s.Require().NoError(err)
}()
// Allow fail over to occur.
time.Sleep(3 * time.Second)
s.doVisit(s.T(), HomeBaseURL)
s.verifyIsHome(ctx, s.T())
// Verify the user is still authenticated
s.doVisit(s.T(), GetLoginBaseURL())
s.verifyIsSecondFactorPage(ctx, s.T())
}
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() { func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
defer cancel() defer cancel()

View File

@ -24,7 +24,10 @@ func (s *NetworkACLSuite) TestShouldAccessSecretUpon2FA() {
wds, err := StartWebDriver() wds, err := StartWebDriver()
s.Require().NoError(err) s.Require().NoError(err)
defer wds.Stop() //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. defer func() {
err = wds.Stop()
s.Require().NoError(err)
}()
targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL) targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL)
wds.doVisit(s.T(), targetURL) wds.doVisit(s.T(), targetURL)
@ -42,7 +45,10 @@ func (s *NetworkACLSuite) TestShouldAccessSecretUpon1FA() {
wds, err := StartWebDriverWithProxy("http://proxy-client1.example.com:3128", GetWebDriverPort()) wds, err := StartWebDriverWithProxy("http://proxy-client1.example.com:3128", GetWebDriverPort())
s.Require().NoError(err) s.Require().NoError(err)
defer wds.Stop() //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. defer func() {
err = wds.Stop()
s.Require().NoError(err)
}()
targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL) targetURL := fmt.Sprintf("%s/secret.html", SecureBaseURL)
wds.doVisit(s.T(), targetURL) wds.doVisit(s.T(), targetURL)
@ -61,7 +67,10 @@ func (s *NetworkACLSuite) TestShouldAccessSecretUpon0FA() {
wds, err := StartWebDriverWithProxy("http://proxy-client2.example.com:3128", GetWebDriverPort()) wds, err := StartWebDriverWithProxy("http://proxy-client2.example.com:3128", GetWebDriverPort())
s.Require().NoError(err) s.Require().NoError(err)
defer wds.Stop() //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. defer func() {
err = wds.Stop()
s.Require().NoError(err)
}()
wds.doVisit(s.T(), fmt.Sprintf("%s/secret.html", SecureBaseURL)) wds.doVisit(s.T(), fmt.Sprintf("%s/secret.html", SecureBaseURL))
wds.verifySecretAuthorized(ctx, s.T()) wds.verifySecretAuthorized(ctx, s.T())

View File

@ -7,35 +7,36 @@ import (
var traefik2SuiteName = "Traefik2" var traefik2SuiteName = "Traefik2"
func init() { var traefik2DockerEnvironment = NewDockerEnvironment([]string{
dockerEnvironment := NewDockerEnvironment([]string{ "internal/suites/docker-compose.yml",
"internal/suites/docker-compose.yml", "internal/suites/Traefik2/docker-compose.yml",
"internal/suites/Traefik2/docker-compose.yml", "internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml", "internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
"internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml", "internal/suites/example/compose/redis/docker-compose.yml",
"internal/suites/example/compose/nginx/backend/docker-compose.yml", "internal/suites/example/compose/nginx/backend/docker-compose.yml",
"internal/suites/example/compose/traefik2/docker-compose.yml", "internal/suites/example/compose/traefik2/docker-compose.yml",
"internal/suites/example/compose/smtp/docker-compose.yml", "internal/suites/example/compose/smtp/docker-compose.yml",
"internal/suites/example/compose/httpbin/docker-compose.yml", "internal/suites/example/compose/httpbin/docker-compose.yml",
}) })
func init() {
setup := func(suitePath string) error { setup := func(suitePath string) error {
if err := dockerEnvironment.Up(); err != nil { if err := traefik2DockerEnvironment.Up(); err != nil {
return err return err
} }
return waitUntilAutheliaIsReady(dockerEnvironment, traefik2SuiteName) return waitUntilAutheliaIsReady(traefik2DockerEnvironment, traefik2SuiteName)
} }
displayAutheliaLogs := func() error { displayAutheliaLogs := func() error {
backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil) backendLogs, err := traefik2DockerEnvironment.Logs("authelia-backend", nil)
if err != nil { if err != nil {
return err return err
} }
fmt.Println(backendLogs) fmt.Println(backendLogs)
frontendLogs, err := dockerEnvironment.Logs("authelia-frontend", nil) frontendLogs, err := traefik2DockerEnvironment.Logs("authelia-frontend", nil)
if err != nil { if err != nil {
return err return err
} }
@ -46,7 +47,7 @@ func init() {
} }
teardown := func(suitePath string) error { teardown := func(suitePath string) error {
err := dockerEnvironment.Down() err := traefik2DockerEnvironment.Down()
return err return err
} }

View File

@ -1,7 +1,10 @@
package suites package suites
import ( import (
"context"
"fmt"
"testing" "testing"
"time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -26,6 +29,34 @@ func (s *Traefik2Suite) TestCustomHeaders() {
suite.Run(s.T(), NewCustomHeadersScenario()) suite.Run(s.T(), NewCustomHeadersScenario())
} }
func (s *Traefik2Suite) TestShouldKeepSessionAfterRedisRestart() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
wds, err := StartWebDriver()
s.Require().NoError(err)
defer func() {
err = wds.Stop()
s.Require().NoError(err)
}()
secret := wds.doRegisterThenLogout(ctx, s.T(), "john", "password")
wds.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
wds.doVisit(s.T(), fmt.Sprintf("%s/secret.html", SecureBaseURL))
wds.verifySecretAuthorized(ctx, s.T())
err = traefik2DockerEnvironment.Restart("redis")
s.Require().NoError(err)
time.Sleep(5 * time.Second)
wds.doVisit(s.T(), fmt.Sprintf("%s/secret.html", SecureBaseURL))
wds.verifySecretAuthorized(ctx, s.T())
}
func TestTraefik2Suite(t *testing.T) { func TestTraefik2Suite(t *testing.T) {
suite.Run(t, NewTraefik2Suite()) suite.Run(t, NewTraefik2Suite())
} }