feat(authentication): file password algorithms (#3848)

This adds significant enhancements to the file auth provider including multiple additional algorithms.
pull/4189/head
James Elliott 2022-10-17 21:51:59 +11:00 committed by GitHub
parent 8eadf72dc7
commit 3a70f6739b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 3242 additions and 1571 deletions

View File

@ -7,5 +7,5 @@
package cmd
const (
versionSwaggerUI = "4.14.2"
versionSwaggerUI = "4.14.3"
)

View File

@ -393,12 +393,32 @@ authentication_backend:
# file:
# path: /config/users_database.yml
# password:
# algorithm: argon2id
# iterations: 1
# key_length: 32
# salt_length: 16
# memory: 1024
# parallelism: 8
# algorithm: argon2
# argon2:
# variant: argon2id
# iterations: 3
# memory: 65536
# parallelism: 4
# key_length: 32
# salt_length: 16
# scrypt:
# iterations: 16
# block_size: 8
# parallelism: 1
# key_length: 32
# salt_length: 16
# pbkdf2:
# variant: sha512
# iterations: 310000
# salt_length: 16
# sha2crypt:
# variant: sha512
# iterations: 50000
# salt_length: 16
# bcrypt:
# variant: standard
# cost: 12
##
## Password Policy Configuration.

View File

@ -21,12 +21,31 @@ authentication_backend:
file:
path: /config/users.yml
password:
algorithm: argon2id
iterations: 3
key_length: 32
salt_length: 16
parallelism: 4
memory: 64
algorithm: argon2
argon2:
variant: argon2id
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
scrypt:
iterations: 16
block_size: 8
parallelism: 1
key_length: 32
salt_length: 16
pbkdf2:
variant: sha512
iterations: 310000
salt_length: 16
sha2crypt:
variant: sha512
iterations: 50000
salt_length: 16
bcrypt:
variant: standard
cost: 12
```
## Options
@ -39,70 +58,7 @@ The path to the file with the user details list. Supported file types are:
* [YAML File](../../reference/guides/passwords.md#yaml-format)
### password
#### algorithm
{{< confkey type="string" default="argon2id" required="no" >}}
Controls the hashing algorithm used for hashing new passwords. Value must be one of:
* `argon2id` for the [Argon2] `id` variant
* `sha512` for the [SHA Crypt] `SHA512` variant
#### iterations
{{< confkey type="integer" required="no" >}}
Controls the number of hashing iterations done by the other hashing settings ([Argon2] parameter `t`, [SHA Crypt]
parameter `rounds`). This affects the effective cost of hashing.
| Algorithm | Minimum | Default | Recommended |
|:---------:|:-------:|:-------:|:------------------------------------------------------------------------------------------:|
| argon2id | 1 | 3 | [See Recommendations](../../reference/guides/passwords.md#recommended-parameters-argon2id) |
| sha512 | 1000 | 50000 | [See Recommendations](../../reference/guides/passwords.md#recommended-parameters-sha512) |
#### key_length
{{< confkey type="integer" default="32" required="no" >}}
*__Important:__ This setting is specific to the `argon2id` algorithm and unused with the `sha512` algorithm.*
Sets the key length of the [Argon2] hash output. The minimum value is `16` with the recommended value of `32` being set
as the default.
#### salt_length
{{< confkey type="integer" default="16" required="no" >}}
Controls the length of the random salt added to each password before hashing. There is not a compelling reason to have
this set to anything other than `16`, however the minimum is `8` with the recommended value of `16` being set as the
default.
#### parallelism
{{< confkey type="integer" default="4" required="no" >}}
*__Important:__ This setting is specific to the `argon2id` algorithm and unused with the `sha512` algorithm.*
Sets the number of threads used by [Argon2] when hashing passwords ([Argon2] parameter `p`). The minimum value is `1`
with the recommended value of `4` being set as the default. This affects the effective cost of hashing.
#### memory
{{< confkey type="integer" default="64" required="no" >}}
*__Important:__ This setting is specific to the `argon2id` algorithm and unused with the `sha512` algorithm.*
Sets the amount of memory in megabytes allocated to a single password hashing calculation ([Argon2] parameter `m`). This
affects the effective cost of hashing.
This memory is released by go after the hashing process completes, however the operating system may not reclaim the
memory until a later time such as when the system is experiencing memory pressure which may cause the appearance of more
memory being in use than Authelia is actually actively using. Authelia will typically reuse this memory if it has not be
reclaimed as long as another hashing calculation is not still utilizing it.
## Reference
## Password Options
A [reference guide](../../reference/guides/passwords.md) exists specifically for choosing password hashing values. This
section contains far more information than is practical to include in this configuration document. See the
@ -110,5 +66,164 @@ section contains far more information than is practical to include in this confi
This guide contains examples such as the [User / Password File](../../reference/guides/passwords.md#user--password-file).
### algorithm
{{< confkey type="string" default="argon2" required="no" >}}
Controls the hashing algorithm used for hashing new passwords. Value must be one of:
* `argon2` for the [Argon2](#argon2) algorithm
* `scrypt` for the [Scrypt](#scrypt) algorithm
* `pbkdf2` for the [PBKDF2](#pbkdf2) algorithm
* `sha2crypt` for the [SHA2Crypt](#sha2crypt) algorithm
* `bcrypt` for the [Bcrypt](#bcrypt) algorithm
### argon2
The [Argon2] algorithm implementation. This is one of the only algorithms that was designed purely with password hashing
in mind and is subsequently one of the best algorithms to date for security.
#### variant
{{< confkey type="string" default="argon2id" required="no" >}}
Controls the variant when hashing passwords using [Argon2]. Recommended `argon2id`.
Permitted values `argon2id`, `argon2i`, `argon2d`.
#### iterations
{{< confkey type="integer" default="3" required="no" >}}
Controls the number of iterations when hashing passwords using [Argon2].
#### memory
{{< confkey type="integer" default="65536" required="no" >}}
Controls the amount of memory in kibibytes when hashing passwords using [Argon2].
#### parallelism
{{< confkey type="integer" default="4" required="no" >}}
Controls the parallelism factor when hashing passwords using [Argon2].
#### key_length
{{< confkey type="integer" default="32" required="no" >}}
Controls the output key length when hashing passwords using [Argon2].
#### salt_length
{{< confkey type="integer" default="16" required="no" >}}
Controls the output salt length when hashing passwords using [Argon2].
### scrypt
The [Scrypt] algorithm implementation.
#### iterations
{{< confkey type="integer" default="16" required="no" >}}
Controls the number of iterations when hashing passwords using [Scrypt].
#### block_size
{{< confkey type="integer" default="8" required="no" >}}
Controls the block size when hashing passwords using [Scrypt].
#### parallelism
{{< confkey type="integer" default="1" required="no" >}}
Controls the parallelism factor when hashing passwords using [Scrypt].
#### key_length
{{< confkey type="integer" default="32" required="no" >}}
Controls the output key length when hashing passwords using [Scrypt].
#### salt_length
{{< confkey type="integer" default="16" required="no" >}}
Controls the output salt length when hashing passwords using [Scrypt].
### pbkdf2
The [PBKDF2] algorithm implementation.
#### variant
{{< confkey type="string" default="sha512" required="no" >}}
Controls the variant when hashing passwords using [PBKDF2]. Recommended `sha512`.
Permitted values `sha1`, `sha224`, `sha256`, `sha384`, `sha512`.
#### iterations
{{< confkey type="integer" default="310000" required="no" >}}
Controls the number of iterations when hashing passwords using [PBKDF2].
#### salt_length
{{< confkey type="integer" default="16" required="no" >}}
Controls the output salt length when hashing passwords using [PBKDF2].
### sha2crypt
The [SHA2 Crypt] algorithm implementation.
#### variant
{{< confkey type="string" default="sha512" required="no" >}}
Controls the variant when hashing passwords using [SHA2 Crypt]. Recommended `sha512`.
Permitted values `sha256`, `sha512`.
#### iterations
{{< confkey type="integer" default="50000" required="no" >}}
Controls the number of iterations when hashing passwords using [SHA2 Crypt].
#### salt_length
{{< confkey type="integer" default="16" required="no" >}}
Controls the output salt length when hashing passwords using [SHA2 Crypt].
### bcrypt
The [Bcrypt] algorithm implementation.
#### variant
{{< confkey type="string" default="standard" required="no" >}}
Controls the variant when hashing passwords using [Bcrypt]. Recommended `standard`.
Permitted values `standard`, `sha256`.
*__Important Note:__ The `sha256` variant is a special variant designed by
[Passlib](https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt_sha256.html). This variant passes the
password through a SHA256 HMAC before passing it to the [Bcrypt] algorithm, effectively bypassing the 72 byte password
truncation that [Bcrypt] does. It is not supported by many other systems.*
#### cost
{{< confkey type="integer" default="12" required="no" >}}
Controls the hashing cost when hashing passwords using [Bcrypt].
[Argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
[SHA Crypt]: https://www.akkadia.org/drepper/SHA-crypt.txt
[Scrypt]: https://en.wikipedia.org/wiki/Scrypt
[PBKDF2]: https://www.ietf.org/rfc/rfc2898.html
[SHA2 Crypt]: https://www.akkadia.org/drepper/SHA-crypt.txt
[Bcrypt]: https://en.wikipedia.org/wiki/Bcrypt

View File

@ -2,7 +2,7 @@
title: "NGINX Proxy Manager"
description: "An integration guide for Authelia and the NGINX Proxy Manager reverse proxy"
lead: "A guide on integrating Authelia with NGINX Proxy Manager."
date: 2022-06-15T17:51:47+10:00
date: 2022-10-08T12:43:26+11:00
draft: false
images: []
menu:

View File

@ -38,5 +38,7 @@ authelia crypto --help
* [authelia](authelia.md) - authelia untagged-unknown-dirty (master, unknown)
* [authelia crypto certificate](authelia_crypto_certificate.md) - Perform certificate cryptographic operations
* [authelia crypto hash](authelia_crypto_hash.md) - Perform cryptographic hash operations
* [authelia crypto pair](authelia_crypto_pair.md) - Perform key pair cryptographic operations
* [authelia crypto rand](authelia_crypto_rand.md) - Generate a cryptographically secure random string

View File

@ -0,0 +1,42 @@
---
title: "authelia crypto hash"
description: "Reference for the authelia crypto hash command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash
Perform cryptographic hash operations
### Synopsis
Perform cryptographic hash operations.
This subcommand allows preforming hashing cryptographic tasks.
### Examples
```
authelia crypto hash --help
```
### Options
```
-h, --help help for hash
```
### SEE ALSO
* [authelia crypto](authelia_crypto.md) - Perform cryptographic operations
* [authelia crypto hash generate](authelia_crypto_hash_generate.md) - Generate cryptographic hash digests
* [authelia crypto hash validate](authelia_crypto_hash_validate.md) - Perform cryptographic hash validations

View File

@ -0,0 +1,54 @@
---
title: "authelia crypto hash generate"
description: "Reference for the authelia crypto hash generate command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash generate
Generate cryptographic hash digests
### Synopsis
Generate cryptographic hash digests.
This subcommand allows generating cryptographic hash digests.
See the help for the subcommands if you want to override the configuration or defaults.
```
authelia crypto hash generate [flags]
```
### Examples
```
authelia crypto hash generate --help
```
### Options
```
-c, --config strings configuration files to load (default [configuration.yml])
-h, --help help for generate
--no-confirm skip the password confirmation prompt
--password string manually supply the password rather than using the terminal prompt
```
### SEE ALSO
* [authelia crypto hash](authelia_crypto_hash.md) - Perform cryptographic hash operations
* [authelia crypto hash generate argon2](authelia_crypto_hash_generate_argon2.md) - Generate cryptographic Argon2 hash digests
* [authelia crypto hash generate bcrypt](authelia_crypto_hash_generate_bcrypt.md) - Generate cryptographic bcrypt hash digests
* [authelia crypto hash generate pbkdf2](authelia_crypto_hash_generate_pbkdf2.md) - Generate cryptographic PBKDF2 hash digests
* [authelia crypto hash generate scrypt](authelia_crypto_hash_generate_scrypt.md) - Generate cryptographic scrypt hash digests
* [authelia crypto hash generate sha2crypt](authelia_crypto_hash_generate_sha2crypt.md) - Generate cryptographic SHA2 Crypt hash digests

View File

@ -0,0 +1,56 @@
---
title: "authelia crypto hash generate argon2"
description: "Reference for the authelia crypto hash generate argon2 command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash generate argon2
Generate cryptographic Argon2 hash digests
### Synopsis
Generate cryptographic Argon2 hash digests.
This subcommand allows generating cryptographic Argon2 hash digests.
```
authelia crypto hash generate argon2 [flags]
```
### Examples
```
authelia crypto hash generate argon2 --help
```
### Options
```
-c, --config strings configuration files to load (default [configuration.yml])
-h, --help help for argon2
-i, --iterations int number of iterations (default 3)
-k, --key-size int key size in bytes (default 32)
-m, --memory int memory in kibibytes (default 65536)
--no-confirm skip the password confirmation prompt
-p, --parallelism int parallelism or threads (default 4)
--password string manually supply the password rather than using the terminal prompt
--profile string profile to use, options are low-memory and recommended
--random uses a randomly generated password
--random.length int when using a randomly generated password it configures the length (default 72)
-s, --salt-size int salt size in bytes (default 16)
-v, --variant string variant, options are 'argon2id', 'argon2i', and 'argon2d' (default "argon2id")
```
### SEE ALSO
* [authelia crypto hash generate](authelia_crypto_hash_generate.md) - Generate cryptographic hash digests

View File

@ -0,0 +1,51 @@
---
title: "authelia crypto hash generate bcrypt"
description: "Reference for the authelia crypto hash generate bcrypt command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash generate bcrypt
Generate cryptographic bcrypt hash digests
### Synopsis
Generate cryptographic bcrypt hash digests.
This subcommand allows generating cryptographic bcrypt hash digests.
```
authelia crypto hash generate bcrypt [flags]
```
### Examples
```
authelia crypto hash generate bcrypt --help
```
### Options
```
-c, --config strings configuration files to load (default [configuration.yml])
-i, --cost int hashing cost (default 12)
-h, --help help for bcrypt
--no-confirm skip the password confirmation prompt
--password string manually supply the password rather than using the terminal prompt
--random uses a randomly generated password
--random.length int when using a randomly generated password it configures the length (default 72)
-v, --variant string variant, options are 'standard' and 'sha256' (default "standard")
```
### SEE ALSO
* [authelia crypto hash generate](authelia_crypto_hash_generate.md) - Generate cryptographic hash digests

View File

@ -0,0 +1,52 @@
---
title: "authelia crypto hash generate pbkdf2"
description: "Reference for the authelia crypto hash generate pbkdf2 command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash generate pbkdf2
Generate cryptographic PBKDF2 hash digests
### Synopsis
Generate cryptographic PBKDF2 hash digests.
This subcommand allows generating cryptographic PBKDF2 hash digests.
```
authelia crypto hash generate pbkdf2 [flags]
```
### Examples
```
authelia crypto hash generate pbkdf2 --help
```
### Options
```
-c, --config strings configuration files to load (default [configuration.yml])
-h, --help help for pbkdf2
-i, --iterations int number of iterations (default 310000)
--no-confirm skip the password confirmation prompt
--password string manually supply the password rather than using the terminal prompt
--random uses a randomly generated password
--random.length int when using a randomly generated password it configures the length (default 72)
-s, --salt-size int salt size in bytes (default 16)
-v, --variant string variant, options are 'sha1', 'sha224', 'sha256', 'sha384', and 'sha512' (default "sha512")
```
### SEE ALSO
* [authelia crypto hash generate](authelia_crypto_hash_generate.md) - Generate cryptographic hash digests

View File

@ -0,0 +1,54 @@
---
title: "authelia crypto hash generate scrypt"
description: "Reference for the authelia crypto hash generate scrypt command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash generate scrypt
Generate cryptographic scrypt hash digests
### Synopsis
Generate cryptographic scrypt hash digests.
This subcommand allows generating cryptographic scrypt hash digests.
```
authelia crypto hash generate scrypt [flags]
```
### Examples
```
authelia crypto hash generate scrypt --help
```
### Options
```
-r, --block-size int block size (default 8)
-c, --config strings configuration files to load (default [configuration.yml])
-h, --help help for scrypt
-i, --iterations int number of iterations (default 16)
-k, --key-size int key size in bytes (default 32)
--no-confirm skip the password confirmation prompt
-p, --parallelism int parallelism or threads (default 1)
--password string manually supply the password rather than using the terminal prompt
--random uses a randomly generated password
--random.length int when using a randomly generated password it configures the length (default 72)
-s, --salt-size int salt size in bytes (default 16)
```
### SEE ALSO
* [authelia crypto hash generate](authelia_crypto_hash_generate.md) - Generate cryptographic hash digests

View File

@ -0,0 +1,52 @@
---
title: "authelia crypto hash generate sha2crypt"
description: "Reference for the authelia crypto hash generate sha2crypt command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash generate sha2crypt
Generate cryptographic SHA2 Crypt hash digests
### Synopsis
Generate cryptographic SHA2 Crypt hash digests.
This subcommand allows generating cryptographic SHA2 Crypt hash digests.
```
authelia crypto hash generate sha2crypt [flags]
```
### Examples
```
authelia crypto hash generate sha2crypt --help
```
### Options
```
-c, --config strings configuration files to load (default [configuration.yml])
-h, --help help for sha2crypt
-i, --iterations int number of iterations (default 50000)
--no-confirm skip the password confirmation prompt
--password string manually supply the password rather than using the terminal prompt
--random uses a randomly generated password
--random.length int when using a randomly generated password it configures the length (default 72)
-s, --salt-size int salt size in bytes (default 16)
-v, --variant string variant, options are sha256 and sha512 (default "sha512")
```
### SEE ALSO
* [authelia crypto hash generate](authelia_crypto_hash_generate.md) - Generate cryptographic hash digests

View File

@ -0,0 +1,46 @@
---
title: "authelia crypto hash validate"
description: "Reference for the authelia crypto hash validate command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto hash validate
Perform cryptographic hash validations
### Synopsis
Perform cryptographic hash validations.
This subcommand allows preforming cryptographic hash validations. i.e. checking hash digests against a password.
```
authelia crypto hash validate [flags] -- <digest>
```
### Examples
```
authelia crypto hash validate --help
authelia crypto hash validate '$5$rounds=500000$WFjMpdCQxIkbNl0k$M0qZaZoK8Gwdh8Cw5diHgGfe5pE0iJvxcVG3.CVnQe.' -- 'p@ssw0rd'
```
### Options
```
-h, --help help for validate
--password string manually supply the password rather than using the terminal prompt
```
### SEE ALSO
* [authelia crypto hash](authelia_crypto_hash.md) - Perform cryptographic hash operations

View File

@ -0,0 +1,55 @@
---
title: "authelia crypto rand"
description: "Reference for the authelia crypto rand command."
lead: ""
date: 2022-08-27T10:46:58+10:00
draft: false
images: []
menu:
reference:
parent: "cli-authelia"
weight: 330
toc: true
---
## authelia crypto rand
Generate a cryptographically secure random string
### Synopsis
Generate a cryptographically secure random string.
This subcommand allows generating cryptographically secure random strings for use for encryption keys, HMAC keys, etc.
```
authelia crypto rand [flags]
```
### Examples
```
authelia crypto rand --help
authelia crypto rand --length 80
authelia crypto rand -n 80
authelia crypto rand --charset alphanumeric
authelia crypto rand --charset alphabetic
authelia crypto rand --charset ascii
authelia crypto rand --charset numeric
authelia crypto rand --charset numeric-hex
authelia crypto rand --characters 0123456789ABCDEF
```
### Options
```
--characters string Sets the explicit characters for the random output
-c, --charset string Sets the charset for the output, options are 'ascii', 'alphanumeric', 'alphabetic', 'numeric', and 'numeric-hex' (default "alphanumeric")
-h, --help help for rand
-n, --length int Sets the length of the random output (default 80)
```
### SEE ALSO
* [authelia crypto](authelia_crypto.md) - Perform cryptographic operations

View File

@ -21,7 +21,7 @@ Hash a password to be used in file-based users database
Hash a password to be used in file-based users database.
```
authelia hash-password [flags] -- <password>
authelia hash-password [flags] -- [password]
```
### Examples
@ -38,13 +38,13 @@ authelia hash-password --key-length=64 -- 'mypass'
### Options
```
-c, --config strings Configuration files
-c, --config strings configuration files to load (default [configuration.yml])
-h, --help help for hash-password
-i, --iterations int set the number of hashing iterations (default 3)
-k, --key-length int [argon2id] set the key length param (default 32)
-m, --memory int [argon2id] set the amount of memory param (in MB) (default 64)
-m, --memory int [argon2id] set the amount of memory param (in MB) (default 65536)
--no-confirm skip the password confirmation prompt
-p, --parallelism int [argon2id] set the parallelism param (default 4)
-s, --salt string set the salt string
-l, --salt-length int set the auto-generated salt length (default 16)
-z, --sha512 use sha512 as the algorithm (changes iterations to 50000, change with -i)
```

View File

@ -52,25 +52,33 @@ users:
The file contains hashed passwords instead of plain text passwords for security reasons.
You can use Authelia binary or docker image to generate the hash of any password. The [hash-password] command has many
tunable options, you can view them with the `authelia hash-password --help` command. For example if you wanted to
improve the entropy you could generate a 16 byte salt and provide it with the `--salt` flag.
You can use Authelia binary or docker image to generate the hash of any password. The [crypt hash generate] command has
many supported algorithms. To view them run the `authelia crypto hash generate --help` command. To see the tunable
options for an algorithm subcommand include that command before `--help`. For example for the [Argon2] algorithm use the
`authelia crypto hash generate argon2 --help` command to see the available options.
Example: `authelia hash-password --salt abcdefghijklhijl -- 'password'`.
Passwords passed to [hash-password] should be single quoted if using special characters to prevent parameter
substitution. In addition the password should be the last parameter, and should be after a `--`. For instance to
generate a hash with the docker image just run:
Passwords passed to [crypt hash generate] should be single quoted if using the `--password` parameter instead of the
console prompt, especially if it has special characters to prevent parameter substitution. For instance to generate an
[Argon2] hash with the docker image just run:
```bash
$ docker run authelia/authelia:latest authelia hash-password -- 'password'
Password hash: $argon2id$v=19$m=65536$3oc26byQuSkQqksq$zM1QiTvVPrMfV6BVLs2t4gM+af5IN7euO0VB6+Q8ZFs
$ docker run authelia/authelia:latest authelia crypto hash generate argon2 --password 'password'
Digest: $argon2id$v=19$m=65536,t=3,p=4$Hjc8e7WYcBFcJmEDUOsS9A$ozM7RyZR1EyDR8cuyVpDDfmLrGPGFgo5E2NNqRumui4
```
You may also use the `--config` flag to point to your existing configuration. When used, the values defined in the
config will be used instead.
config will be used instead. For example to generate the password with a configuration file named `configuration.yml`
in the current directory:
See the [full CLI reference documentation](../cli/authelia/authelia_hash-password.md).
```bash
$ docker run -v ./configuration.yml:/configuration.yml -it authelia/authelia:latest authelia crypto hash generate --config /configuration.yml
Enter Password:
Confirm Password:
Digest: $argon2id$v=19$m=65536,t=3,p=4$Hjc8e7WYcBFcJmEDUOsS9A$ozM7RyZR1EyDR8cuyVpDDfmLrGPGFgo5E2NNqRumui4
```
See the [full CLI reference documentation](../cli/authelia/authelia_crypto_hash_generate.md).
### Cost
@ -88,11 +96,11 @@ all algorithms. The main cost type measurements are:
* CPU
* Memory
*__Important Note:__ When using algorithms that use a memory cost like [Argon2] it should be noted that this memory is
released by Go after the hashing process completes, however the operating system may not reclaim the memory until a
later time such as when the system is experiencing memory pressure which may cause the appearance of more memory being
in use than Authelia is actually actively using. Authelia will typically reuse this memory if it has not be reclaimed as
long as another hashing calculation is not still utilizing it.*
*__Important Note:__ When using algorithms that use a memory cost like [Argon2] and [Scrypt] it should be noted that
this memory is released by Go after the hashing process completes, however the operating system may not reclaim the
memory until a later time such as when the system is experiencing memory pressure which may cause the appearance of more
memory being in use than Authelia is actually actively using. Authelia will typically reuse this memory if it has not be
reclaimed as long as another hashing calculation is not still utilizing it.*
To get a rough estimate of how much memory should be utilized with these algorithms you can utilize the following
command:
@ -114,7 +122,7 @@ widely considered to be the best hashing algorithm, and in 2015 won the [Passwor
customizable parameters including a memory parameter allowing the [cost](#cost) of computing a hash to scale into the
future with better hardware which makes it harder to brute-force.
For backwards compatibility and user choice support for the [SHA Crypt] algorithm (`SHA512` variant) is still available.
For backwards compatibility and user choice support for the [SHA2 Crypt] algorithm (`SHA512` variant) is still available.
While it's a reasonable hashing function given high enough iterations, as hardware improves it has a higher chance of
being brute-forced since it only allows scaling the CPU [cost](#cost) whereas [Argon2] allows scaling both for CPU and
Memory [cost](#cost).
@ -123,10 +131,21 @@ Memory [cost](#cost).
The algorithm that a hash is utilizing is identifiable by its prefix:
| Algorithm | Variant | Prefix |
|:-----------:|:--------:|:------------:|
| [Argon2] | `id` | `$argon2id$` |
| [SHA Crypt] | `SHA512` | `$6$` |
| Algorithm | Variant | Prefix |
|:------------:|:----------:|:-----------------:|
| [Argon2] | `argon2id` | `$argon2id$` |
| [Argon2] | `argon2i` | `$argon2i$` |
| [Argon2] | `argon2d` | `$argon2d$` |
| [Scrypt] | N/A | `$scrypt$` |
| [PBKDF2] | `sha1` | `$pbkdf2$` |
| [PBKDF2] | `sha224` | `$pbkdf2-sha224$` |
| [PBKDF2] | `sha256` | `$pbkdf2-sha256$` |
| [PBKDF2] | `sha384` | `$pbkdf2-sha384$` |
| [PBKDF2] | `sha512` | `$pbkdf2-sha512$` |
| [SHA2 Crypt] | `SHA256` | `$5$` |
| [SHA2 Crypt] | `SHA512` | `$6$` |
| [Bcrypt] | `standard` | `$2b$` |
| [Bcrypt] | `sha256` | `$bcrypt-sha256$` |
See the [Crypt (C) Wiki page](https://en.wikipedia.org/wiki/Crypt_(C)) for more information.
@ -140,27 +159,44 @@ adequately determine the [cost](#cost).
While there are recommended parameters for each algorithm it's your responsibility to tune these individually for your
particular system.
#### Recommended Parameters: Argon2id
#### Algorithm Choice
We generally discourage [Bcrypt] except when needed for interoperability with legacy systems. The `argon2id` variant of
the [Argon2] algorithm is the best choice of the algorithms available, but it's important to note that the `argon2id`
variant is the most resilient variant, followed by the `argon2d` variant and the `argon2i` variant not being recommended.
It's strongly recommended if you're unsure that you use `argon2id`. [Scrypt] is a likely second best algorithm. [PBKDF2]
is practically the only choice when it comes to [FIPS-140 compliance]. The `sha512` variant of the [SHA2 Crypt]
algorithm is also a reasonable option, but is mainly available for backwards compatability.
All other algorithms and variants available exist only for interoperability and we discourage their use if a better
algorithm is available in your scenario.
#### Recommended Parameters: Argon2
This table adapts the [RFC9106 Parameter Choice] recommendations to our configuration options:
| Situation | Iterations (t) | Parallelism (p) | Memory (m) | Salt Size | Key Size |
|:-----------:|:--------------:|:---------------:|:----------:|:---------:|:--------:|
| Low Memory | 3 | 4 | 64 | 16 | 32 |
| Recommended | 1 | 4 | 2048 | 16 | 32 |
| Situation | Variant | Iterations (t) | Parallelism (p) | Memory (m) | Salt Size | Key Size |
|:-----------:|:--------:|:--------------:|:---------------:|:----------:|:---------:|:--------:|
| Low Memory | argon2id | 3 | 4 | 65536 | 16 | 32 |
| Recommended | argon2id | 1 | 4 | 2097152 | 16 | 32 |
#### Recommended Parameters: SHA512
#### Recommended Parameters: SHA2 Crypt
This table suggests the parameters for the [SHA Crypt] (`SHA512` variant) algorithm:
This table suggests the parameters for the [SHA2 Crypt] algorithm:
| Situation | Iterations (rounds) | Salt Size |
|:------------:|:-------------------:|:---------:|
| Standard CPU | 50000 | 16 |
| High End CPU | 150000 | 16 |
| Situation | Variant | Iterations (rounds) | Salt Size |
|:------------:|:-------:|:-------------------:|:---------:|
| Standard CPU | sha512 | 50000 | 16 |
| High End CPU | sha512 | 150000 | 16 |
[Argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
[Scrypt]: https://en.wikipedia.org/wiki/Scrypt
[PBKDF2]: https://www.ietf.org/rfc/rfc2898.html
[SHA2 Crypt]: https://www.akkadia.org/drepper/SHA-crypt.txt
[Bcrypt]: https://en.wikipedia.org/wiki/Bcrypt
[FIPS-140 compliance]: https://csrc.nist.gov/publications/detail/fips/140/2/final
[RFC9106 Parameter Choice]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
[YAML]: https://yaml.org/
[Argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
[SHA Crypt]: https://www.akkadia.org/drepper/SHA-crypt.txt
[hash-password]: ../cli/authelia/authelia_hash-password.md
[crypt hash generate]: ../cli/authelia/authelia_crypto_hash_generate.md
[Password Hashing Competition]: https://en.wikipedia.org/wiki/Password_Hashing_Competition

File diff suppressed because one or more lines are too long

8
go.mod
View File

@ -10,6 +10,7 @@ require (
github.com/fasthttp/router v1.4.12
github.com/fasthttp/session/v2 v2.4.13
github.com/go-asn1-ber/asn1-ber v1.5.4
github.com/go-crypt/crypt v0.1.13
github.com/go-ldap/ldap/v3 v3.4.4
github.com/go-rod/rod v0.111.0
github.com/go-sql-driver/mysql v1.6.0
@ -29,7 +30,6 @@ require (
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.13.0
github.com/simia-tech/crypt v0.5.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.0
github.com/spf13/pflag v1.0.5
@ -37,6 +37,7 @@ require (
github.com/trustelem/zxcvbn v1.0.1
github.com/valyala/fasthttp v1.40.0
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
golang.org/x/term v0.0.0-20220919170432-7a66f970e087
golang.org/x/text v0.3.8
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v3 v3.0.1
@ -57,6 +58,7 @@ require (
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-crypt/x v0.1.3 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-webauthn/revoke v0.1.3 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
@ -106,7 +108,7 @@ require (
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.42.0 // indirect
@ -115,4 +117,4 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/mattn/go-sqlite3 v2.0.3+incompatible => github.com/mattn/go-sqlite3 v1.14.14
replace github.com/mattn/go-sqlite3 v2.0.3+incompatible => github.com/mattn/go-sqlite3 v1.14.15

17
go.sum
View File

@ -239,6 +239,10 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-crypt/crypt v0.1.13 h1:MdmKsHjuT17LmFROTMtGEXBgojN23OyWhmV6JfGZNvw=
github.com/go-crypt/crypt v0.1.13/go.mod h1:VNLdWMD0go46arq5WVZB2MV/9Vw02FOWhKDORXl7K2c=
github.com/go-crypt/x v0.1.3 h1:3YSlHqOZsw4gcPzfqrcc5kg4GIhTKmkjl/ZVqJ3CbbU=
github.com/go-crypt/x v0.1.3/go.mod h1:/6X1DjQki055ajXV/7pCHZM0OmMR1+csiXFkxK73Kc8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -959,8 +963,7 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mattn/goveralls v0.0.6 h1:cr8Y0VMo/MnEZBjxNN/vh6G90SZ7IMb6lms1dzMoO+Y=
@ -1248,8 +1251,6 @@ github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UD
github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/simia-tech/crypt v0.5.1 h1:rQa3qz8Xx4zNu4Uwtl4e6l7AKZBhYLrawZGfZjRLJYU=
github.com/simia-tech/crypt v0.5.1/go.mod h1:VUAuUEkBhS6nI4JupmA8WBg+tkcZCSANpFSMLpsJAcQ=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
@ -1317,7 +1318,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -1493,7 +1493,6 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c=
@ -1722,11 +1721,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -65,6 +65,10 @@ const (
ldapAttributeUserPassword = "userPassword"
)
const (
ldapBaseObjectFilter = "(objectClass=*)"
)
const (
ldapPlaceholderInput = "{input}"
ldapPlaceholderDistinguishedName = "{dn}"
@ -75,37 +79,17 @@ const (
none = "none"
)
// CryptAlgo the crypt representation of an algorithm used in the prefix of the hash.
type CryptAlgo string
const (
// HashingAlgorithmArgon2id Argon2id hash identifier.
HashingAlgorithmArgon2id CryptAlgo = argon2id
// HashingAlgorithmSHA512 SHA512 hash identifier.
HashingAlgorithmSHA512 CryptAlgo = "6"
hashArgon2 = "argon2"
hashSHA2Crypt = "sha2crypt"
hashPBKDF2 = "pbkdf2"
hashSCrypt = "scrypt"
hashBCrypt = "bcrypt"
)
// These are the default values from the upstream crypt module we use them to for GetInt
// and they need to be checked when updating github.com/simia-tech/crypt.
const (
HashingDefaultArgon2idTime = 1
HashingDefaultArgon2idMemory = 32 * 1024
HashingDefaultArgon2idParallelism = 4
HashingDefaultArgon2idKeyLength = 32
HashingDefaultSHA512Iterations = 5000
)
// HashingPossibleSaltCharacters represents valid hashing runes.
var HashingPossibleSaltCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"
// ErrUserNotFound indicates the user wasn't found in the authentication backend.
var ErrUserNotFound = errors.New("user not found")
const argon2id = "argon2id"
const sha512 = "sha512"
const testPassword = "my;secure*password"
const fileAuthenticationMode = 0600
// OWASP recommends to escape some special characters.

View File

@ -4,11 +4,8 @@ import (
_ "embed" // Embed users_database.template.yml.
"fmt"
"os"
"strings"
"sync"
"github.com/asaskevich/govalidator"
"gopkg.in/yaml.v3"
"github.com/go-crypt/crypt"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging"
@ -16,196 +13,148 @@ import (
// FileUserProvider is a provider reading details from a file.
type FileUserProvider struct {
configuration *schema.FileAuthenticationBackendConfiguration
database *DatabaseModel
lock *sync.Mutex
}
// UserDetailsModel is the model of user details in the file database.
type UserDetailsModel struct {
HashedPassword string `yaml:"password" valid:"required"`
DisplayName string `yaml:"displayname" valid:"required"`
Email string `yaml:"email"`
Groups []string `yaml:"groups"`
}
// DatabaseModel is the model of users file database.
type DatabaseModel struct {
Users map[string]UserDetailsModel `yaml:"users" valid:"required"`
config *schema.FileAuthenticationBackend
hash crypt.Hash
database *FileUserDatabase
}
// NewFileUserProvider creates a new instance of FileUserProvider.
func NewFileUserProvider(configuration *schema.FileAuthenticationBackendConfiguration) *FileUserProvider {
logger := logging.Logger()
errs := checkDatabase(configuration.Path)
if errs != nil {
for _, err := range errs {
logger.Error(err)
}
os.Exit(1)
}
database, err := readDatabase(configuration.Path)
if err != nil {
// Panic since the file does not exist when Authelia is starting.
panic(err)
}
// Early check whether hashed passwords are correct for all users.
err = checkPasswordHashes(database)
if err != nil {
panic(err)
}
func NewFileUserProvider(config *schema.FileAuthenticationBackend) (provider *FileUserProvider) {
return &FileUserProvider{
configuration: configuration,
database: database,
lock: &sync.Mutex{},
config: config,
}
}
func checkPasswordHashes(database *DatabaseModel) error {
for u, v := range database.Users {
v.HashedPassword = strings.ReplaceAll(v.HashedPassword, "{CRYPT}", "")
_, err := ParseHash(v.HashedPassword)
// CheckUserPassword checks if provided password matches for the given user.
func (p *FileUserProvider) CheckUserPassword(username string, password string) (match bool, err error) {
var details DatabaseUserDetails
if err != nil {
return fmt.Errorf("Unable to parse hash of user %s: %s", u, err)
}
if details, err = p.database.GetUserDetails(username); err != nil {
return false, err
}
database.Users[u] = v
if details.Disabled {
return false, ErrUserNotFound
}
return details.Digest.MatchAdvanced(password)
}
// GetDetails retrieve the groups a user belongs to.
func (p *FileUserProvider) GetDetails(username string) (details *UserDetails, err error) {
var d DatabaseUserDetails
if d, err = p.database.GetUserDetails(username); err != nil {
return nil, err
}
if d.Disabled {
return nil, ErrUserNotFound
}
return d.ToUserDetails(), nil
}
// UpdatePassword update the password of the given user.
func (p *FileUserProvider) UpdatePassword(username string, newPassword string) (err error) {
var details DatabaseUserDetails
if details, err = p.database.GetUserDetails(username); err != nil {
return err
}
if details.Disabled {
return ErrUserNotFound
}
if details.Digest, err = p.hash.Hash(newPassword); err != nil {
return err
}
p.database.SetUserDetails(details.Username, &details)
if err = p.database.Save(); err != nil {
return err
}
return nil
}
func checkDatabase(path string) []error {
_, err := os.Stat(path)
if err != nil {
errs := []error{
fmt.Errorf("Unable to find database file: %v", path),
fmt.Errorf("Generating database file: %v", path),
// StartupCheck implements the startup check provider interface.
func (p *FileUserProvider) StartupCheck() (err error) {
if err = checkDatabase(p.config.Path); err != nil {
logging.Logger().WithError(err).Errorf("Error checking user authentication YAML database")
return fmt.Errorf("one or more errors occurred checking the authentication database")
}
if p.hash, err = NewFileCryptoHashFromConfig(p.config.Password); err != nil {
return err
}
p.database = NewFileUserDatabase(p.config.Path)
if err = p.database.Load(); err != nil {
return err
}
return nil
}
// NewFileCryptoHashFromConfig returns a crypt.Hash given a valid configuration.
func NewFileCryptoHashFromConfig(config schema.Password) (hash crypt.Hash, err error) {
switch config.Algorithm {
case hashArgon2, "":
hash = crypt.NewArgon2Hash().
WithVariant(crypt.NewArgon2Variant(config.Argon2.Variant)).
WithT(config.Argon2.Iterations).
WithM(config.Argon2.Memory).
WithP(config.Argon2.Parallelism).
WithK(config.Argon2.KeyLength).
WithS(config.Argon2.SaltLength)
case hashSHA2Crypt:
hash = crypt.NewSHA2CryptHash().
WithVariant(crypt.NewSHA2CryptVariant(config.SHA2Crypt.Variant)).
WithRounds(config.SHA2Crypt.Iterations).
WithSaltLength(config.SHA2Crypt.SaltLength)
case hashPBKDF2:
hash = crypt.NewPBKDF2Hash().
WithVariant(crypt.NewPBKDF2Variant(config.PBKDF2.Variant)).
WithIterations(config.PBKDF2.Iterations).
WithSaltLength(config.PBKDF2.SaltLength)
case hashSCrypt:
hash = crypt.NewScryptHash().
WithLN(config.SCrypt.Iterations).
WithP(config.SCrypt.Parallelism).
WithR(config.SCrypt.BlockSize)
case hashBCrypt:
hash = crypt.NewBcryptHash().
WithVariant(crypt.NewBcryptVariant(config.BCrypt.Variant)).
WithCost(config.BCrypt.Cost)
default:
return nil, fmt.Errorf("algorithm '%s' is unknown", config.Algorithm)
}
if err = hash.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate hash settings: %w", err)
}
return hash, nil
}
func checkDatabase(path string) (err error) {
if _, err = os.Stat(path); os.IsNotExist(err) {
if err = os.WriteFile(path, userYAMLTemplate, 0600); err != nil {
return fmt.Errorf("user authentication database file doesn't exist at path '%s' and could not be generated: %w", path, err)
}
err := generateDatabaseFromTemplate(path)
if err != nil {
errs = append(errs, err)
} else {
errs = append(errs, fmt.Errorf("Generated database at: %v", path))
}
return errs
return fmt.Errorf("user authentication database file doesn't exist at path '%s' and has been generated", path)
} else if err != nil {
return fmt.Errorf("error checking user authentication database file: %w", err)
}
return nil
}
//go:embed users_database.template.yml
var cfg []byte
func generateDatabaseFromTemplate(path string) error {
err := os.WriteFile(path, cfg, 0600)
if err != nil {
return fmt.Errorf("Unable to generate %v: %v", path, err)
}
return nil
}
func readDatabase(path string) (*DatabaseModel, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("Unable to read database from file %s: %s", path, err)
}
db := DatabaseModel{}
err = yaml.Unmarshal(content, &db)
if err != nil {
return nil, fmt.Errorf("Unable to parse database: %s", err)
}
ok, err := govalidator.ValidateStruct(db)
if err != nil {
return nil, fmt.Errorf("Invalid schema of database: %s", err)
}
if !ok {
return nil, fmt.Errorf("The database format is invalid: %s", err)
}
return &db, nil
}
// CheckUserPassword checks if provided password matches for the given user.
func (p *FileUserProvider) CheckUserPassword(username string, password string) (bool, error) {
if details, ok := p.database.Users[username]; ok {
ok, err := CheckPassword(password, details.HashedPassword)
if err != nil {
return false, err
}
return ok, nil
}
return false, ErrUserNotFound
}
// GetDetails retrieve the groups a user belongs to.
func (p *FileUserProvider) GetDetails(username string) (*UserDetails, error) {
if details, ok := p.database.Users[username]; ok {
return &UserDetails{
Username: username,
DisplayName: details.DisplayName,
Groups: details.Groups,
Emails: []string{details.Email},
}, nil
}
return nil, fmt.Errorf("User '%s' does not exist in database", username)
}
// UpdatePassword update the password of the given user.
func (p *FileUserProvider) UpdatePassword(username string, newPassword string) error {
details, ok := p.database.Users[username]
if !ok {
return ErrUserNotFound
}
algorithm, err := ConfigAlgoToCryptoAlgo(p.configuration.Password.Algorithm)
if err != nil {
return err
}
hash, err := HashPassword(
newPassword, "", algorithm, p.configuration.Password.Iterations,
p.configuration.Password.Memory*1024, p.configuration.Password.Parallelism,
p.configuration.Password.KeyLength, p.configuration.Password.SaltLength)
if err != nil {
return err
}
details.HashedPassword = hash
p.lock.Lock()
p.database.Users[username] = details
b, err := yaml.Marshal(p.database)
if err != nil {
p.lock.Unlock()
return err
}
err = os.WriteFile(p.configuration.Path, b, fileAuthenticationMode)
p.lock.Unlock()
return err
}
// StartupCheck implements the startup check provider interface.
func (p *FileUserProvider) StartupCheck() (err error) {
return nil
}
var userYAMLTemplate []byte

View File

@ -0,0 +1,222 @@
package authentication
import (
"fmt"
"os"
"sync"
"github.com/asaskevich/govalidator"
"github.com/go-crypt/crypt"
"gopkg.in/yaml.v3"
)
// NewFileUserDatabase creates a new FileUserDatabase.
func NewFileUserDatabase(filePath string) (database *FileUserDatabase) {
return &FileUserDatabase{
RWMutex: &sync.RWMutex{},
Path: filePath,
Users: map[string]DatabaseUserDetails{},
}
}
// FileUserDatabase is a user details database that is concurrency safe database and can be reloaded.
type FileUserDatabase struct {
*sync.RWMutex
Path string
Users map[string]DatabaseUserDetails
}
// Save the database to disk.
func (m *FileUserDatabase) Save() (err error) {
m.RLock()
defer m.RUnlock()
if err = m.ToDatabaseModel().Write(m.Path); err != nil {
return err
}
return nil
}
// Load the database from disk.
func (m *FileUserDatabase) Load() (err error) {
yml := &DatabaseModel{Users: map[string]UserDetailsModel{}}
if err = yml.Read(m.Path); err != nil {
return fmt.Errorf("error reading the authentication database: %w", err)
}
m.Lock()
defer m.Unlock()
if err = yml.ReadToFileUserDatabase(m); err != nil {
return fmt.Errorf("error decoding the authentication database: %w", err)
}
return nil
}
// GetUserDetails get a DatabaseUserDetails given a username as a value type where the username must be the users actual
// username.
func (m *FileUserDatabase) GetUserDetails(username string) (user DatabaseUserDetails, err error) {
m.RLock()
defer m.RUnlock()
if details, ok := m.Users[username]; ok {
return details, nil
}
return user, ErrUserNotFound
}
// SetUserDetails sets the DatabaseUserDetails for a given user.
func (m *FileUserDatabase) SetUserDetails(username string, details *DatabaseUserDetails) {
if details == nil {
return
}
m.Lock()
m.Users[username] = *details
m.Unlock()
}
// ToDatabaseModel converts the FileUserDatabase into the DatabaseModel for saving.
func (m *FileUserDatabase) ToDatabaseModel() (model *DatabaseModel) {
model = &DatabaseModel{
Users: map[string]UserDetailsModel{},
}
m.RLock()
for user, details := range m.Users {
model.Users[user] = details.ToUserDetailsModel()
}
m.RUnlock()
return model
}
// DatabaseUserDetails is the model of user details in the file database.
type DatabaseUserDetails struct {
Username string
Digest crypt.Digest
Disabled bool
DisplayName string
Email string
Groups []string
}
// ToUserDetails converts DatabaseUserDetails into a *UserDetails given a username.
func (m DatabaseUserDetails) ToUserDetails() (details *UserDetails) {
return &UserDetails{
Username: m.Username,
DisplayName: m.DisplayName,
Emails: []string{m.Email},
Groups: m.Groups,
}
}
// ToUserDetailsModel converts DatabaseUserDetails into a UserDetailsModel.
func (m DatabaseUserDetails) ToUserDetailsModel() (model UserDetailsModel) {
return UserDetailsModel{
HashedPassword: m.Digest.Encode(),
DisplayName: m.DisplayName,
Email: m.Email,
Groups: m.Groups,
}
}
// DatabaseModel is the model of users file database.
type DatabaseModel struct {
Users map[string]UserDetailsModel `yaml:"users" valid:"required"`
}
// ReadToFileUserDatabase reads the DatabaseModel into a FileUserDatabase.
func (m *DatabaseModel) ReadToFileUserDatabase(db *FileUserDatabase) (err error) {
users := map[string]DatabaseUserDetails{}
var udm *DatabaseUserDetails
for user, details := range m.Users {
if udm, err = details.ToDatabaseUserDetailsModel(user); err != nil {
return fmt.Errorf("failed to parse hash for user '%s': %w", user, err)
}
users[user] = *udm
}
db.Users = users
return nil
}
// Read a DatabaseModel from disk.
func (m *DatabaseModel) Read(filePath string) (err error) {
var (
content []byte
ok bool
)
if content, err = os.ReadFile(filePath); err != nil {
return fmt.Errorf("failed to read the '%s' file: %w", filePath, err)
}
if err = yaml.Unmarshal(content, m); err != nil {
return fmt.Errorf("could not parse the YAML database: %w", err)
}
if ok, err = govalidator.ValidateStruct(m); err != nil {
return fmt.Errorf("could not validate the schema: %w", err)
}
if !ok {
return fmt.Errorf("the schema is invalid")
}
return nil
}
// Write a DatabaseModel to disk.
func (m *DatabaseModel) Write(fileName string) (err error) {
var (
data []byte
)
if data, err = yaml.Marshal(m); err != nil {
return err
}
return os.WriteFile(fileName, data, fileAuthenticationMode)
}
// UserDetailsModel is the model of user details in the file database.
type UserDetailsModel struct {
HashedPassword string `yaml:"password" valid:"required"`
DisplayName string `yaml:"displayname" valid:"required"`
Email string `yaml:"email"`
Groups []string `yaml:"groups"`
}
// ToDatabaseUserDetailsModel converts a UserDetailsModel into a *DatabaseUserDetails.
func (m UserDetailsModel) ToDatabaseUserDetailsModel(username string) (model *DatabaseUserDetails, err error) {
var d crypt.Digest
if d, err = crypt.Decode(m.HashedPassword); err != nil {
return nil, err
}
return &DatabaseUserDetails{
Username: username,
Digest: d,
DisplayName: m.DisplayName,
Email: m.Email,
Groups: m.Groups,
}, nil
}

View File

@ -39,24 +39,16 @@ func TestShouldErrorPermissionsOnLocalFS(t *testing.T) {
}
_ = os.Mkdir("/tmp/noperms/", 0000)
errors := checkDatabase("/tmp/noperms/users_database.yml")
err := checkDatabase("/tmp/noperms/users_database.yml")
require.Len(t, errors, 3)
require.EqualError(t, errors[0], "Unable to find database file: /tmp/noperms/users_database.yml")
require.EqualError(t, errors[1], "Generating database file: /tmp/noperms/users_database.yml")
require.EqualError(t, errors[2], "Unable to generate /tmp/noperms/users_database.yml: open /tmp/noperms/users_database.yml: permission denied")
require.EqualError(t, err, "error checking user authentication database file: stat /tmp/noperms/users_database.yml: permission denied")
}
func TestShouldErrorAndGenerateUserDB(t *testing.T) {
errors := checkDatabase("./nonexistent.yml")
err := checkDatabase("./nonexistent.yml")
_ = os.Remove("./nonexistent.yml")
require.Len(t, errors, 3)
require.EqualError(t, errors[0], "Unable to find database file: ./nonexistent.yml")
require.EqualError(t, errors[1], "Generating database file: ./nonexistent.yml")
require.EqualError(t, errors[2], "Generated database at: ./nonexistent.yml")
require.EqualError(t, err, "user authentication database file doesn't exist at path './nonexistent.yml' and has been generated")
}
func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
@ -64,6 +56,9 @@ func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err)
@ -75,7 +70,11 @@ func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("harry", "password")
assert.NoError(t, err)
@ -87,7 +86,11 @@ func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("john", "wrong_password")
assert.NoError(t, err)
@ -99,8 +102,11 @@ func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("enumeration", "wrong_password")
assert.NoError(t, err)
assert.False(t, ok)
@ -111,7 +117,11 @@ func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("fake", "password")
assert.Error(t, err)
assert.Equal(t, false, ok)
@ -123,12 +133,16 @@ func TestShouldRetrieveUserDetails(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
details, err := provider.GetDetails("john")
assert.NoError(t, err)
assert.Equal(t, details.Username, "john")
assert.Equal(t, details.Emails, []string{"john.doe@authelia.com"})
assert.Equal(t, details.Groups, []string{"admins", "dev"})
assert.Equal(t, "john", details.Username)
assert.Equal(t, []string{"john.doe@authelia.com"}, details.Emails)
assert.Equal(t, []string{"admins", "dev"}, details.Groups)
})
}
@ -136,12 +150,19 @@ func TestShouldUpdatePassword(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
@ -153,17 +174,24 @@ func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "$6$"))
assert.NoError(t, provider.StartupCheck())
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].Digest.Encode(), "$6$"))
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "$argon2id$"))
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].Digest.Encode(), "$argon2id$"))
})
}
@ -171,20 +199,26 @@ func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
config.Password.Algorithm = "sha512"
config.Password.Iterations = 50000
config.Password.Algorithm = "sha2crypt"
config.Password.SHA2Crypt.Iterations = 50000
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "$argon2id$"))
assert.NoError(t, provider.StartupCheck())
assert.True(t, strings.HasPrefix(provider.database.Users["john"].Digest.Encode(), "$argon2id$"))
err := provider.UpdatePassword("john", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("john", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "$6$"))
assert.True(t, strings.HasPrefix(provider.database.Users["john"].Digest.Encode(), "$6$"))
})
}
@ -192,9 +226,10 @@ func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
WithDatabase(MalformedUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse database: yaml: line 4: mapping values are not allowed in this context", func() {
NewFileUserProvider(&config)
})
provider := NewFileUserProvider(&config)
assert.EqualError(t, provider.StartupCheck(), "error reading the authentication database: could not parse the YAML database: yaml: line 4: mapping values are not allowed in this context")
})
}
@ -202,9 +237,10 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
WithDatabase(BadSchemaUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Invalid schema of database: Users: non zero value required", func() {
NewFileUserProvider(&config)
})
provider := NewFileUserProvider(&config)
assert.EqualError(t, provider.StartupCheck(), "error reading the authentication database: could not validate the schema: Users: non zero value required")
})
}
@ -212,9 +248,10 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *tes
WithDatabase(BadSHA512HashContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/)", func() {
NewFileUserProvider(&config)
})
provider := NewFileUserProvider(&config)
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': sha2crypt decode error: provided encoded hash has an invalid option: option 'rounds00000' is invalid")
})
}
@ -222,9 +259,10 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTim
WithDatabase(BadArgon2idHashSettingsContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM)", func() {
NewFileUserProvider(&config)
})
provider := NewFileUserProvider(&config)
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': argon2 decode error: provided encoded hash has an invalid option: option 'm65536' is invalid")
})
}
@ -232,9 +270,10 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *
WithDatabase(BadArgon2idHashKeyContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Hash key contains invalid base64 characters", func() {
NewFileUserProvider(&config)
})
provider := NewFileUserProvider(&config)
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': argon2 decode error: provided encoded hash has a key value that can't be decoded: illegal base64 data at input byte 0")
})
}
@ -242,9 +281,10 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t
WithDatabase(BadArgon2idHashSaltContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Salt contains invalid base64 characters", func() {
NewFileUserProvider(&config)
})
provider := NewFileUserProvider(&config)
assert.EqualError(t, provider.StartupCheck(), "error decoding the authentication database: failed to parse hash for user 'john': argon2 decode error: provided encoded hash has a salt value that can't be decoded: illegal base64 data at input byte 0")
})
}
@ -252,7 +292,11 @@ func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
WithDatabase(UserDatabaseWithoutCryptContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err)
@ -261,16 +305,9 @@ func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
}
var (
DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackendConfiguration{
Path: "",
Password: &schema.PasswordConfiguration{
Iterations: schema.DefaultCIPasswordConfiguration.Iterations,
KeyLength: schema.DefaultCIPasswordConfiguration.KeyLength,
SaltLength: schema.DefaultCIPasswordConfiguration.SaltLength,
Algorithm: schema.DefaultCIPasswordConfiguration.Algorithm,
Memory: schema.DefaultCIPasswordConfiguration.Memory,
Parallelism: schema.DefaultCIPasswordConfiguration.Parallelism,
},
DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackend{
Path: "",
Password: schema.DefaultCIPasswordConfig,
}
)
@ -385,6 +422,7 @@ users:
- admins
- dev
`)
var BadArgon2idHashSaltContent = []byte(`
users:
john:

View File

@ -17,7 +17,7 @@ import (
// LDAPUserProvider is a UserProvider that connects to LDAP servers like ActiveDirectory, OpenLDAP, OpenDJ, FreeIPA, etc.
type LDAPUserProvider struct {
config schema.LDAPAuthenticationBackendConfiguration
config schema.LDAPAuthenticationBackend
tlsConfig *tls.Config
dialOpts []ldap.DialOpt
log *logrus.Logger
@ -42,15 +42,15 @@ type LDAPUserProvider struct {
}
// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
func NewLDAPUserProvider(config schema.AuthenticationBackendConfiguration, certPool *x509.CertPool) (provider *LDAPUserProvider) {
func NewLDAPUserProvider(config schema.AuthenticationBackend, certPool *x509.CertPool) (provider *LDAPUserProvider) {
provider = newLDAPUserProvider(*config.LDAP, config.PasswordReset.Disable, certPool, nil)
return provider
}
func newLDAPUserProvider(config schema.LDAPAuthenticationBackendConfiguration, disableResetPassword bool, certPool *x509.CertPool, factory LDAPClientFactory) (provider *LDAPUserProvider) {
func newLDAPUserProvider(config schema.LDAPAuthenticationBackend, disableResetPassword bool, certPool *x509.CertPool, factory LDAPClientFactory) (provider *LDAPUserProvider) {
if config.TLS == nil {
config.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
config.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS
}
tlsConfig := utils.NewTLSConfig(config.TLS, tls.VersionTLS12, certPool)
@ -126,9 +126,9 @@ func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, er
}
var (
filter string
searchRequest *ldap.SearchRequest
searchResult *ldap.SearchResult
filter string
request *ldap.SearchRequest
result *ldap.SearchResult
)
if filter, err = p.resolveGroupsFilter(username, profile); err != nil {
@ -136,18 +136,18 @@ func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, er
}
// Search for the users groups.
searchRequest = ldap.NewSearchRequest(
request = ldap.NewSearchRequest(
p.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
0, 0, false, filter, p.groupsAttributes, nil,
)
if searchResult, err = p.search(client, searchRequest); err != nil {
if result, err = p.search(client, request); err != nil {
return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err)
}
groups := make([]string, 0)
for _, res := range searchResult.Entries {
for _, res := range result.Entries {
if len(res.Attributes) == 0 {
p.log.Warningf("No groups retrieved from LDAP for user %s", username)
break
@ -254,35 +254,35 @@ func (p *LDAPUserProvider) connectCustom(url, username, password string, startTL
return client, nil
}
func (p *LDAPUserProvider) search(client LDAPClient, searchRequest *ldap.SearchRequest) (searchResult *ldap.SearchResult, err error) {
if searchResult, err = client.Search(searchRequest); err != nil {
func (p *LDAPUserProvider) search(client LDAPClient, request *ldap.SearchRequest) (result *ldap.SearchResult, err error) {
if result, err = client.Search(request); err != nil {
if referral, ok := p.getReferral(err); ok {
if searchResult == nil {
searchResult = &ldap.SearchResult{
if result == nil {
result = &ldap.SearchResult{
Referrals: []string{referral},
}
} else {
searchResult.Referrals = append(searchResult.Referrals, referral)
result.Referrals = append(result.Referrals, referral)
}
}
}
if !p.config.PermitReferrals || len(searchResult.Referrals) == 0 {
if !p.config.PermitReferrals || len(result.Referrals) == 0 {
if err != nil {
return nil, err
}
return searchResult, nil
return result, nil
}
if err = p.searchReferrals(searchRequest, searchResult); err != nil {
if err = p.searchReferrals(request, result); err != nil {
return nil, err
}
return searchResult, nil
return result, nil
}
func (p *LDAPUserProvider) searchReferral(referral string, searchRequest *ldap.SearchRequest, searchResult *ldap.SearchResult) (err error) {
func (p *LDAPUserProvider) searchReferral(referral string, request *ldap.SearchRequest, searchResult *ldap.SearchResult) (err error) {
var (
client LDAPClient
result *ldap.SearchResult
@ -294,7 +294,7 @@ func (p *LDAPUserProvider) searchReferral(referral string, searchRequest *ldap.S
defer client.Close()
if result, err = client.Search(searchRequest); err != nil {
if result, err = client.Search(request); err != nil {
return fmt.Errorf("error occurred performing search on referred LDAP server '%s': %w", referral, err)
}
@ -307,9 +307,9 @@ func (p *LDAPUserProvider) searchReferral(referral string, searchRequest *ldap.S
return nil
}
func (p *LDAPUserProvider) searchReferrals(searchRequest *ldap.SearchRequest, searchResult *ldap.SearchResult) (err error) {
for i := 0; i < len(searchResult.Referrals); i++ {
if err = p.searchReferral(searchResult.Referrals[i], searchRequest, searchResult); err != nil {
func (p *LDAPUserProvider) searchReferrals(request *ldap.SearchRequest, result *ldap.SearchResult) (err error) {
for i := 0; i < len(result.Referrals); i++ {
if err = p.searchReferral(result.Referrals[i], request, result); err != nil {
return err
}
}
@ -321,30 +321,30 @@ func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (p
userFilter := p.resolveUsersFilter(username)
// Search for the given username.
searchRequest := ldap.NewSearchRequest(
request := ldap.NewSearchRequest(
p.usersBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
1, 0, false, userFilter, p.usersAttributes, nil,
)
var searchResult *ldap.SearchResult
var result *ldap.SearchResult
if searchResult, err = p.search(client, searchRequest); err != nil {
if result, err = p.search(client, request); err != nil {
return nil, fmt.Errorf("cannot find user DN of user '%s'. Cause: %w", username, err)
}
if len(searchResult.Entries) == 0 {
if len(result.Entries) == 0 {
return nil, ErrUserNotFound
}
if len(searchResult.Entries) > 1 {
return nil, fmt.Errorf("there were %d users found when searching for '%s' but there should only be 1", len(searchResult.Entries), username)
if len(result.Entries) > 1 {
return nil, fmt.Errorf("there were %d users found when searching for '%s' but there should only be 1", len(result.Entries), username)
}
userProfile := ldapUserProfile{
DN: searchResult.Entries[0].DN,
DN: result.Entries[0].DN,
}
for _, attr := range searchResult.Entries[0].Attributes {
for _, attr := range result.Entries[0].Attributes {
attrs := len(attr.Values)
if attr.Name == p.config.UsernameAttribute {

View File

@ -47,14 +47,14 @@ func (p *LDAPUserProvider) StartupCheck() (err error) {
func (p *LDAPUserProvider) getServerSupportedFeatures(client LDAPClient) (features LDAPSupportedFeatures, err error) {
var (
searchRequest *ldap.SearchRequest
searchResult *ldap.SearchResult
request *ldap.SearchRequest
result *ldap.SearchResult
)
searchRequest = ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases,
1, 0, false, "(objectClass=*)", []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute}, nil)
request = ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases,
1, 0, false, ldapBaseObjectFilter, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute}, nil)
if searchResult, err = client.Search(searchRequest); err != nil {
if result, err = client.Search(request); err != nil {
if p.config.PermitFeatureDetectionFailure {
p.log.WithError(err).Warnf("Error occurred during RootDSE search. This may result in reduced functionality.")
@ -64,7 +64,7 @@ func (p *LDAPUserProvider) getServerSupportedFeatures(client LDAPClient) (featur
return features, fmt.Errorf("error occurred during RootDSE search: %w", err)
}
if len(searchResult.Entries) != 1 {
if len(result.Entries) != 1 {
p.log.Errorf("The LDAP Server did not respond appropriately to a RootDSE search. This may result in reduced functionality.")
return features, nil
@ -72,7 +72,7 @@ func (p *LDAPUserProvider) getServerSupportedFeatures(client LDAPClient) (featur
var controlTypeOIDs, extensionOIDs []string
controlTypeOIDs, extensionOIDs, features = ldapGetFeatureSupportFromEntry(searchResult.Entries[0])
controlTypeOIDs, extensionOIDs, features = ldapGetFeatureSupportFromEntry(result.Entries[0])
controlTypes, extensions := none, none

View File

@ -23,7 +23,7 @@ func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -55,7 +55,7 @@ func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldaps://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -105,7 +105,7 @@ func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) {
mockFactory := NewMockLDAPClientFactory(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldaps://127.0.0.1:389",
GroupsFilter: "(|(member={dn})(uid={username})(uid={input}))",
},
@ -163,7 +163,7 @@ func TestShouldCheckLDAPServerExtensions(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
@ -228,7 +228,7 @@ func TestShouldNotCheckLDAPServerExtensionsWhenRootDSEReturnsMoreThanOneEntry(t
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
@ -294,7 +294,7 @@ func TestShouldCheckLDAPServerControlTypes(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
@ -359,7 +359,7 @@ func TestShouldNotEnablePasswdModifyExtensionOrControlTypes(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
@ -424,7 +424,7 @@ func TestShouldReturnCheckServerConnectError(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
@ -457,7 +457,7 @@ func TestShouldReturnCheckServerSearchError(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
@ -519,7 +519,7 @@ func TestShouldEscapeUserInput(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
@ -553,7 +553,7 @@ func TestShouldReturnEmailWhenAttributeSameAsUsername(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -624,7 +624,7 @@ func TestShouldReturnUsernameAndBlankDisplayNameWhenAttributesTheSame(t *testing
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -695,7 +695,7 @@ func TestShouldReturnBlankEmailAndDisplayNameWhenAttrsLenZero(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -769,7 +769,7 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsernameAttribute: "uid",
@ -820,7 +820,7 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -893,7 +893,7 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -954,7 +954,7 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -1027,7 +1027,7 @@ func TestShouldReturnUsernameFromLDAPWithReferrals(t *testing.T) {
mockClientReferral := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -1119,7 +1119,7 @@ func TestShouldReturnUsernameFromLDAPWithReferralsInErrorAndResult(t *testing.T)
mockClientReferralAlt := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -1244,7 +1244,7 @@ func TestShouldReturnUsernameFromLDAPWithReferralsErr(t *testing.T) {
mockClientReferral := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -1331,7 +1331,7 @@ func TestShouldNotUpdateUserPasswordConnect(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -1398,7 +1398,7 @@ func TestShouldNotUpdateUserPasswordGetDetails(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -1475,7 +1475,7 @@ func TestShouldUpdateUserPassword(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -1582,7 +1582,7 @@ func TestShouldUpdateUserPasswordMSAD(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -1692,7 +1692,7 @@ func TestShouldUpdateUserPasswordMSADWithReferrals(t *testing.T) {
mockClientReferral := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -1820,7 +1820,7 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralConnectErr(t *test
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -1939,7 +1939,7 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralModifyErr(t *testi
mockClientReferral := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -2071,7 +2071,7 @@ func TestShouldUpdateUserPasswordMSADWithoutReferrals(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -2185,7 +2185,7 @@ func TestShouldUpdateUserPasswordPasswdModifyExtension(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -2292,7 +2292,7 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferrals(t *testing.T
mockClientReferral := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -2419,7 +2419,7 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithoutReferrals(t *testin
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -2532,7 +2532,7 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferralsReferralConne
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -2650,7 +2650,7 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferralsReferralPassw
mockClientReferral := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -2781,7 +2781,7 @@ func TestShouldUpdateUserPasswordActiveDirectoryWithServerPolicyHints(t *testing
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -2892,7 +2892,7 @@ func TestShouldUpdateUserPasswordActiveDirectoryWithServerPolicyHintsDeprecated(
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -3003,7 +3003,7 @@ func TestShouldUpdateUserPasswordActiveDirectory(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "activedirectory",
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
@ -3114,7 +3114,7 @@ func TestShouldUpdateUserPasswordBasic(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
Implementation: "custom",
URL: "ldap://127.0.0.1:389",
User: "uid=admin,dc=example,dc=com",
@ -3222,7 +3222,7 @@ func TestShouldReturnErrorWhenMultipleUsernameAttributes(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3288,7 +3288,7 @@ func TestShouldReturnErrorWhenZeroUsernameAttributes(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3354,7 +3354,7 @@ func TestShouldReturnErrorWhenUsernameAttributeNotReturned(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3416,7 +3416,7 @@ func TestShouldReturnErrorWhenMultipleUsersFound(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3499,7 +3499,7 @@ func TestShouldReturnErrorWhenNoDN(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3565,7 +3565,7 @@ func TestShouldCheckValidUserPassword(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3633,7 +3633,7 @@ func TestShouldNotCheckValidUserPasswordWithConnectError(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3672,7 +3672,7 @@ func TestShouldCheckInvalidUserPassword(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3740,7 +3740,7 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3815,7 +3815,7 @@ func TestShouldParseDynamicConfiguration(t *testing.T) {
mockFactory := NewMockLDAPClientFactory(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3853,7 +3853,7 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -3936,7 +3936,7 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) {
mockClient := NewMockLDAPClient(ctrl)
ldapClient := newLDAPUserProvider(
schema.LDAPAuthenticationBackendConfiguration{
schema.LDAPAuthenticationBackend{
URL: "ldaps://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",

View File

@ -1,220 +0,0 @@
package authentication
import (
"crypto/subtle"
"errors"
"fmt"
"strconv"
"strings"
"github.com/simia-tech/crypt"
"github.com/authelia/authelia/v4/internal/utils"
)
// PasswordHash represents all characteristics of a password hash.
// Authelia only supports salted SHA512 or salted argon2id method, i.e., $6$ mode or $argon2id$ mode.
type PasswordHash struct {
Algorithm CryptAlgo
Iterations int
Salt string
Key string
KeyLength int
Memory int
Parallelism int
}
// ConfigAlgoToCryptoAlgo returns a CryptAlgo and nil error if valid, otherwise it returns argon2id and an error.
func ConfigAlgoToCryptoAlgo(fromConfig string) (CryptAlgo, error) {
switch fromConfig {
case argon2id:
return HashingAlgorithmArgon2id, nil
case sha512:
return HashingAlgorithmSHA512, nil
default:
return HashingAlgorithmArgon2id, errors.New("Invalid algorithm in configuration. It should be `argon2id` or `sha512`")
}
}
// ParseHash extracts all characteristics of a hash given its string representation.
func ParseHash(hash string) (passwordHash *PasswordHash, err error) {
parts := strings.Split(hash, "$")
// This error can be ignored as it's always nil.
c, parameters, salt, key, _ := crypt.DecodeSettings(hash)
code := CryptAlgo(c)
h := &PasswordHash{}
h.Salt = salt
h.Key = key
if h.Key != parts[len(parts)-1] {
return nil, fmt.Errorf("Hash key is not the last parameter, the hash is likely malformed (%s)", hash)
}
if h.Key == "" {
return nil, fmt.Errorf("Hash key contains no characters or the field length is invalid (%s)", hash)
}
switch code {
case HashingAlgorithmSHA512:
h.Iterations = parameters.GetInt("rounds", HashingDefaultSHA512Iterations)
h.Algorithm = HashingAlgorithmSHA512
if parameters["rounds"] != "" && parameters["rounds"] != strconv.Itoa(h.Iterations) {
return nil, fmt.Errorf("SHA512 iterations is not numeric (%s)", parameters["rounds"])
}
case HashingAlgorithmArgon2id:
_, err = crypt.Base64Encoding.DecodeString(h.Salt)
if err != nil {
return nil, errors.New("Salt contains invalid base64 characters")
}
version := parameters.GetInt("v", 0)
if version < 19 {
if version == 0 {
return nil, fmt.Errorf("Argon2id version parameter not found (%s)", hash)
}
return nil, fmt.Errorf("Argon2id versions less than v19 are not supported (hash is version %d)", version)
} else if version > 19 {
return nil, fmt.Errorf("Argon2id versions greater than v19 are not supported (hash is version %d)", version)
}
h.Algorithm = HashingAlgorithmArgon2id
h.Memory = parameters.GetInt("m", HashingDefaultArgon2idMemory)
h.Iterations = parameters.GetInt("t", HashingDefaultArgon2idTime)
h.Parallelism = parameters.GetInt("p", HashingDefaultArgon2idParallelism)
h.KeyLength = parameters.GetInt("k", HashingDefaultArgon2idKeyLength)
decodedKey, err := crypt.Base64Encoding.DecodeString(h.Key)
if err != nil {
return nil, errors.New("Hash key contains invalid base64 characters")
}
if len(decodedKey) != h.KeyLength {
return nil, fmt.Errorf("Argon2id key length parameter (%d) does not match the actual key length (%d)", h.KeyLength, len(decodedKey))
}
default:
return nil, fmt.Errorf("Authelia only supports salted SHA512 hashing ($6$) and salted argon2id ($argon2id$), not $%s$", code)
}
return h, nil
}
// HashPassword generate a salt and hash the password with the salt and a constant number of rounds.
func HashPassword(password, salt string, algorithm CryptAlgo, iterations, memory, parallelism, keyLength, saltLength int) (hash string, err error) {
var settings string
if algorithm != HashingAlgorithmArgon2id && algorithm != HashingAlgorithmSHA512 {
return "", fmt.Errorf("Hashing algorithm input of '%s' is invalid, only values of %s and %s are supported", algorithm, HashingAlgorithmArgon2id, HashingAlgorithmSHA512)
}
if algorithm == HashingAlgorithmArgon2id {
err := validateArgon2idSettings(memory, parallelism, iterations, keyLength)
if err != nil {
return "", err
}
}
if algorithm != HashingAlgorithmSHA512 {
err = validateSalt(salt, saltLength)
if err != nil {
return "", err
}
}
if salt == "" {
salt = crypt.Base64Encoding.EncodeToString(utils.RandomBytes(saltLength, HashingPossibleSaltCharacters, true))
}
settings = getCryptSettings(salt, algorithm, iterations, memory, parallelism, keyLength)
// This error can be ignored because we check for it before a user gets here.
hash, _ = crypt.Crypt(password, settings)
return hash, nil
}
// CheckPassword check a password against a hash.
func CheckPassword(password, hash string) (ok bool, err error) {
expectedHash, err := ParseHash(hash)
if err != nil {
return false, err
}
passwordHashString, err := HashPassword(password, expectedHash.Salt, expectedHash.Algorithm, expectedHash.Iterations, expectedHash.Memory, expectedHash.Parallelism, expectedHash.KeyLength, len(expectedHash.Salt))
if err != nil {
return false, err
}
passwordHash, err := ParseHash(passwordHashString)
if err != nil {
return false, err
}
return subtle.ConstantTimeCompare([]byte(passwordHash.Key), []byte(expectedHash.Key)) == 1, nil
}
func getCryptSettings(salt string, algorithm CryptAlgo, iterations, memory, parallelism, keyLength int) (settings string) {
switch algorithm {
case HashingAlgorithmArgon2id:
settings, _ = crypt.Argon2idSettings(memory, iterations, parallelism, keyLength, salt)
case HashingAlgorithmSHA512:
settings = fmt.Sprintf("$6$rounds=%d$%s", iterations, salt)
default:
panic("invalid password hashing algorithm provided")
}
return settings
}
// validateSalt checks the salt input and settings are valid and returns it and a nil error if they are, otherwise returns an error.
func validateSalt(salt string, saltLength int) error {
if salt == "" {
if saltLength < 8 {
return fmt.Errorf("Salt length input of %d is invalid, it must be 8 or higher", saltLength)
}
return nil
}
decodedSalt, err := crypt.Base64Encoding.DecodeString(salt)
if err != nil {
return fmt.Errorf("Salt input of %s is invalid, only base64 strings are valid for input", salt)
}
if len(decodedSalt) < 8 {
return fmt.Errorf("Salt input of %s is invalid (%d characters), it must be 8 or more characters", decodedSalt, len(decodedSalt))
}
return nil
}
// validateArgon2idSettings checks the argon2id settings are valid.
func validateArgon2idSettings(memory, parallelism, iterations, keyLength int) error {
// Caution: Increasing any of the values in the below block has a high chance in old passwords that cannot be verified.
if memory < 8 {
return fmt.Errorf("Memory (argon2id) input of %d is invalid, it must be 8 or higher", memory)
}
if parallelism < 1 {
return fmt.Errorf("Parallelism (argon2id) input of %d is invalid, it must be 1 or higher", parallelism)
}
if memory < parallelism*8 {
return fmt.Errorf("Memory (argon2id) input of %d is invalid with a parallelism input of %d, it must be %d (parallelism * 8) or higher", memory, parallelism, parallelism*8)
}
if keyLength < 16 {
return fmt.Errorf("Key length (argon2id) input of %d is invalid, it must be 16 or higher", keyLength)
}
if iterations < 1 {
return fmt.Errorf("Iterations (argon2id) input of %d is invalid, it must be 1 or more", iterations)
}
// Caution: Increasing any of the values in the above block has a high chance in old passwords that cannot be verified.
return nil
}

View File

@ -1,322 +0,0 @@
package authentication
import (
"fmt"
"testing"
"github.com/simia-tech/crypt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
func TestShouldHashSHA512Password(t *testing.T) {
hash, err := HashPassword("password", "aFr56HjK3DrB8t3S", HashingAlgorithmSHA512, 50000, 0, 0, 0, 16)
assert.NoError(t, err)
code, parameters, salt, hash, _ := crypt.DecodeSettings(hash)
assert.Equal(t, "6", code)
assert.Equal(t, "aFr56HjK3DrB8t3S", salt)
assert.Equal(t, "zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1", hash)
assert.Equal(t, schema.DefaultPasswordSHA512Configuration.Iterations, parameters.GetInt("rounds", HashingDefaultSHA512Iterations))
}
func TestShouldHashArgon2idPassword(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, schema.DefaultCIPasswordConfiguration.Memory*1024,
schema.DefaultCIPasswordConfiguration.Parallelism, schema.DefaultCIPasswordConfiguration.KeyLength,
schema.DefaultCIPasswordConfiguration.SaltLength)
assert.NoError(t, err)
code, parameters, salt, key, err := crypt.DecodeSettings(hash)
assert.NoError(t, err)
assert.Equal(t, argon2id, code)
assert.Equal(t, "BpLnfgDsc2WD8F2q", salt)
assert.Equal(t, "kYempka60N8ETZ+EedP+Fn3z83mEPMl08RQEXTwY6u0", key)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Iterations, parameters.GetInt("t", HashingDefaultArgon2idTime))
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Memory*1024, parameters.GetInt("m", HashingDefaultArgon2idMemory))
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Parallelism, parameters.GetInt("p", HashingDefaultArgon2idParallelism))
assert.Equal(t, schema.DefaultCIPasswordConfiguration.KeyLength, parameters.GetInt("k", HashingDefaultArgon2idKeyLength))
}
func TestShouldValidateArgon2idHashWithTEqualOne(t *testing.T) {
hash := "$argon2id$v=19$m=1024,t=1,p=1,k=16$c2FsdG9uY2U$Sk4UjzxXdCrBcyyMYiPEsQ"
valid, err := CheckPassword("apple", hash)
assert.True(t, valid)
assert.NoError(t, err)
}
// This checks the method of hashing (for argon2id) supports all the characters we allow in Authelia's hash function.
func TestArgon2idHashSaltValidValues(t *testing.T) {
var err error
var hash string
datas := utils.SliceString(HashingPossibleSaltCharacters, 16)
for _, salt := range datas {
hash, err = HashPassword("password", salt, HashingAlgorithmArgon2id, 1, 8, 1, 32, 16)
assert.NoError(t, err)
assert.Equal(t, fmt.Sprintf("$argon2id$v=19$m=8,t=1,p=1$%s$", salt), hash[0:44])
}
}
// This checks the method of hashing (for sha512) supports all the characters we allow in Authelia's hash function.
func TestSHA512HashSaltValidValues(t *testing.T) {
var err error
var hash string
datas := utils.SliceString(HashingPossibleSaltCharacters, 16)
for _, salt := range datas {
hash, err = HashPassword("password", salt, HashingAlgorithmSHA512, 1000, 0, 0, 0, 16)
assert.NoError(t, err)
assert.Equal(t, fmt.Sprintf("$6$rounds=1000$%s$", salt), hash[0:32])
}
}
func TestShouldNotHashPasswordWithNonExistentAlgorithm(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", "bogus",
schema.DefaultCIPasswordConfiguration.Iterations, schema.DefaultCIPasswordConfiguration.Memory*1024,
schema.DefaultCIPasswordConfiguration.Parallelism, schema.DefaultCIPasswordConfiguration.KeyLength,
schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Hashing algorithm input of 'bogus' is invalid, only values of argon2id and 6 are supported")
}
func TestShouldNotHashArgon2idPasswordDueToMemoryParallelismMismatch(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, 8, 2,
schema.DefaultCIPasswordConfiguration.KeyLength, schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Memory (argon2id) input of 8 is invalid with a parallelism input of 2, it must be 16 (parallelism * 8) or higher")
}
func TestShouldNotHashArgon2idPasswordDueToMemoryLessThanEight(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, 1, schema.DefaultCIPasswordConfiguration.Parallelism,
schema.DefaultCIPasswordConfiguration.KeyLength, schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Memory (argon2id) input of 1 is invalid, it must be 8 or higher")
}
func TestShouldNotHashArgon2idPasswordDueToKeyLengthLessThanSixteen(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, schema.DefaultCIPasswordConfiguration.Memory*1024,
schema.DefaultCIPasswordConfiguration.Parallelism, 5, schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Key length (argon2id) input of 5 is invalid, it must be 16 or higher")
}
func TestShouldNotHashArgon2idPasswordDueParallelismLessThanOne(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, schema.DefaultCIPasswordConfiguration.Memory*1024, -1,
schema.DefaultCIPasswordConfiguration.KeyLength, schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Parallelism (argon2id) input of -1 is invalid, it must be 1 or higher")
}
func TestShouldNotHashArgon2idPasswordDueIterationsLessThanOne(t *testing.T) {
hash, err := HashPassword("password", "BpLnfgDsc2WD8F2q", HashingAlgorithmArgon2id,
0, schema.DefaultCIPasswordConfiguration.Memory*1024, schema.DefaultCIPasswordConfiguration.Parallelism,
schema.DefaultCIPasswordConfiguration.KeyLength, schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Iterations (argon2id) input of 0 is invalid, it must be 1 or more")
}
func TestShouldNotHashPasswordDueToSaltLength(t *testing.T) {
hash, err := HashPassword("password", "", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, schema.DefaultCIPasswordConfiguration.Memory*1024,
schema.DefaultCIPasswordConfiguration.Parallelism, schema.DefaultCIPasswordConfiguration.KeyLength, 0)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt length input of 0 is invalid, it must be 8 or higher")
}
func TestShouldNotHashPasswordDueToSaltCharLengthTooShort(t *testing.T) {
// The salt 'YQ' is the base64 value for 'a' which is why the length is 1.
hash, err := HashPassword("password", "YQ", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, schema.DefaultCIPasswordConfiguration.Memory*1024,
schema.DefaultCIPasswordConfiguration.Parallelism, schema.DefaultCIPasswordConfiguration.KeyLength,
schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt input of a is invalid (1 characters), it must be 8 or more characters")
}
func TestShouldNotHashPasswordWithNonBase64CharsInSalt(t *testing.T) {
hash, err := HashPassword("password", "abc&123", HashingAlgorithmArgon2id,
schema.DefaultCIPasswordConfiguration.Iterations, schema.DefaultCIPasswordConfiguration.Memory*1024,
schema.DefaultCIPasswordConfiguration.Parallelism, schema.DefaultCIPasswordConfiguration.KeyLength,
schema.DefaultCIPasswordConfiguration.SaltLength)
assert.Equal(t, "", hash)
assert.EqualError(t, err, "Salt input of abc&123 is invalid, only base64 strings are valid for input")
}
func TestShouldNotParseHashWithNoneBase64CharsInKey(t *testing.T) {
passwordHash, err := ParseHash("$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$^^vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Hash key contains invalid base64 characters")
assert.Nil(t, passwordHash)
}
func TestShouldNotParseHashWithNoneBase64CharsInSalt(t *testing.T) {
passwordHash, err := ParseHash("$argon2id$v=19$m=65536$^^wTFoFjITudo57a$Z4NH/EKkdv6PJ01Ye1twJ61fsmRJujZZn1IXdUOyrJY")
assert.EqualError(t, err, "Salt contains invalid base64 characters")
assert.Nil(t, passwordHash)
}
func TestShouldNotParseWithMalformedHash(t *testing.T) {
hashExtraField := "$argon2id$v=19$m=65536,t=3,p=2$abc$BpLnfgDsc2WD8F2q$^^vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
hashMissingSaltAndParams := "$argon2id$v=1$2t9X8nNCN2n3/kFYJ3xWNBg5k/rO782Qr7JJoJIK7G4"
hashMissingSalt := "$argon2id$v=1$m=65536,t=3,p=2$2t9X8nNCN2n3/kFYJ3xWNBg5k/rO782Qr7JJoJIK7G4"
passwordHash, err := ParseHash(hashExtraField)
assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s)", hashExtraField))
assert.Nil(t, passwordHash)
passwordHash, err = ParseHash(hashMissingSaltAndParams)
assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s)", hashMissingSaltAndParams))
assert.Nil(t, passwordHash)
passwordHash, err = ParseHash(hashMissingSalt)
assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s)", hashMissingSalt))
assert.Nil(t, passwordHash)
}
func TestShouldNotParseHashWithEmptyKey(t *testing.T) {
hash := "$argon2id$v=19$m=65536$fvwTFoFjITudo57a$"
passwordHash, err := ParseHash(hash)
assert.EqualError(t, err, fmt.Sprintf("Hash key contains no characters or the field length is invalid (%s)", hash))
assert.Nil(t, passwordHash)
}
func TestShouldNotParseArgon2idHashWithEmptyVersion(t *testing.T) {
hash := "$argon2id$m=65536$fvwTFoFjITudo57a$Z4NH/EKkdv6PJ01Ye1twJ61fsmRJujZZn1IXdUOyrJY"
passwordHash, err := ParseHash(hash)
assert.EqualError(t, err, fmt.Sprintf("Argon2id version parameter not found (%s)", hash))
assert.Nil(t, passwordHash)
}
func TestShouldNotParseArgon2idHashWithWrongKeyLength(t *testing.T) {
hash := "$argon2id$v=19$m=65536,k=50$fvwTFoFjITudo57a$Z4NH/EKkdv6PJ01Ye1twJ61fsmRJujZZn1IXdUOyrJY"
passwordHash, err := ParseHash(hash)
assert.EqualError(t, err, "Argon2id key length parameter (50) does not match the actual key length (32)")
assert.Nil(t, passwordHash)
}
func TestShouldParseArgon2idHash(t *testing.T) {
passwordHash, err := ParseHash("$argon2id$v=19$m=65536,t=3,p=4$NEwwcVNuQWlQMFpkMndxdg$LlHjiLxPB94pdmOiNwr7Bgy+uy3huSv6y9phCQ+mLls")
assert.NoError(t, err)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Iterations, passwordHash.Iterations)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Parallelism, passwordHash.Parallelism)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.KeyLength, passwordHash.KeyLength)
assert.Equal(t, schema.DefaultCIPasswordConfiguration.Memory*1024, passwordHash.Memory)
}
func TestShouldCheckSHA512Password(t *testing.T) {
ok, err := CheckPassword("password", "$6$rounds=50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.NoError(t, err)
assert.True(t, ok)
}
func TestShouldCheckArgon2idPassword(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.NoError(t, err)
assert.True(t, ok)
}
func TestCannotParseSHA512Hash(t *testing.T) {
ok, err := CheckPassword("password", "$6$roSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.EqualError(t, err, "Hash key is not the last parameter, the hash is likely malformed ($6$roSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1)")
assert.False(t, ok)
}
func TestCannotParseArgon2idHash(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Hash key is not the last parameter, the hash is likely malformed ($argon2id$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM)")
assert.False(t, ok)
}
func TestOnlySupportSHA512AndArgon2id(t *testing.T) {
ok, err := CheckPassword("password", "$8$rounds=50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.EqualError(t, err, "Authelia only supports salted SHA512 hashing ($6$) and salted argon2id ($argon2id$), not $8$")
assert.False(t, ok)
}
func TestCannotFindNumberOfRounds(t *testing.T) {
hash := "$6$rounds50000$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1"
ok, err := CheckPassword("password", hash)
assert.EqualError(t, err, fmt.Sprintf("Hash key is not the last parameter, the hash is likely malformed (%s)", hash))
assert.False(t, ok)
}
func TestCannotMatchArgon2idParamPattern(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Hash key is not the last parameter, the hash is likely malformed ($argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM)")
assert.False(t, ok)
}
func TestArgon2idVersionLessThanSupported(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=18$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Argon2id versions less than v19 are not supported (hash is version 18)")
assert.False(t, ok)
}
func TestArgon2idVersionGreaterThanSupported(t *testing.T) {
ok, err := CheckPassword("password", "$argon2id$v=20$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM")
assert.EqualError(t, err, "Argon2id versions greater than v19 are not supported (hash is version 20)")
assert.False(t, ok)
}
func TestNumberOfRoundsNotInt(t *testing.T) {
ok, err := CheckPassword("password", "$6$rounds=abc$aFr56HjK3DrB8t3S$zhPQiS85cgBlNhUKKE6n/AHMlpqrvYSnSL3fEVkK0yHFQ.oFFAd8D4OhPAy18K5U61Z2eBhxQXExGU/eknXlY1")
assert.EqualError(t, err, "SHA512 iterations is not numeric (abc)")
assert.False(t, ok)
}
func TestShouldCheckPasswordArgon2idHashedWithAuthelia(t *testing.T) {
password := testPassword
hash, err := HashPassword(password, "", HashingAlgorithmArgon2id, schema.DefaultCIPasswordConfiguration.Iterations,
schema.DefaultCIPasswordConfiguration.Memory*1024, schema.DefaultCIPasswordConfiguration.Parallelism,
schema.DefaultCIPasswordConfiguration.KeyLength, schema.DefaultCIPasswordConfiguration.SaltLength)
assert.NoError(t, err)
equal, err := CheckPassword(password, hash)
require.NoError(t, err)
assert.True(t, equal)
}
func TestShouldCheckPasswordSHA512HashedWithAuthelia(t *testing.T) {
password := testPassword
hash, err := HashPassword(password, "", HashingAlgorithmSHA512, schema.DefaultPasswordSHA512Configuration.Iterations,
0, 0, 0, schema.DefaultPasswordSHA512Configuration.SaltLength)
assert.NoError(t, err)
equal, err := CheckPassword(password, hash)
require.NoError(t, err)
assert.True(t, equal)
}

View File

@ -56,7 +56,7 @@ func newAccessControlCheckCommand() (cmd *cobra.Command) {
}
func accessControlCheckRunE(cmd *cobra.Command, _ []string) (err error) {
configs, err := cmd.Flags().GetStringSlice("config")
configs, err := cmd.Flags().GetStringSlice(cmdFlagNameConfig)
if err != nil {
return err
}

View File

@ -15,9 +15,9 @@ import (
// cmdWithConfigFlags is used for commands which require access to the configuration to add the flag to the command.
func cmdWithConfigFlags(cmd *cobra.Command, persistent bool, configs []string) {
if persistent {
cmd.PersistentFlags().StringSliceP("config", "c", configs, "configuration files to load")
cmd.PersistentFlags().StringSliceP(cmdFlagNameConfig, "c", configs, "configuration files to load")
} else {
cmd.Flags().StringSliceP("config", "c", configs, "configuration files to load")
cmd.Flags().StringSliceP(cmdFlagNameConfig, "c", configs, "configuration files to load")
}
}
@ -33,7 +33,7 @@ func newCmdWithConfigPreRun(ensureConfigExists, validateKeys, validateConfigurat
logger = logging.Logger()
if configs, err = cmd.Flags().GetStringSlice("config"); err != nil {
if configs, err = cmd.Flags().GetStringSlice(cmdFlagNameConfig); err != nil {
logger.Fatalf("Error reading flags: %v", err)
}

View File

@ -310,6 +310,57 @@ This subcommand allows preforming cryptographic certificate, key pair, etc tasks
cmdAutheliaCryptoExample = `authelia crypto --help`
cmdAutheliaCryptoRandShort = "Generate a cryptographically secure random string"
cmdAutheliaCryptoRandLong = `Generate a cryptographically secure random string.
This subcommand allows generating cryptographically secure random strings for use for encryption keys, HMAC keys, etc.`
cmdAutheliaCryptoRandExample = `authelia crypto rand --help
authelia crypto rand --length 80
authelia crypto rand -n 80
authelia crypto rand --charset alphanumeric
authelia crypto rand --charset alphabetic
authelia crypto rand --charset ascii
authelia crypto rand --charset numeric
authelia crypto rand --charset numeric-hex
authelia crypto rand --characters 0123456789ABCDEF`
cmdAutheliaCryptoHashShort = "Perform cryptographic hash operations"
cmdAutheliaCryptoHashLong = `Perform cryptographic hash operations.
This subcommand allows preforming hashing cryptographic tasks.`
cmdAutheliaCryptoHashExample = `authelia crypto hash --help`
cmdAutheliaCryptoHashValidateShort = "Perform cryptographic hash validations"
cmdAutheliaCryptoHashValidateLong = `Perform cryptographic hash validations.
This subcommand allows preforming cryptographic hash validations. i.e. checking hash digests against a password.`
cmdAutheliaCryptoHashValidateExample = `authelia crypto hash validate --help
authelia crypto hash validate '$5$rounds=500000$WFjMpdCQxIkbNl0k$M0qZaZoK8Gwdh8Cw5diHgGfe5pE0iJvxcVG3.CVnQe.' -- 'p@ssw0rd'`
cmdAutheliaCryptoHashGenerateShort = "Generate cryptographic hash digests"
cmdAutheliaCryptoHashGenerateLong = `Generate cryptographic hash digests.
This subcommand allows generating cryptographic hash digests.
See the help for the subcommands if you want to override the configuration or defaults.`
cmdAutheliaCryptoHashGenerateExample = `authelia crypto hash generate --help`
fmtCmdAutheliaCryptoHashGenerateSubShort = "Generate cryptographic %s hash digests"
fmtCmdAutheliaCryptoHashGenerateSubLong = `Generate cryptographic %s hash digests.
This subcommand allows generating cryptographic %s hash digests.`
fmtCmdAutheliaCryptoHashGenerateSubExample = `authelia crypto hash generate %s --help`
cmdAutheliaCryptoCertificateShort = "Perform certificate cryptographic operations"
cmdAutheliaCryptoCertificateLong = `Perform certificate cryptographic operations.
@ -324,11 +375,7 @@ This subcommand allows preforming certificate cryptographic tasks.`
This subcommand allows preforming %s certificate cryptographic tasks.`
cmdAutheliaCryptoCertificateRSAExample = `authelia crypto certificate rsa --help`
cmdAutheliaCryptoCertificateECDSAExample = `authelia crypto certificate ecdsa --help`
cmdAutheliaCryptoCertificateEd25519Example = `authelia crypto certificate ed25519 --help`
fmtCmdAutheliaCryptoCertificateSubExample = `authelia crypto certificate %s --help`
fmtCmdAutheliaCryptoCertificateGenerateRequestShort = "Generate an %s private key and %s"
@ -444,11 +491,43 @@ const (
cmdFlagNamePKCS8 = "pkcs8"
cmdFlagNameBits = "bits"
cmdFlagNameCurve = "curve"
cmdFlagNamePassword = "password"
cmdFlagNameRandom = "random"
cmdFlagNameRandomLength = "random.length"
cmdFlagNameNoConfirm = "no-confirm"
cmdFlagNameVariant = "variant"
cmdFlagNameCost = "cost"
cmdFlagNameIterations = "iterations"
cmdFlagNameParallelism = "parallelism"
cmdFlagNameBlockSize = "block-size"
cmdFlagNameMemory = "memory"
cmdFlagNameKeySize = "key-size"
cmdFlagNameSaltSize = "salt-size"
cmdFlagNameProfile = "profile"
cmdFlagNameSHA512 = "sha512"
cmdFlagNameConfig = "config"
cmdFlagNameCharSet = "charset"
cmdFlagNameCharacters = "characters"
cmdFlagNameLength = "length"
)
const (
cmdUseHashPassword = "hash-password [flags] -- [password]"
cmdUseHash = "hash"
cmdUseHashArgon2 = "argon2"
cmdUseHashSHA2Crypt = "sha2crypt"
cmdUseHashPBKDF2 = "pbkdf2"
cmdUseHashBCrypt = "bcrypt"
cmdUseHashSCrypt = "scrypt"
cmdUseCrypto = "crypto"
cmdUseRand = "rand"
cmdUseCertificate = "certificate"
cmdUseGenerate = "generate"
cmdUseValidate = "validate"
cmdUseFmtValidate = "%s [flags] -- <digest>"
cmdUseRequest = "request"
cmdUsePair = "pair"
cmdUseRSA = "rsa"
@ -459,6 +538,8 @@ const (
const (
cryptoCertPubCertOut = "certificate"
cryptoCertCSROut = "certificate signing request"
prefixFilePassword = "authentication_backend.file.password"
)
var (

View File

@ -17,7 +17,7 @@ import (
func newCryptoCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "crypto",
Use: cmdUseCrypto,
Short: cmdAutheliaCryptoShort,
Long: cmdAutheliaCryptoLong,
Example: cmdAutheliaCryptoExample,
@ -27,13 +27,84 @@ func newCryptoCmd() (cmd *cobra.Command) {
}
cmd.AddCommand(
newCryptoRandCmd(),
newCryptoCertificateCmd(),
newCryptoHashCmd(),
newCryptoPairCmd(),
)
return cmd
}
func newCryptoRandCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseRand,
Short: cmdAutheliaCryptoRandShort,
Long: cmdAutheliaCryptoRandLong,
Example: cmdAutheliaCryptoRandExample,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) (err error) {
useCharSet, useCharacters := cmd.Flags().Changed(cmdFlagNameCharSet), cmd.Flags().Changed(cmdFlagNameCharacters)
if useCharSet && useCharacters {
return fmt.Errorf("flags '--%s' and '--%s' are mutually exclusive, only one may be specified", cmdFlagNameCharSet, cmdFlagNameCharacters)
}
var (
charset string
n int
)
if n, err = cmd.Flags().GetInt(cmdFlagNameLength); err != nil {
return err
}
if n < 1 {
return fmt.Errorf("length must be at least 1")
}
switch {
case useCharSet, !useCharSet && !useCharacters:
var c string
if c, err = cmd.Flags().GetString(cmdFlagNameCharSet); err != nil {
return err
}
switch c {
case "ascii":
charset = utils.CharSetASCII
case "alphanumeric":
charset = utils.CharSetAlphaNumeric
case "alphabetic":
charset = utils.CharSetAlphabetic
case "numeric-hex":
charset = utils.CharSetNumericHex
case "numeric":
charset = utils.CharSetNumeric
default:
return fmt.Errorf("invalid charset '%s', must be one of 'ascii', 'alphanumeric', 'alphabetic', 'numeric', or 'numeric-hex'", c)
}
case useCharacters:
if charset, err = cmd.Flags().GetString(cmdFlagNameCharacters); err != nil {
return err
}
}
fmt.Printf("Random Value: %s\n", utils.RandomString(n, charset, true))
return nil
},
DisableAutoGenTag: true,
}
cmd.Flags().StringP(cmdFlagNameCharSet, "c", "alphanumeric", "Sets the charset for the output, options are 'ascii', 'alphanumeric', 'alphabetic', 'numeric', and 'numeric-hex'")
cmd.Flags().String(cmdFlagNameCharacters, "", "Sets the explicit characters for the random output")
cmd.Flags().IntP(cmdFlagNameLength, "n", 80, "Sets the length of the random output")
return cmd
}
func newCryptoCertificateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseCertificate,
@ -55,26 +126,13 @@ func newCryptoCertificateCmd() (cmd *cobra.Command) {
}
func newCryptoCertificateSubCmd(use string) (cmd *cobra.Command) {
var (
example, useFmt string
)
useFmt = fmtCryptoUse(use)
switch use {
case cmdUseRSA:
example = cmdAutheliaCryptoCertificateRSAExample
case cmdUseECDSA:
example = cmdAutheliaCryptoCertificateECDSAExample
case cmdUseEd25519:
example = cmdAutheliaCryptoCertificateEd25519Example
}
useFmt := fmtCryptoCertificateUse(use)
cmd = &cobra.Command{
Use: use,
Short: fmt.Sprintf(fmtCmdAutheliaCryptoCertificateSubShort, useFmt),
Long: fmt.Sprintf(fmtCmdAutheliaCryptoCertificateSubLong, useFmt, useFmt),
Example: example,
Example: fmt.Sprintf(fmtCmdAutheliaCryptoCertificateSubExample, use),
Args: cobra.NoArgs,
DisableAutoGenTag: true,
@ -98,7 +156,7 @@ func newCryptoCertificateRequestCmd(algorithm string) (cmd *cobra.Command) {
cmdFlagsCryptoCertificateCommon(cmd)
cmdFlagsCryptoCertificateRequest(cmd)
algorithmFmt := fmtCryptoUse(algorithm)
algorithmFmt := fmtCryptoCertificateUse(algorithm)
cmd.Short = fmt.Sprintf(fmtCmdAutheliaCryptoCertificateGenerateRequestShort, algorithmFmt, cryptoCertCSROut)
cmd.Long = fmt.Sprintf(fmtCmdAutheliaCryptoCertificateGenerateRequestLong, algorithmFmt, cryptoCertCSROut, algorithmFmt, cryptoCertCSROut)
@ -146,7 +204,7 @@ func newCryptoPairSubCmd(use string) (cmd *cobra.Command) {
example, useFmt string
)
useFmt = fmtCryptoUse(use)
useFmt = fmtCryptoCertificateUse(use)
switch use {
case cmdUseRSA:
@ -184,7 +242,7 @@ func newCryptoGenerateCmd(category, algorithm string) (cmd *cobra.Command) {
cmdFlagsCryptoPrivateKey(cmd)
algorithmFmt := fmtCryptoUse(algorithm)
algorithmFmt := fmtCryptoCertificateUse(algorithm)
switch category {
case cmdUseCertificate:

View File

@ -0,0 +1,527 @@
package commands
import (
"fmt"
"strings"
"syscall"
"github.com/go-crypt/crypt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/term"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
"github.com/authelia/authelia/v4/internal/utils"
)
func newHashPasswordCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseHashPassword,
Short: cmdAutheliaHashPasswordShort,
Long: cmdAutheliaHashPasswordLong,
Example: cmdAutheliaHashPasswordExample,
Args: cobra.MaximumNArgs(1),
RunE: cmdHashPasswordRunE,
DisableAutoGenTag: true,
}
cmdFlagConfig(cmd)
cmd.Flags().BoolP(cmdFlagNameSHA512, "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordConfig.SHA2Crypt.Iterations))
cmd.Flags().IntP(cmdFlagNameIterations, "i", schema.DefaultPasswordConfig.Argon2.Iterations, "set the number of hashing iterations")
cmd.Flags().IntP(cmdFlagNameMemory, "m", schema.DefaultPasswordConfig.Argon2.Memory, "[argon2id] set the amount of memory param (in MB)")
cmd.Flags().IntP(cmdFlagNameParallelism, "p", schema.DefaultPasswordConfig.Argon2.Parallelism, "[argon2id] set the parallelism param")
cmd.Flags().IntP("key-length", "k", schema.DefaultPasswordConfig.Argon2.KeyLength, "[argon2id] set the key length param")
cmd.Flags().IntP("salt-length", "l", schema.DefaultPasswordConfig.Argon2.SaltLength, "set the auto-generated salt length")
cmd.Flags().Bool(cmdFlagNameNoConfirm, false, "skip the password confirmation prompt")
return cmd
}
func cmdHashPasswordRunE(cmd *cobra.Command, args []string) (err error) {
var (
flagsMap map[string]string
sha512 bool
)
if sha512, err = cmd.Flags().GetBool(cmdFlagNameSHA512); err != nil {
return err
}
switch {
case sha512:
flagsMap = map[string]string{
cmdFlagNameIterations: prefixFilePassword + ".sha2crypt.iterations",
"salt-length": prefixFilePassword + ".sha2crypt.salt_length",
}
default:
flagsMap = map[string]string{
cmdFlagNameIterations: prefixFilePassword + ".argon2.iterations",
"key-length": prefixFilePassword + ".argon2.key_length",
"salt-length": prefixFilePassword + ".argon2.salt_length",
cmdFlagNameParallelism: prefixFilePassword + ".argon2.parallelism",
cmdFlagNameMemory: prefixFilePassword + ".argon2.memory",
}
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func newCryptoHashCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseHash,
Short: cmdAutheliaCryptoHashShort,
Long: cmdAutheliaCryptoHashLong,
Example: cmdAutheliaCryptoHashExample,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
}
cmd.AddCommand(
newCryptoHashValidateCmd(),
newCryptoHashGenerateCmd(),
)
return cmd
}
func newCryptoHashGenerateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: cmdUseGenerate,
Short: cmdAutheliaCryptoHashGenerateShort,
Long: cmdAutheliaCryptoHashGenerateLong,
Example: cmdAutheliaCryptoHashGenerateExample,
RunE: func(cmd *cobra.Command, args []string) error {
return cmdCryptoHashGenerateFinish(cmd, args, map[string]string{})
},
DisableAutoGenTag: true,
}
cmdFlagConfig(cmd)
cmdFlagPassword(cmd, true)
for _, use := range []string{cmdUseHashArgon2, cmdUseHashSHA2Crypt, cmdUseHashPBKDF2, cmdUseHashBCrypt, cmdUseHashSCrypt} {
cmd.AddCommand(newCryptoHashGenerateSubCmd(use))
}
return cmd
}
func newCryptoHashGenerateSubCmd(use string) (cmd *cobra.Command) {
useFmt := fmtCryptoHashUse(use)
cmd = &cobra.Command{
Use: use,
Short: fmt.Sprintf(fmtCmdAutheliaCryptoHashGenerateSubShort, useFmt),
Long: fmt.Sprintf(fmtCmdAutheliaCryptoHashGenerateSubLong, useFmt, useFmt),
Example: fmt.Sprintf(fmtCmdAutheliaCryptoHashGenerateSubExample, use),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
DisableAutoGenTag: true,
}
cmdFlagConfig(cmd)
cmdFlagPassword(cmd, true)
cmdFlagRandomPassword(cmd)
switch use {
case cmdUseHashArgon2:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.Argon2.Iterations)
cmdFlagParallelism(cmd, schema.DefaultPasswordConfig.Argon2.Parallelism)
cmdFlagKeySize(cmd, schema.DefaultPasswordConfig.Argon2.KeyLength)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.Argon2.SaltLength)
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.Argon2.Variant, "variant, options are 'argon2id', 'argon2i', and 'argon2d'")
cmd.Flags().IntP(cmdFlagNameMemory, "m", schema.DefaultPasswordConfig.Argon2.Memory, "memory in kibibytes")
cmd.Flags().String(cmdFlagNameProfile, "", "profile to use, options are low-memory and recommended")
cmd.RunE = cryptoHashGenerateArgon2RunE
case cmdUseHashSHA2Crypt:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.SHA2Crypt.Iterations)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.SHA2Crypt.SaltLength)
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.SHA2Crypt.Variant, "variant, options are sha256 and sha512")
cmd.RunE = cryptoHashGenerateSHA2CryptRunE
case cmdUseHashPBKDF2:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.PBKDF2.Iterations)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.PBKDF2.SaltLength)
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.PBKDF2.Variant, "variant, options are 'sha1', 'sha224', 'sha256', 'sha384', and 'sha512'")
cmd.RunE = cryptoHashGeneratePBKDF2RunE
case cmdUseHashBCrypt:
cmd.Flags().StringP(cmdFlagNameVariant, "v", schema.DefaultPasswordConfig.BCrypt.Variant, "variant, options are 'standard' and 'sha256'")
cmd.Flags().IntP(cmdFlagNameCost, "i", schema.DefaultPasswordConfig.BCrypt.Cost, "hashing cost")
cmd.RunE = cryptoHashGenerateBCryptRunE
case cmdUseHashSCrypt:
cmdFlagIterations(cmd, schema.DefaultPasswordConfig.SCrypt.Iterations)
cmdFlagKeySize(cmd, schema.DefaultPasswordConfig.SCrypt.KeyLength)
cmdFlagSaltSize(cmd, schema.DefaultPasswordConfig.SCrypt.SaltLength)
cmdFlagParallelism(cmd, schema.DefaultPasswordConfig.SCrypt.Parallelism)
cmd.Flags().IntP(cmdFlagNameBlockSize, "r", schema.DefaultPasswordConfig.SCrypt.BlockSize, "block size")
cmd.RunE = cryptoHashGenerateSCryptRunE
}
return cmd
}
func cryptoHashGenerateArgon2RunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".argon2.variant",
cmdFlagNameIterations: prefixFilePassword + ".argon2.iterations",
cmdFlagNameMemory: prefixFilePassword + ".argon2.memory",
cmdFlagNameParallelism: prefixFilePassword + ".argon2.parallelism",
cmdFlagNameKeySize: prefixFilePassword + ".argon2.key_length",
cmdFlagNameSaltSize: prefixFilePassword + ".argon2.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGenerateSHA2CryptRunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".sha2crypt.variant",
cmdFlagNameIterations: prefixFilePassword + ".sha2crypt.iterations",
cmdFlagNameSaltSize: prefixFilePassword + ".sha2crypt.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGeneratePBKDF2RunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".pbkdf2.variant",
cmdFlagNameIterations: prefixFilePassword + ".pbkdf2.iterations",
cmdFlagNameKeySize: prefixFilePassword + ".pbkdf2.key_length",
cmdFlagNameSaltSize: prefixFilePassword + ".pbkdf2.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGenerateBCryptRunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameVariant: prefixFilePassword + ".bcrypt.variant",
cmdFlagNameCost: prefixFilePassword + ".bcrypt.cost",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func cryptoHashGenerateSCryptRunE(cmd *cobra.Command, args []string) (err error) {
flagsMap := map[string]string{
cmdFlagNameIterations: prefixFilePassword + ".scrypt.iterations",
cmdFlagNameBlockSize: prefixFilePassword + ".scrypt.block_size",
cmdFlagNameParallelism: prefixFilePassword + ".scrypt.parallelism",
cmdFlagNameKeySize: prefixFilePassword + ".scrypt.key_length",
cmdFlagNameSaltSize: prefixFilePassword + ".scrypt.salt_length",
}
return cmdCryptoHashGenerateFinish(cmd, args, flagsMap)
}
func newCryptoHashValidateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: fmt.Sprintf(cmdUseFmtValidate, cmdUseValidate),
Short: cmdAutheliaCryptoHashValidateShort,
Long: cmdAutheliaCryptoHashValidateLong,
Example: cmdAutheliaCryptoHashValidateExample,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
var (
password string
valid bool
)
if password, _, err = cmdCryptoHashGetPassword(cmd, args, false, false); err != nil {
return fmt.Errorf("error occurred trying to obtain the password: %w", err)
}
if len(password) == 0 {
return fmt.Errorf("no password provided")
}
if valid, err = crypt.CheckPassword(password, args[0]); err != nil {
return fmt.Errorf("error occurred trying to validate the password against the digest: %w", err)
}
switch {
case valid:
fmt.Println("The password matches the digest.")
default:
fmt.Println("The password does not match the digest.")
}
return nil
},
DisableAutoGenTag: true,
}
cmdFlagPassword(cmd, false)
return cmd
}
func cmdCryptoHashGenerateFinish(cmd *cobra.Command, args []string, flagsMap map[string]string) (err error) {
var (
algorithm string
configs []string
c schema.Password
)
if configs, err = cmd.Flags().GetStringSlice(cmdFlagNameConfig); err != nil {
return err
}
// Skip config if the flag wasn't set and the default is non-existent.
if !cmd.Flags().Changed(cmdFlagNameConfig) {
configs = configFilterExisting(configs)
}
legacy := cmd.Use == cmdUseHashPassword
switch {
case cmd.Use == cmdUseGenerate:
break
case legacy:
if sha512, _ := cmd.Flags().GetBool(cmdFlagNameSHA512); sha512 {
algorithm = cmdUseHashSHA2Crypt
} else {
algorithm = cmdUseHashArgon2
}
default:
algorithm = cmd.Use
}
if c, err = cmdCryptoHashGetConfig(algorithm, configs, cmd.Flags(), flagsMap); err != nil {
return err
}
if legacy && algorithm == cmdUseHashArgon2 && cmd.Flags().Changed(cmdFlagNameMemory) {
c.Argon2.Memory *= 1024
}
var (
hash crypt.Hash
digest crypt.Digest
password string
random bool
)
if password, random, err = cmdCryptoHashGetPassword(cmd, args, legacy, !legacy); err != nil {
return err
}
if len(password) == 0 {
return fmt.Errorf("no password provided")
}
if hash, err = authentication.NewFileCryptoHashFromConfig(c); err != nil {
return err
}
if digest, err = hash.Hash(password); err != nil {
return err
}
if random {
fmt.Printf("Random Password: %s\n", password)
}
fmt.Printf("Digest: %s\n", digest.Encode())
return nil
}
func cmdCryptoHashGetConfig(algorithm string, configs []string, flags *pflag.FlagSet, flagsMap map[string]string) (c schema.Password, err error) {
mapDefaults := map[string]interface{}{
prefixFilePassword + ".algorithm": schema.DefaultPasswordConfig.Algorithm,
prefixFilePassword + ".argon2.variant": schema.DefaultPasswordConfig.Argon2.Variant,
prefixFilePassword + ".argon2.iterations": schema.DefaultPasswordConfig.Argon2.Iterations,
prefixFilePassword + ".argon2.memory": schema.DefaultPasswordConfig.Argon2.Memory,
prefixFilePassword + ".argon2.parallelism": schema.DefaultPasswordConfig.Argon2.Parallelism,
prefixFilePassword + ".argon2.key_length": schema.DefaultPasswordConfig.Argon2.KeyLength,
prefixFilePassword + ".argon2.salt_length": schema.DefaultPasswordConfig.Argon2.SaltLength,
prefixFilePassword + ".sha2crypt.variant": schema.DefaultPasswordConfig.SHA2Crypt.Variant,
prefixFilePassword + ".sha2crypt.iterations": schema.DefaultPasswordConfig.SHA2Crypt.Iterations,
prefixFilePassword + ".sha2crypt.salt_length": schema.DefaultPasswordConfig.SHA2Crypt.SaltLength,
prefixFilePassword + ".pbkdf2.variant": schema.DefaultPasswordConfig.PBKDF2.Variant,
prefixFilePassword + ".pbkdf2.iterations": schema.DefaultPasswordConfig.PBKDF2.Iterations,
prefixFilePassword + ".pbkdf2.salt_length": schema.DefaultPasswordConfig.PBKDF2.SaltLength,
prefixFilePassword + ".bcrypt.variant": schema.DefaultPasswordConfig.BCrypt.Variant,
prefixFilePassword + ".bcrypt.cost": schema.DefaultPasswordConfig.BCrypt.Cost,
prefixFilePassword + ".scrypt.iterations": schema.DefaultPasswordConfig.SCrypt.Iterations,
prefixFilePassword + ".scrypt.block_size": schema.DefaultPasswordConfig.SCrypt.BlockSize,
prefixFilePassword + ".scrypt.parallelism": schema.DefaultPasswordConfig.SCrypt.Parallelism,
prefixFilePassword + ".scrypt.key_length": schema.DefaultPasswordConfig.SCrypt.KeyLength,
prefixFilePassword + ".scrypt.salt_length": schema.DefaultPasswordConfig.SCrypt.SaltLength,
}
sources := configuration.NewDefaultSourcesWithDefaults(configs,
configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter,
configuration.NewMapSource(mapDefaults),
configuration.NewCommandLineSourceWithMapping(flags, flagsMap, false, false),
)
if algorithm != "" {
alg := map[string]interface{}{prefixFilePassword + ".algorithm": algorithm}
sources = append(sources, configuration.NewMapSource(alg))
}
val := schema.NewStructValidator()
if _, err = configuration.LoadAdvanced(val, prefixFilePassword, &c, sources...); err != nil {
return schema.Password{}, fmt.Errorf("error occurred loading configuration: %w", err)
}
validator.ValidatePasswordConfiguration(&c, val)
errs := val.Errors()
if len(errs) != 0 {
for i, e := range errs {
if i == 0 {
err = e
continue
}
err = fmt.Errorf("%v, %w", err, e)
}
return schema.Password{}, fmt.Errorf("errors occurred validating the password configuration: %w", err)
}
return c, nil
}
func cmdCryptoHashGetPassword(cmd *cobra.Command, args []string, useArgs, useRandom bool) (password string, random bool, err error) {
if useRandom {
if random, err = cmd.Flags().GetBool(cmdFlagNameRandom); err != nil {
return
}
}
switch {
case random:
var length int
if length, err = cmd.Flags().GetInt(cmdFlagNameRandomLength); err != nil {
return
}
password = utils.RandomString(length, utils.CharSetAlphaNumeric, true)
return
case cmd.Flags().Changed(cmdFlagNamePassword):
password, err = cmd.Flags().GetString(cmdFlagNamePassword)
return
case useArgs && len(args) != 0:
password, err = strings.Join(args, " "), nil
return
}
var (
data []byte
noConfirm bool
)
if data, err = hashReadPasswordWithPrompt("Enter Password: "); err != nil {
err = fmt.Errorf("failed to read the password from the terminal: %w", err)
return
}
password = string(data)
if cmd.Use == fmt.Sprintf(cmdUseFmtValidate, cmdUseValidate) {
fmt.Println("")
return
}
if noConfirm, err = cmd.Flags().GetBool(cmdFlagNameNoConfirm); err == nil && !noConfirm {
if data, err = hashReadPasswordWithPrompt("Confirm Password: "); err != nil {
err = fmt.Errorf("failed to read the password from the terminal: %w", err)
return
}
if password != string(data) {
fmt.Println("")
err = fmt.Errorf("the password did not match the confirmation password")
return
}
}
fmt.Println("")
return
}
func hashReadPasswordWithPrompt(prompt string) (data []byte, err error) {
fmt.Print(prompt)
if data, err = term.ReadPassword(int(syscall.Stdin)); err != nil { //nolint:unconvert // Conversion required.
if err.Error() == "inappropriate ioctl for device" {
return nil, fmt.Errorf("the terminal doesn't appear to be interactive either use the '--password' flag or use an interactive terminal: %w", err)
}
return nil, err
}
fmt.Println("")
return data, nil
}
func cmdFlagConfig(cmd *cobra.Command) {
cmd.Flags().StringSliceP(cmdFlagNameConfig, "c", []string{"configuration.yml"}, "configuration files to load")
}
func cmdFlagPassword(cmd *cobra.Command, noConfirm bool) {
cmd.Flags().String(cmdFlagNamePassword, "", "manually supply the password rather than using the terminal prompt")
if noConfirm {
cmd.Flags().Bool(cmdFlagNameNoConfirm, false, "skip the password confirmation prompt")
}
}
func cmdFlagRandomPassword(cmd *cobra.Command) {
cmd.Flags().Bool(cmdFlagNameRandom, false, "uses a randomly generated password")
cmd.Flags().Int(cmdFlagNameRandomLength, 72, "when using a randomly generated password it configures the length")
}
func cmdFlagIterations(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameIterations, "i", value, "number of iterations")
}
func cmdFlagKeySize(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameKeySize, "k", value, "key size in bytes")
}
func cmdFlagSaltSize(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameSaltSize, "s", value, "salt size in bytes")
}
func cmdFlagParallelism(cmd *cobra.Command, value int) {
cmd.Flags().IntP(cmdFlagNameParallelism, "p", value, "parallelism or threads")
}

View File

@ -416,7 +416,20 @@ func cryptoGetCertificateFromCmd(cmd *cobra.Command) (certificate *x509.Certific
return certificate, nil
}
func fmtCryptoUse(use string) string {
func fmtCryptoHashUse(use string) string {
switch use {
case cmdUseHashArgon2:
return "Argon2"
case cmdUseHashSHA2Crypt:
return "SHA2 Crypt"
case cmdUseHashPBKDF2:
return "PBKDF2"
default:
return use
}
}
func fmtCryptoCertificateUse(use string) string {
switch use {
case cmdUseEd25519:
return "Ed25519"

View File

@ -1,121 +0,0 @@
package commands
import (
"fmt"
"github.com/simia-tech/crypt"
"github.com/spf13/cobra"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
)
func newHashPasswordCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "hash-password [flags] -- <password>",
Short: cmdAutheliaHashPasswordShort,
Long: cmdAutheliaHashPasswordLong,
Example: cmdAutheliaHashPasswordExample,
Args: cobra.MinimumNArgs(1),
RunE: cmdHashPasswordRunE,
DisableAutoGenTag: true,
}
cmd.Flags().BoolP("sha512", "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordSHA512Configuration.Iterations))
cmd.Flags().IntP("iterations", "i", schema.DefaultPasswordConfiguration.Iterations, "set the number of hashing iterations")
cmd.Flags().StringP("salt", "s", "", "set the salt string")
cmd.Flags().IntP("memory", "m", schema.DefaultPasswordConfiguration.Memory, "[argon2id] set the amount of memory param (in MB)")
cmd.Flags().IntP("parallelism", "p", schema.DefaultPasswordConfiguration.Parallelism, "[argon2id] set the parallelism param")
cmd.Flags().IntP("key-length", "k", schema.DefaultPasswordConfiguration.KeyLength, "[argon2id] set the key length param")
cmd.Flags().IntP("salt-length", "l", schema.DefaultPasswordConfiguration.SaltLength, "set the auto-generated salt length")
cmd.Flags().StringSliceP("config", "c", []string{}, "Configuration files")
return cmd
}
func cmdHashPasswordRunE(cmd *cobra.Command, args []string) (err error) {
salt, _ := cmd.Flags().GetString("salt")
sha512, _ := cmd.Flags().GetBool("sha512")
configs, _ := cmd.Flags().GetStringSlice("config")
mapDefaults := map[string]interface{}{
"authentication_backend.file.password.algorithm": schema.DefaultPasswordConfiguration.Algorithm,
"authentication_backend.file.password.iterations": schema.DefaultPasswordConfiguration.Iterations,
"authentication_backend.file.password.key_length": schema.DefaultPasswordConfiguration.KeyLength,
"authentication_backend.file.password.salt_length": schema.DefaultPasswordConfiguration.SaltLength,
"authentication_backend.file.password.parallelism": schema.DefaultPasswordConfiguration.Parallelism,
"authentication_backend.file.password.memory": schema.DefaultPasswordConfiguration.Memory,
}
if sha512 {
mapDefaults["authentication_backend.file.password.algorithm"] = schema.DefaultPasswordSHA512Configuration.Algorithm
mapDefaults["authentication_backend.file.password.iterations"] = schema.DefaultPasswordSHA512Configuration.Iterations
mapDefaults["authentication_backend.file.password.salt_length"] = schema.DefaultPasswordSHA512Configuration.SaltLength
}
mapCLI := map[string]string{
"iterations": "authentication_backend.file.password.iterations",
"key-length": "authentication_backend.file.password.key_length",
"salt-length": "authentication_backend.file.password.salt_length",
"parallelism": "authentication_backend.file.password.parallelism",
"memory": "authentication_backend.file.password.memory",
}
sources := configuration.NewDefaultSourcesWithDefaults(configs,
configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter,
configuration.NewMapSource(mapDefaults),
configuration.NewCommandLineSourceWithMapping(cmd.Flags(), mapCLI, false, false),
)
val := schema.NewStructValidator()
if _, config, err = configuration.Load(val, sources...); err != nil {
return fmt.Errorf("error occurred loading configuration: %w", err)
}
var (
hash string
algorithm authentication.CryptAlgo
)
p := config.AuthenticationBackend.File.Password
switch p.Algorithm {
case "sha512":
algorithm = authentication.HashingAlgorithmSHA512
default:
algorithm = authentication.HashingAlgorithmArgon2id
}
validator.ValidatePasswordConfiguration(p, val)
errs := val.Errors()
if len(errs) != 0 {
for i, e := range errs {
if i == 0 {
err = e
continue
}
err = fmt.Errorf("%v, %w", err, e)
}
return fmt.Errorf("errors occurred validating the password configuration: %w", err)
}
if salt != "" {
salt = crypt.Base64Encoding.EncodeToString([]byte(salt))
}
if hash, err = authentication.HashPassword(args[0], salt, algorithm, p.Iterations, p.Memory*1024, p.Parallelism, p.KeyLength, p.SaltLength); err != nil {
return fmt.Errorf("error during password hashing: %w", err)
}
fmt.Printf("Password hash: %s\n", hash)
return nil
}

View File

@ -42,12 +42,12 @@ func NewRootCmd() (cmd *cobra.Command) {
cmdWithConfigFlags(cmd, false, []string{})
cmd.AddCommand(
newAccessControlCommand(),
newBuildInfoCmd(),
newCryptoCmd(),
newHashPasswordCmd(),
newStorageCmd(),
newValidateConfigCmd(),
newAccessControlCommand(),
)
return cmd

View File

@ -29,13 +29,13 @@ import (
func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
var configs []string
if configs, err = cmd.Flags().GetStringSlice("config"); err != nil {
if configs, err = cmd.Flags().GetStringSlice(cmdFlagNameConfig); err != nil {
return err
}
sources := make([]configuration.Source, 0, len(configs)+3)
if cmd.Flags().Changed("config") {
if cmd.Flags().Changed(cmdFlagNameConfig) {
for _, configFile := range configs {
if _, err := os.Stat(configFile); os.IsNotExist(err) {
return fmt.Errorf("could not load the provided configuration file %s: %w", configFile, err)
@ -406,7 +406,7 @@ func storageTOTPExportGetConfigFromFlags(cmd *cobra.Command) (format, dir string
break
case storageTOTPExportFormatPNG:
if dir == "" {
dir = utils.RandomString(8, utils.AlphaNumericCharacters, false)
dir = utils.RandomString(8, utils.CharSetAlphaNumeric, false)
}
if _, err = os.Stat(dir); !os.IsNotExist(err) {

View File

@ -2,6 +2,7 @@ package commands
import (
"fmt"
"os"
)
func recoverErr(i any) error {
@ -16,3 +17,15 @@ func recoverErr(i any) error {
return fmt.Errorf("recovered panic with unknown type: %v", v)
}
}
func configFilterExisting(configs []string) (finalConfigs []string) {
var err error
for _, c := range configs {
if _, err = os.Stat(c); err == nil || !os.IsNotExist(err) {
finalConfigs = append(finalConfigs, c)
}
}
return finalConfigs
}

View File

@ -31,7 +31,7 @@ func cmdValidateConfigRunE(cmd *cobra.Command, _ []string) (err error) {
val *schema.StructValidator
)
if configs, err = cmd.Flags().GetStringSlice("config"); err != nil {
if configs, err = cmd.Flags().GetStringSlice(cmdFlagNameConfig); err != nil {
return err
}

View File

@ -393,12 +393,32 @@ authentication_backend:
# file:
# path: /config/users_database.yml
# password:
# algorithm: argon2id
# iterations: 1
# key_length: 32
# salt_length: 16
# memory: 1024
# parallelism: 8
# algorithm: argon2
# argon2:
# variant: argon2id
# iterations: 3
# memory: 65536
# parallelism: 4
# key_length: 32
# salt_length: 16
# scrypt:
# iterations: 16
# block_size: 8
# parallelism: 1
# key_length: 32
# salt_length: 16
# pbkdf2:
# variant: sha512
# iterations: 310000
# salt_length: 16
# sha2crypt:
# variant: sha512
# iterations: 50000
# salt_length: 16
# bcrypt:
# variant: standard
# cost: 12
##
## Password Policy Configuration.

View File

@ -5,8 +5,86 @@ import (
"time"
)
// LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server.
type LDAPAuthenticationBackendConfiguration struct {
// AuthenticationBackend represents the configuration related to the authentication backend.
type AuthenticationBackend struct {
PasswordReset PasswordResetAuthenticationBackend `koanf:"password_reset"`
RefreshInterval string `koanf:"refresh_interval"`
File *FileAuthenticationBackend `koanf:"file"`
LDAP *LDAPAuthenticationBackend `koanf:"ldap"`
}
// PasswordResetAuthenticationBackend represents the configuration related to password reset functionality.
type PasswordResetAuthenticationBackend struct {
Disable bool `koanf:"disable"`
CustomURL url.URL `koanf:"custom_url"`
}
// FileAuthenticationBackend represents the configuration related to file-based backend.
type FileAuthenticationBackend struct {
Path string `koanf:"path"`
Password Password `koanf:"password"`
}
// Password represents the configuration related to password hashing.
type Password struct {
Algorithm string `koanf:"algorithm"`
Argon2 Argon2Password `koanf:"argon2"`
SHA2Crypt SHA2CryptPassword `koanf:"sha2crypt"`
PBKDF2 PBKDF2Password `koanf:"pbkdf2"`
BCrypt BCryptPassword `koanf:"bcrypt"`
SCrypt SCryptPassword `koanf:"scrypt"`
Iterations int `koanf:"iterations"`
Memory int `koanf:"memory"`
Parallelism int `koanf:"parallelism"`
KeyLength int `koanf:"key_length"`
SaltLength int `koanf:"salt_length"`
}
// Argon2Password represents the argon2 hashing settings.
type Argon2Password struct {
Variant string `koanf:"variant"`
Iterations int `koanf:"iterations"`
Memory int `koanf:"memory"`
Parallelism int `koanf:"parallelism"`
KeyLength int `koanf:"key_length"`
SaltLength int `koanf:"salt_length"`
}
// SHA2CryptPassword represents the sha2crypt hashing settings.
type SHA2CryptPassword struct {
Variant string `koanf:"variant"`
Iterations int `koanf:"iterations"`
SaltLength int `koanf:"salt_length"`
}
// PBKDF2Password represents the PBKDF2 hashing settings.
type PBKDF2Password struct {
Variant string `koanf:"variant"`
Iterations int `koanf:"iterations"`
SaltLength int `koanf:"salt_length"`
}
// BCryptPassword represents the bcrypt hashing settings.
type BCryptPassword struct {
Variant string `koanf:"variant"`
Cost int `koanf:"cost"`
}
// SCryptPassword represents the scrypt hashing settings.
type SCryptPassword struct {
Iterations int `koanf:"iterations"`
BlockSize int `koanf:"block_size"`
Parallelism int `koanf:"parallelism"`
KeyLength int `koanf:"key_length"`
SaltLength int `koanf:"salt_length"`
}
// LDAPAuthenticationBackend represents the configuration related to LDAP server.
type LDAPAuthenticationBackend struct {
Implementation string `koanf:"implementation"`
URL string `koanf:"url"`
Timeout time.Duration `koanf:"timeout"`
@ -34,68 +112,59 @@ type LDAPAuthenticationBackendConfiguration struct {
Password string `koanf:"password"`
}
// FileAuthenticationBackendConfiguration represents the configuration related to file-based backend.
type FileAuthenticationBackendConfiguration struct {
Path string `koanf:"path"`
Password *PasswordConfiguration `koanf:"password"`
// DefaultPasswordConfig represents the default configuration related to Argon2id hashing.
var DefaultPasswordConfig = Password{
Algorithm: argon2,
Argon2: Argon2Password{
Variant: argon2id,
Iterations: 3,
Memory: 64 * 1024,
Parallelism: 4,
KeyLength: 32,
SaltLength: 16,
},
SHA2Crypt: SHA2CryptPassword{
Variant: sha512,
Iterations: 50000,
SaltLength: 16,
},
PBKDF2: PBKDF2Password{
Variant: sha512,
Iterations: 310000,
SaltLength: 16,
},
BCrypt: BCryptPassword{
Variant: "standard",
Cost: 12,
},
SCrypt: SCryptPassword{
Iterations: 16,
BlockSize: 8,
Parallelism: 1,
KeyLength: 32,
SaltLength: 16,
},
}
// PasswordConfiguration represents the configuration related to password hashing.
type PasswordConfiguration struct {
Iterations int `koanf:"iterations"`
KeyLength int `koanf:"key_length"`
SaltLength int `koanf:"salt_length"`
Algorithm string `koanf:"algorithm"`
Memory int `koanf:"memory"`
Parallelism int `koanf:"parallelism"`
// DefaultCIPasswordConfig represents the default configuration related to Argon2id hashing for CI.
var DefaultCIPasswordConfig = Password{
Algorithm: argon2,
Argon2: Argon2Password{
Iterations: 3,
Memory: 64,
Parallelism: 4,
KeyLength: 32,
SaltLength: 16,
},
SHA2Crypt: SHA2CryptPassword{
Variant: sha512,
Iterations: 50000,
SaltLength: 16,
},
}
// AuthenticationBackendConfiguration represents the configuration related to the authentication backend.
type AuthenticationBackendConfiguration struct {
LDAP *LDAPAuthenticationBackendConfiguration `koanf:"ldap"`
File *FileAuthenticationBackendConfiguration `koanf:"file"`
PasswordReset PasswordResetAuthenticationBackendConfiguration `koanf:"password_reset"`
RefreshInterval string `koanf:"refresh_interval"`
}
// PasswordResetAuthenticationBackendConfiguration represents the configuration related to password reset functionality.
type PasswordResetAuthenticationBackendConfiguration struct {
Disable bool `koanf:"disable"`
CustomURL url.URL `koanf:"custom_url"`
}
// DefaultPasswordConfiguration represents the default configuration related to Argon2id hashing.
var DefaultPasswordConfiguration = PasswordConfiguration{
Iterations: 3,
KeyLength: 32,
SaltLength: 16,
Algorithm: argon2id,
Memory: 64,
Parallelism: 4,
}
// DefaultCIPasswordConfiguration represents the default configuration related to Argon2id hashing for CI.
var DefaultCIPasswordConfiguration = PasswordConfiguration{
Iterations: 3,
KeyLength: 32,
SaltLength: 16,
Algorithm: argon2id,
Memory: 64,
Parallelism: 4,
}
// DefaultPasswordSHA512Configuration represents the default configuration related to SHA512 hashing.
var DefaultPasswordSHA512Configuration = PasswordConfiguration{
Iterations: 50000,
SaltLength: 16,
Algorithm: "sha512",
}
// DefaultLDAPAuthenticationBackendConfiguration represents the default LDAP config.
var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendConfiguration{
Implementation: LDAPImplementationCustom,
// DefaultLDAPAuthenticationBackendConfigurationImplementationCustom represents the default LDAP config.
var DefaultLDAPAuthenticationBackendConfigurationImplementationCustom = LDAPAuthenticationBackend{
UsernameAttribute: "uid",
MailAttribute: "mail",
DisplayNameAttribute: "displayName",
@ -106,12 +175,16 @@ var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendCon
},
}
// DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration represents the default LDAP config for the MSAD Implementation.
var DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration = LDAPAuthenticationBackendConfiguration{
// DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory represents the default LDAP config for the MSAD Implementation.
var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory = LDAPAuthenticationBackend{
UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0)))",
UsernameAttribute: "sAMAccountName",
MailAttribute: "mail",
DisplayNameAttribute: "displayName",
GroupsFilter: "(&(member={dn})(objectClass=group))",
GroupNameAttribute: "cn",
Timeout: time.Second * 5,
TLS: &TLSConfig{
MinimumVersion: "TLS1.2",
},
}

View File

@ -8,19 +8,19 @@ type Configuration struct {
DefaultRedirectionURL string `koanf:"default_redirection_url"`
Default2FAMethod string `koanf:"default_2fa_method"`
Log LogConfiguration `koanf:"log"`
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"`
Session SessionConfiguration `koanf:"session"`
TOTP TOTPConfiguration `koanf:"totp"`
DuoAPI DuoAPIConfiguration `koanf:"duo_api"`
AccessControl AccessControlConfiguration `koanf:"access_control"`
NTP NTPConfiguration `koanf:"ntp"`
Regulation RegulationConfiguration `koanf:"regulation"`
Storage StorageConfiguration `koanf:"storage"`
Notifier NotifierConfiguration `koanf:"notifier"`
Server ServerConfiguration `koanf:"server"`
Telemetry TelemetryConfig `koanf:"telemetry"`
Webauthn WebauthnConfiguration `koanf:"webauthn"`
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
Log LogConfiguration `koanf:"log"`
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
AuthenticationBackend AuthenticationBackend `koanf:"authentication_backend"`
Session SessionConfiguration `koanf:"session"`
TOTP TOTPConfiguration `koanf:"totp"`
DuoAPI DuoAPIConfiguration `koanf:"duo_api"`
AccessControl AccessControlConfiguration `koanf:"access_control"`
NTP NTPConfiguration `koanf:"ntp"`
Regulation RegulationConfiguration `koanf:"regulation"`
Storage StorageConfiguration `koanf:"storage"`
Notifier NotifierConfiguration `koanf:"notifier"`
Server ServerConfiguration `koanf:"server"`
Telemetry TelemetryConfig `koanf:"telemetry"`
Webauthn WebauthnConfiguration `koanf:"webauthn"`
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
}

View File

@ -5,7 +5,12 @@ import (
"time"
)
const argon2id = "argon2id"
const (
argon2 = "argon2"
argon2id = "argon2id"
sha512 = "sha512"
sha256 = "sha256"
)
// ProfileRefreshDisabled represents a value for refresh_interval that disables the check entirely.
const ProfileRefreshDisabled = "disable"

View File

@ -46,6 +46,35 @@ var Keys = []string{
"identity_providers.oidc.clients[].userinfo_signing_algorithm",
"identity_providers.oidc.clients[].authorization_policy",
"identity_providers.oidc.clients[].pre_configured_consent_duration",
"authentication_backend.password_reset.disable",
"authentication_backend.password_reset.custom_url",
"authentication_backend.refresh_interval",
"authentication_backend.file.path",
"authentication_backend.file.password.algorithm",
"authentication_backend.file.password.argon2.variant",
"authentication_backend.file.password.argon2.iterations",
"authentication_backend.file.password.argon2.memory",
"authentication_backend.file.password.argon2.parallelism",
"authentication_backend.file.password.argon2.key_length",
"authentication_backend.file.password.argon2.salt_length",
"authentication_backend.file.password.sha2crypt.variant",
"authentication_backend.file.password.sha2crypt.iterations",
"authentication_backend.file.password.sha2crypt.salt_length",
"authentication_backend.file.password.pbkdf2.variant",
"authentication_backend.file.password.pbkdf2.iterations",
"authentication_backend.file.password.pbkdf2.salt_length",
"authentication_backend.file.password.bcrypt.variant",
"authentication_backend.file.password.bcrypt.cost",
"authentication_backend.file.password.scrypt.iterations",
"authentication_backend.file.password.scrypt.block_size",
"authentication_backend.file.password.scrypt.parallelism",
"authentication_backend.file.password.scrypt.key_length",
"authentication_backend.file.password.scrypt.salt_length",
"authentication_backend.file.password.iterations",
"authentication_backend.file.password.memory",
"authentication_backend.file.password.parallelism",
"authentication_backend.file.password.key_length",
"authentication_backend.file.password.salt_length",
"authentication_backend.ldap.implementation",
"authentication_backend.ldap.url",
"authentication_backend.ldap.timeout",
@ -67,16 +96,6 @@ var Keys = []string{
"authentication_backend.ldap.permit_feature_detection_failure",
"authentication_backend.ldap.user",
"authentication_backend.ldap.password",
"authentication_backend.file.path",
"authentication_backend.file.password.iterations",
"authentication_backend.file.password.key_length",
"authentication_backend.file.password.salt_length",
"authentication_backend.file.password.algorithm",
"authentication_backend.file.password.memory",
"authentication_backend.file.password.parallelism",
"authentication_backend.password_reset.disable",
"authentication_backend.password_reset.custom_url",
"authentication_backend.refresh_interval",
"session.name",
"session.domain",
"session.same_site",

View File

@ -62,7 +62,7 @@ func (suite *AccessControl) TestShouldValidateEitherDomainsOrDomainsRegex() {
}
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
suite.config.AccessControl.DefaultPolicy = testInvalidPolicy
suite.config.AccessControl.DefaultPolicy = testInvalid
ValidateAccessControl(suite.config, suite.validator)
@ -135,7 +135,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() {
suite.config.AccessControl.Rules = []schema.ACLRule{
{
Domains: []string{"public.example.com"},
Policy: testInvalidPolicy,
Policy: testInvalid,
},
}
@ -183,7 +183,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidMethod() {
func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() {
domains := []string{"public.example.com"}
subjects := [][]string{{"invalid"}}
subjects := [][]string{{testInvalid}}
suite.config.AccessControl.Rules = []schema.ACLRule{
{
Domains: domains,

View File

@ -5,26 +5,18 @@ import (
"net/url"
"strings"
"github.com/go-crypt/crypt"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidateAuthenticationBackend validates and updates the authentication backend configuration.
func ValidateAuthenticationBackend(config *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) {
func ValidateAuthenticationBackend(config *schema.AuthenticationBackend, validator *schema.StructValidator) {
if config.LDAP == nil && config.File == nil {
validator.Push(fmt.Errorf(errFmtAuthBackendNotConfigured))
}
if config.LDAP != nil && config.File != nil {
validator.Push(fmt.Errorf(errFmtAuthBackendMultipleConfigured))
}
if config.File != nil {
validateFileAuthenticationBackend(config.File, validator)
} else if config.LDAP != nil {
validateLDAPAuthenticationBackend(config, validator)
}
if config.RefreshInterval == "" {
config.RefreshInterval = schema.RefreshIntervalDefault
} else {
@ -42,110 +34,306 @@ func ValidateAuthenticationBackend(config *schema.AuthenticationBackendConfigura
validator.Push(fmt.Errorf(errFmtAuthBackendPasswordResetCustomURLScheme, config.PasswordReset.CustomURL.String(), config.PasswordReset.CustomURL.Scheme))
}
}
if config.LDAP != nil && config.File != nil {
validator.Push(fmt.Errorf(errFmtAuthBackendMultipleConfigured))
}
if config.File != nil {
validateFileAuthenticationBackend(config.File, validator)
}
if config.LDAP != nil {
validateLDAPAuthenticationBackend(config, validator)
}
}
// validateFileAuthenticationBackend validates and updates the file authentication backend configuration.
func validateFileAuthenticationBackend(config *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
func validateFileAuthenticationBackend(config *schema.FileAuthenticationBackend, validator *schema.StructValidator) {
if config.Path == "" {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPathNotConfigured))
}
if config.Password == nil {
config.Password = &schema.DefaultPasswordConfiguration
} else {
ValidatePasswordConfiguration(config.Password, validator)
}
ValidatePasswordConfiguration(&config.Password, validator)
}
// ValidatePasswordConfiguration validates the file auth backend password configuration.
func ValidatePasswordConfiguration(config *schema.PasswordConfiguration, validator *schema.StructValidator) {
// Salt Length.
func ValidatePasswordConfiguration(config *schema.Password, validator *schema.StructValidator) {
validateFileAuthenticationBackendPasswordConfigLegacy(config)
switch {
case config.SaltLength == 0:
config.SaltLength = schema.DefaultPasswordConfiguration.SaltLength
case config.SaltLength < 8:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordSaltLength, config.SaltLength))
}
switch config.Algorithm {
case "":
config.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
fallthrough
case hashArgon2id:
validateFileAuthenticationBackendArgon2id(config, validator)
case hashSHA512:
validateFileAuthenticationBackendSHA512(config)
case config.Algorithm == "":
config.Algorithm = schema.DefaultPasswordConfig.Algorithm
case utils.IsStringInSlice(config.Algorithm, validHashAlgorithms):
break
default:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordUnknownAlg, config.Algorithm))
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordUnknownAlg, config.Algorithm, strings.Join(validHashAlgorithms, "', '")))
}
if config.Iterations < 1 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidIterations, config.Iterations))
validateFileAuthenticationBackendPasswordConfigArgon2(config, validator)
validateFileAuthenticationBackendPasswordConfigSHA2Crypt(config, validator)
validateFileAuthenticationBackendPasswordConfigPBKDF2(config, validator)
validateFileAuthenticationBackendPasswordConfigBCrypt(config, validator)
validateFileAuthenticationBackendPasswordConfigSCrypt(config, validator)
}
//nolint:gocyclo // Function is well formed.
func validateFileAuthenticationBackendPasswordConfigArgon2(config *schema.Password, validator *schema.StructValidator) {
switch {
case config.Argon2.Variant == "":
config.Argon2.Variant = schema.DefaultPasswordConfig.Argon2.Variant
case utils.IsStringInSlice(config.Argon2.Variant, validArgon2Variants):
break
default:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidVariant, hashArgon2, config.Argon2.Variant, strings.Join(validArgon2Variants, "', '")))
}
switch {
case config.Argon2.Iterations == 0:
config.Argon2.Iterations = schema.DefaultPasswordConfig.Argon2.Iterations
case config.Argon2.Iterations < crypt.Argon2IterationsMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashArgon2, "iterations", config.Argon2.Iterations, crypt.Argon2IterationsMin))
case config.Argon2.Iterations > crypt.Argon2IterationsMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashArgon2, "iterations", config.Argon2.Iterations, crypt.Argon2IterationsMax))
}
switch {
case config.Argon2.Parallelism == 0:
config.Argon2.Parallelism = schema.DefaultPasswordConfig.Argon2.Parallelism
case config.Argon2.Parallelism < crypt.Argon2ParallelismMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashArgon2, "parallelism", config.Argon2.Parallelism, crypt.Argon2ParallelismMin))
case config.Argon2.Parallelism > crypt.Argon2ParallelismMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashArgon2, "parallelism", config.Argon2.Parallelism, crypt.Argon2ParallelismMax))
}
switch {
case config.Argon2.Memory == 0:
config.Argon2.Memory = schema.DefaultPasswordConfig.Argon2.Memory
case config.Argon2.Memory < 0:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashArgon2, "memory", config.Argon2.Parallelism, 1))
case config.Argon2.Memory < (crypt.Argon2MemoryMinParallelismMultiplier * config.Argon2.Parallelism):
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2MemoryTooLow, config.Argon2.Memory, config.Argon2.Parallelism*crypt.Argon2MemoryMinParallelismMultiplier, config.Argon2.Parallelism, crypt.Argon2MemoryMinParallelismMultiplier))
}
switch {
case config.Argon2.KeyLength == 0:
config.Argon2.KeyLength = schema.DefaultPasswordConfig.Argon2.KeyLength
case config.Argon2.KeyLength < crypt.Argon2KeySizeMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashArgon2, "key_length", config.Argon2.KeyLength, crypt.Argon2KeySizeMin))
case config.Argon2.KeyLength > crypt.Argon2KeySizeMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashArgon2, "key_length", config.Argon2.KeyLength, crypt.Argon2KeySizeMax))
}
switch {
case config.Argon2.SaltLength == 0:
config.Argon2.SaltLength = schema.DefaultPasswordConfig.Argon2.SaltLength
case config.Argon2.SaltLength < crypt.Argon2SaltSizeMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashArgon2, "salt_length", config.Argon2.SaltLength, crypt.Argon2SaltSizeMin))
case config.Argon2.SaltLength > crypt.Argon2SaltSizeMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashArgon2, "salt_length", config.Argon2.SaltLength, crypt.Argon2SaltSizeMax))
}
}
func validateFileAuthenticationBackendSHA512(config *schema.PasswordConfiguration) {
// Iterations (time).
if config.Iterations == 0 {
config.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations
}
}
func validateFileAuthenticationBackendArgon2id(config *schema.PasswordConfiguration, validator *schema.StructValidator) {
// Iterations (time).
if config.Iterations == 0 {
config.Iterations = schema.DefaultPasswordConfiguration.Iterations
func validateFileAuthenticationBackendPasswordConfigSHA2Crypt(config *schema.Password, validator *schema.StructValidator) {
switch {
case config.SHA2Crypt.Variant == "":
config.SHA2Crypt.Variant = schema.DefaultPasswordConfig.SHA2Crypt.Variant
case utils.IsStringInSlice(config.SHA2Crypt.Variant, validSHA2CryptVariants):
break
default:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidVariant, hashSHA2Crypt, config.SHA2Crypt.Variant, strings.Join(validSHA2CryptVariants, "', '")))
}
// Parallelism.
if config.Parallelism == 0 {
config.Parallelism = schema.DefaultPasswordConfiguration.Parallelism
} else if config.Parallelism < 1 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidParallelism, config.Parallelism))
switch {
case config.SHA2Crypt.Iterations == 0:
config.SHA2Crypt.Iterations = schema.DefaultPasswordConfig.SHA2Crypt.Iterations
case config.SHA2Crypt.Iterations < crypt.SHA2CryptIterationsMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashSHA2Crypt, "iterations", config.SHA2Crypt.Iterations, crypt.SHA2CryptIterationsMin))
case config.SHA2Crypt.Iterations > crypt.SHA2CryptIterationsMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashSHA2Crypt, "iterations", config.SHA2Crypt.Iterations, crypt.SHA2CryptIterationsMax))
}
// Memory.
if config.Memory == 0 {
config.Memory = schema.DefaultPasswordConfiguration.Memory
} else if config.Memory < config.Parallelism*8 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidMemory, config.Parallelism, config.Parallelism*8, config.Memory))
}
// Key Length.
if config.KeyLength == 0 {
config.KeyLength = schema.DefaultPasswordConfiguration.KeyLength
} else if config.KeyLength < 16 {
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidKeyLength, config.KeyLength))
switch {
case config.SHA2Crypt.SaltLength == 0:
config.SHA2Crypt.SaltLength = schema.DefaultPasswordConfig.SHA2Crypt.SaltLength
case config.SHA2Crypt.SaltLength < crypt.SHA2CryptSaltSizeMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashSHA2Crypt, "salt_length", config.SHA2Crypt.SaltLength, crypt.SHA2CryptSaltSizeMin))
case config.SHA2Crypt.SaltLength > crypt.SHA2CryptSaltSizeMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashSHA2Crypt, "salt_length", config.SHA2Crypt.SaltLength, crypt.SHA2CryptSaltSizeMax))
}
}
func validateLDAPAuthenticationBackend(config *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) {
if config.LDAP.Timeout == 0 {
config.LDAP.Timeout = schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout
func validateFileAuthenticationBackendPasswordConfigPBKDF2(config *schema.Password, validator *schema.StructValidator) {
switch {
case config.PBKDF2.Variant == "":
config.PBKDF2.Variant = schema.DefaultPasswordConfig.PBKDF2.Variant
case utils.IsStringInSlice(config.PBKDF2.Variant, validPBKDF2Variants):
break
default:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidVariant, hashPBKDF2, config.PBKDF2.Variant, strings.Join(validPBKDF2Variants, "', '")))
}
switch {
case config.PBKDF2.Iterations == 0:
config.PBKDF2.Iterations = schema.DefaultPasswordConfig.PBKDF2.Iterations
case config.PBKDF2.Iterations < crypt.PBKDF2IterationsMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashPBKDF2, "iterations", config.PBKDF2.Iterations, crypt.PBKDF2IterationsMin))
case config.PBKDF2.Iterations > crypt.PBKDF2IterationsMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashPBKDF2, "iterations", config.PBKDF2.Iterations, crypt.PBKDF2IterationsMax))
}
switch {
case config.PBKDF2.SaltLength == 0:
config.PBKDF2.SaltLength = schema.DefaultPasswordConfig.PBKDF2.SaltLength
case config.PBKDF2.SaltLength < crypt.PBKDF2SaltSizeMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashPBKDF2, "salt_length", config.PBKDF2.SaltLength, crypt.PBKDF2SaltSizeMin))
case config.PBKDF2.SaltLength > crypt.PBKDF2SaltSizeMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashPBKDF2, "salt_length", config.PBKDF2.SaltLength, crypt.PBKDF2SaltSizeMax))
}
}
func validateFileAuthenticationBackendPasswordConfigBCrypt(config *schema.Password, validator *schema.StructValidator) {
switch {
case config.BCrypt.Variant == "":
config.BCrypt.Variant = schema.DefaultPasswordConfig.BCrypt.Variant
case utils.IsStringInSlice(config.BCrypt.Variant, validBCryptVariants):
break
default:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidVariant, hashBCrypt, config.BCrypt.Variant, strings.Join(validBCryptVariants, "', '")))
}
switch {
case config.BCrypt.Cost == 0:
config.BCrypt.Cost = schema.DefaultPasswordConfig.BCrypt.Cost
case config.BCrypt.Cost < crypt.BcryptCostMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashBCrypt, "cost", config.BCrypt.Cost, crypt.BcryptCostMin))
case config.BCrypt.Cost > crypt.BcryptCostMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashBCrypt, "cost", config.BCrypt.Cost, crypt.BcryptCostMax))
}
}
func validateFileAuthenticationBackendPasswordConfigSCrypt(config *schema.Password, validator *schema.StructValidator) {
switch {
case config.SCrypt.Iterations == 0:
config.SCrypt.Iterations = schema.DefaultPasswordConfig.SCrypt.Iterations
case config.SCrypt.Iterations < crypt.ScryptIterationsMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashSCrypt, "iterations", config.SCrypt.Iterations, crypt.ScryptIterationsMin))
}
switch {
case config.SCrypt.BlockSize == 0:
config.SCrypt.BlockSize = schema.DefaultPasswordConfig.SCrypt.BlockSize
case config.SCrypt.BlockSize < crypt.ScryptBlockSizeMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashSCrypt, "block_size", config.SCrypt.BlockSize, crypt.ScryptBlockSizeMin))
case config.SCrypt.BlockSize > crypt.ScryptBlockSizeMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashSCrypt, "block_size", config.SCrypt.BlockSize, crypt.ScryptBlockSizeMax))
}
switch {
case config.SCrypt.Parallelism == 0:
config.SCrypt.Parallelism = schema.DefaultPasswordConfig.SCrypt.Parallelism
case config.SCrypt.Parallelism < crypt.ScryptParallelismMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashSCrypt, "parallelism", config.SCrypt.Parallelism, crypt.ScryptParallelismMin))
}
switch {
case config.SCrypt.KeyLength == 0:
config.SCrypt.KeyLength = schema.DefaultPasswordConfig.SCrypt.KeyLength
case config.SCrypt.KeyLength < crypt.ScryptKeySizeMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashSCrypt, "key_length", config.SCrypt.KeyLength, crypt.ScryptKeySizeMin))
case config.SCrypt.KeyLength > crypt.ScryptKeySizeMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashSCrypt, "key_length", config.SCrypt.KeyLength, crypt.ScryptKeySizeMax))
}
switch {
case config.SCrypt.SaltLength == 0:
config.SCrypt.SaltLength = schema.DefaultPasswordConfig.SCrypt.SaltLength
case config.SCrypt.SaltLength < crypt.ScryptSaltSizeMin:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooSmall, hashSCrypt, "salt_length", config.SCrypt.SaltLength, crypt.ScryptSaltSizeMin))
case config.SCrypt.SaltLength > crypt.ScryptSaltSizeMax:
validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordOptionTooLarge, hashSCrypt, "salt_length", config.SCrypt.SaltLength, crypt.ScryptSaltSizeMax))
}
}
//nolint:gocyclo // Function is clear enough.
func validateFileAuthenticationBackendPasswordConfigLegacy(config *schema.Password) {
switch config.Algorithm {
case hashLegacySHA512:
config.Algorithm = hashSHA2Crypt
if config.SHA2Crypt.Variant == "" {
config.SHA2Crypt.Variant = schema.DefaultPasswordConfig.SHA2Crypt.Variant
}
if config.Iterations > 0 && config.SHA2Crypt.Iterations == 0 {
config.SHA2Crypt.Iterations = config.Iterations
}
if config.SaltLength > 0 && config.SHA2Crypt.SaltLength == 0 {
if config.SaltLength > 16 {
config.SHA2Crypt.SaltLength = 16
} else {
config.SHA2Crypt.SaltLength = config.SaltLength
}
}
case hashLegacyArgon2id:
config.Algorithm = hashArgon2
if config.Argon2.Variant == "" {
config.Argon2.Variant = schema.DefaultPasswordConfig.Argon2.Variant
}
if config.Iterations > 0 && config.Argon2.Memory == 0 {
config.Argon2.Iterations = config.Iterations
}
if config.Memory > 0 && config.Argon2.Memory == 0 {
config.Argon2.Memory = config.Memory * 1024
}
if config.Parallelism > 0 && config.Argon2.Parallelism == 0 {
config.Argon2.Parallelism = config.Parallelism
}
if config.KeyLength > 0 && config.Argon2.KeyLength == 0 {
config.Argon2.KeyLength = config.KeyLength
}
if config.SaltLength > 0 && config.Argon2.SaltLength == 0 {
config.Argon2.SaltLength = config.SaltLength
}
}
}
func validateLDAPAuthenticationBackend(config *schema.AuthenticationBackend, validator *schema.StructValidator) {
if config.LDAP.Implementation == "" {
config.LDAP.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation
config.LDAP.Implementation = schema.LDAPImplementationCustom
}
if config.LDAP.TLS == nil {
config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
} else if config.LDAP.TLS.MinimumVersion == "" {
config.LDAP.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion
}
if _, err := utils.TLSStringToTLSConfigVersion(config.LDAP.TLS.MinimumVersion); err != nil {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendTLSMinVersion, config.LDAP.TLS.MinimumVersion, err))
}
var implementation *schema.LDAPAuthenticationBackend
switch config.LDAP.Implementation {
case schema.LDAPImplementationCustom:
setDefaultImplementationCustomLDAPAuthenticationBackend(config.LDAP)
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom
case schema.LDAPImplementationActiveDirectory:
setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(config.LDAP)
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory
default:
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, config.LDAP.Implementation, strings.Join([]string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory}, "', '")))
}
if implementation != nil {
setDefaultImplementationLDAPAuthenticationBackendProfileMisc(config.LDAP, implementation)
setDefaultImplementationLDAPAuthenticationBackendProfileAttributes(config.LDAP, implementation)
}
if config.LDAP.TLS != nil {
if _, err := utils.TLSStringToTLSConfigVersion(config.LDAP.TLS.MinimumVersion); err != nil {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendTLSMinVersion, config.LDAP.TLS.MinimumVersion, err))
}
} else {
config.LDAP.TLS = &schema.TLSConfig{}
}
if strings.Contains(config.LDAP.UsersFilter, "{0}") {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterReplacedPlaceholders, "users_filter", "{0}", "{input}"))
}
@ -167,7 +355,53 @@ func validateLDAPAuthenticationBackend(config *schema.AuthenticationBackendConfi
validateLDAPRequiredParameters(config, validator)
}
func validateLDAPAuthenticationBackendURL(config *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
func setDefaultImplementationLDAPAuthenticationBackendProfileMisc(config *schema.LDAPAuthenticationBackend, implementation *schema.LDAPAuthenticationBackend) {
if config.Timeout == 0 {
config.Timeout = implementation.Timeout
}
if implementation.TLS == nil {
return
}
if config.TLS == nil {
config.TLS = implementation.TLS
} else if config.TLS.MinimumVersion == "" {
config.TLS.MinimumVersion = implementation.TLS.MinimumVersion
}
}
func ldapImplementationShouldSetStr(config, implementation string) bool {
return config == "" && implementation != ""
}
func setDefaultImplementationLDAPAuthenticationBackendProfileAttributes(config *schema.LDAPAuthenticationBackend, implementation *schema.LDAPAuthenticationBackend) {
if ldapImplementationShouldSetStr(config.UsersFilter, implementation.UsersFilter) {
config.UsersFilter = implementation.UsersFilter
}
if ldapImplementationShouldSetStr(config.UsernameAttribute, implementation.UsernameAttribute) {
config.UsernameAttribute = implementation.UsernameAttribute
}
if ldapImplementationShouldSetStr(config.DisplayNameAttribute, implementation.DisplayNameAttribute) {
config.DisplayNameAttribute = implementation.DisplayNameAttribute
}
if ldapImplementationShouldSetStr(config.MailAttribute, implementation.MailAttribute) {
config.MailAttribute = implementation.MailAttribute
}
if ldapImplementationShouldSetStr(config.GroupsFilter, implementation.GroupsFilter) {
config.GroupsFilter = implementation.GroupsFilter
}
if ldapImplementationShouldSetStr(config.GroupNameAttribute, implementation.GroupNameAttribute) {
config.GroupNameAttribute = implementation.GroupNameAttribute
}
}
func validateLDAPAuthenticationBackendURL(config *schema.LDAPAuthenticationBackend, validator *schema.StructValidator) {
var (
parsedURL *url.URL
err error
@ -191,7 +425,7 @@ func validateLDAPAuthenticationBackendURL(config *schema.LDAPAuthenticationBacke
}
}
func validateLDAPRequiredParameters(config *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) {
func validateLDAPRequiredParameters(config *schema.AuthenticationBackend, validator *schema.StructValidator) {
if config.LDAP.PermitUnauthenticatedBind {
if config.LDAP.Password != "" {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendUnauthenticatedBindWithPassword))
@ -237,47 +471,3 @@ func validateLDAPRequiredParameters(config *schema.AuthenticationBackendConfigur
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, "groups_filter", config.LDAP.GroupsFilter, config.LDAP.GroupsFilter))
}
}
func setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration) {
if config.UsersFilter == "" {
config.UsersFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter
}
if config.UsernameAttribute == "" {
config.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute
}
if config.DisplayNameAttribute == "" {
config.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute
}
if config.MailAttribute == "" {
config.MailAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute
}
if config.GroupsFilter == "" {
config.GroupsFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter
}
if config.GroupNameAttribute == "" {
config.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute
}
}
func setDefaultImplementationCustomLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration) {
if config.UsernameAttribute == "" {
config.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute
}
if config.GroupNameAttribute == "" {
config.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute
}
if config.MailAttribute == "" {
config.MailAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.MailAttribute
}
if config.DisplayNameAttribute == "" {
config.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.DisplayNameAttribute
}
}

View File

@ -14,22 +14,28 @@ import (
func TestShouldRaiseErrorWhenBothBackendsProvided(t *testing.T) {
validator := schema.NewStructValidator()
backendConfig := schema.AuthenticationBackendConfiguration{}
backendConfig := schema.AuthenticationBackend{}
backendConfig.LDAP = &schema.LDAPAuthenticationBackendConfiguration{}
backendConfig.File = &schema.FileAuthenticationBackendConfiguration{
backendConfig.LDAP = &schema.LDAPAuthenticationBackend{}
backendConfig.File = &schema.FileAuthenticationBackend{
Path: "/tmp",
}
ValidateAuthenticationBackend(&backendConfig, validator)
require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Errors(), 7)
assert.EqualError(t, validator.Errors()[0], "authentication_backend: please ensure only one of the 'file' or 'ldap' backend is configured")
assert.EqualError(t, validator.Errors()[1], "authentication_backend: ldap: option 'url' is required")
assert.EqualError(t, validator.Errors()[2], "authentication_backend: ldap: option 'user' is required")
assert.EqualError(t, validator.Errors()[3], "authentication_backend: ldap: option 'password' is required")
assert.EqualError(t, validator.Errors()[4], "authentication_backend: ldap: option 'base_dn' is required")
assert.EqualError(t, validator.Errors()[5], "authentication_backend: ldap: option 'users_filter' is required")
assert.EqualError(t, validator.Errors()[6], "authentication_backend: ldap: option 'groups_filter' is required")
}
func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) {
validator := schema.NewStructValidator()
backendConfig := schema.AuthenticationBackendConfiguration{}
backendConfig := schema.AuthenticationBackend{}
ValidateAuthenticationBackend(&backendConfig, validator)
@ -39,23 +45,18 @@ func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) {
type FileBasedAuthenticationBackend struct {
suite.Suite
config schema.AuthenticationBackendConfiguration
config schema.AuthenticationBackend
validator *schema.StructValidator
}
func (suite *FileBasedAuthenticationBackend) SetupTest() {
password := schema.DefaultPasswordConfig
suite.validator = schema.NewStructValidator()
suite.config = schema.AuthenticationBackendConfiguration{}
suite.config.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path", Password: &schema.PasswordConfiguration{
Algorithm: schema.DefaultPasswordConfiguration.Algorithm,
Iterations: schema.DefaultPasswordConfiguration.Iterations,
Parallelism: schema.DefaultPasswordConfiguration.Parallelism,
Memory: schema.DefaultPasswordConfiguration.Memory,
KeyLength: schema.DefaultPasswordConfiguration.KeyLength,
SaltLength: schema.DefaultPasswordConfiguration.SaltLength,
}}
suite.config.File.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
suite.config = schema.AuthenticationBackend{}
suite.config.File = &schema.FileAuthenticationBackend{Path: "/a/path", Password: password}
}
func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() {
ValidateAuthenticationBackend(&suite.config, suite.validator)
@ -74,20 +75,8 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvi
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: option 'path' is required")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMoreThanEightTimesParallelism() {
suite.config.File.Password.Memory = 8
suite.config.File.Password.Parallelism = 2
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'memory' must at least be parallelism multiplied by 8 when using algorithm 'argon2id' with parallelism 2 it should be at least 16 but it is configured as '8'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenBlank() {
suite.config.File.Password = &schema.PasswordConfiguration{}
suite.config.File.Password = schema.Password{}
suite.Assert().Equal(0, suite.config.File.Password.KeyLength)
suite.Assert().Equal(0, suite.config.File.Password.Iterations)
@ -101,51 +90,384 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWh
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.config.File.Password.KeyLength)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.config.File.Password.Parallelism)
suite.Assert().Equal(schema.DefaultPasswordConfig.KeyLength, suite.config.File.Password.KeyLength)
suite.Assert().Equal(schema.DefaultPasswordConfig.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordConfig.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordConfig.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordConfig.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordConfig.Parallelism, suite.config.File.Password.Parallelism)
}
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenOnlySHA512Set() {
suite.config.File.Password = &schema.PasswordConfiguration{}
func (suite *FileBasedAuthenticationBackend) TestShouldMigrateLegacyConfigurationSHA512() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password.Algorithm = "sha512"
suite.config.File.Password = schema.Password{
Algorithm: digestSHA512,
Iterations: 1000000,
SaltLength: 8,
}
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.config.File.Password.KeyLength)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Parallelism, suite.config.File.Password.Parallelism)
suite.Assert().Equal(hashSHA2Crypt, suite.config.File.Password.Algorithm)
suite.Assert().Equal(digestSHA512, suite.config.File.Password.SHA2Crypt.Variant)
suite.Assert().Equal(1000000, suite.config.File.Password.SHA2Crypt.Iterations)
suite.Assert().Equal(8, suite.config.File.Password.SHA2Crypt.SaltLength)
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() {
suite.config.File.Password.KeyLength = 1
func (suite *FileBasedAuthenticationBackend) TestShouldMigrateLegacyConfigurationSHA512ButNotOverride() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password = schema.Password{
Algorithm: digestSHA512,
Iterations: 1000000,
SaltLength: 8,
SHA2Crypt: schema.SHA2CryptPassword{
Variant: digestSHA256,
Iterations: 50000,
SaltLength: 12,
},
}
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(hashSHA2Crypt, suite.config.File.Password.Algorithm)
suite.Assert().Equal(digestSHA256, suite.config.File.Password.SHA2Crypt.Variant)
suite.Assert().Equal(50000, suite.config.File.Password.SHA2Crypt.Iterations)
suite.Assert().Equal(12, suite.config.File.Password.SHA2Crypt.SaltLength)
}
func (suite *FileBasedAuthenticationBackend) TestShouldMigrateLegacyConfigurationSHA512Alt() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password = schema.Password{
Algorithm: digestSHA512,
Iterations: 1000000,
SaltLength: 64,
}
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(hashSHA2Crypt, suite.config.File.Password.Algorithm)
suite.Assert().Equal(digestSHA512, suite.config.File.Password.SHA2Crypt.Variant)
suite.Assert().Equal(1000000, suite.config.File.Password.SHA2Crypt.Iterations)
suite.Assert().Equal(16, suite.config.File.Password.SHA2Crypt.SaltLength)
}
func (suite *FileBasedAuthenticationBackend) TestShouldMigrateLegacyConfigurationArgon2() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password = schema.Password{
Algorithm: "argon2id",
Iterations: 4,
Memory: 1024,
Parallelism: 4,
KeyLength: 64,
SaltLength: 64,
}
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal("argon2", suite.config.File.Password.Algorithm)
suite.Assert().Equal("argon2id", suite.config.File.Password.Argon2.Variant)
suite.Assert().Equal(4, suite.config.File.Password.Argon2.Iterations)
suite.Assert().Equal(1048576, suite.config.File.Password.Argon2.Memory)
suite.Assert().Equal(4, suite.config.File.Password.Argon2.Parallelism)
suite.Assert().Equal(64, suite.config.File.Password.Argon2.KeyLength)
suite.Assert().Equal(64, suite.config.File.Password.Argon2.SaltLength)
}
func (suite *FileBasedAuthenticationBackend) TestShouldMigrateLegacyConfigurationArgon2ButNotOverride() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password = schema.Password{
Algorithm: "argon2id",
Iterations: 4,
Memory: 1024,
Parallelism: 4,
KeyLength: 64,
SaltLength: 64,
Argon2: schema.Argon2Password{
Variant: "argon2d",
Iterations: 1,
Memory: 2048,
Parallelism: 1,
KeyLength: 32,
SaltLength: 32,
},
}
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal("argon2", suite.config.File.Password.Algorithm)
suite.Assert().Equal("argon2d", suite.config.File.Password.Argon2.Variant)
suite.Assert().Equal(1, suite.config.File.Password.Argon2.Iterations)
suite.Assert().Equal(2048, suite.config.File.Password.Argon2.Memory)
suite.Assert().Equal(1, suite.config.File.Password.Argon2.Parallelism)
suite.Assert().Equal(32, suite.config.File.Password.Argon2.KeyLength)
suite.Assert().Equal(32, suite.config.File.Password.Argon2.SaltLength)
}
func (suite *FileBasedAuthenticationBackend) TestShouldMigrateLegacyConfigurationWhenOnlySHA512Set() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password.Algorithm = digestSHA512
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(hashSHA2Crypt, suite.config.File.Password.Algorithm)
suite.Assert().Equal(digestSHA512, suite.config.File.Password.SHA2Crypt.Variant)
suite.Assert().Equal(schema.DefaultPasswordConfig.SHA2Crypt.Iterations, suite.config.File.Password.SHA2Crypt.Iterations)
suite.Assert().Equal(schema.DefaultPasswordConfig.SHA2Crypt.SaltLength, suite.config.File.Password.SHA2Crypt.SaltLength)
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorOnInvalidArgon2Variant() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password.Algorithm = "argon2"
suite.config.File.Password.Argon2.Variant = testInvalid
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'key_length' must be 16 or more when using algorithm 'argon2id' but it is configured as '1'")
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: argon2: option 'variant' is configured as 'invalid' but must be one of the following values: 'argon2id', 'id', 'argon2i', 'i', 'argon2d', 'd'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() {
suite.config.File.Password.SaltLength = -1
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorOnInvalidSHA2CryptVariant() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password.Algorithm = hashSHA2Crypt
suite.config.File.Password.SHA2Crypt.Variant = testInvalid
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'salt_length' must be 2 or more but it is configured a '-1'")
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: sha2crypt: option 'variant' is configured as 'invalid' but must be one of the following values: 'sha256', 'sha512'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorOnInvalidSHA2CryptSaltLength() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password.Algorithm = hashSHA2Crypt
suite.config.File.Password.SHA2Crypt.SaltLength = 40
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: sha2crypt: option 'salt_length' is configured as '40' but must be less than or equal to '16'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorOnInvalidPBKDF2Variant() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password.Algorithm = "pbkdf2"
suite.config.File.Password.PBKDF2.Variant = testInvalid
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: pbkdf2: option 'variant' is configured as 'invalid' but must be one of the following values: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorOnInvalidBCryptVariant() {
suite.config.File.Password = schema.Password{}
suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.config.File.Password.Algorithm = "bcrypt"
suite.config.File.Password.BCrypt.Variant = testInvalid
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: bcrypt: option 'variant' is configured as 'invalid' but must be one of the following values: 'standard', 'sha256'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSHA2CryptOptionsTooLow() {
suite.config.File.Password.SHA2Crypt.Iterations = -1
suite.config.File.Password.SHA2Crypt.SaltLength = -1
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 2)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: sha2crypt: option 'iterations' is configured as '-1' but must be greater than or equal to '1000'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: sha2crypt: option 'salt_length' is configured as '-1' but must be greater than or equal to '1'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSHA2CryptOptionsTooHigh() {
suite.config.File.Password.SHA2Crypt.Iterations = 999999999999
suite.config.File.Password.SHA2Crypt.SaltLength = 99
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 2)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: sha2crypt: option 'iterations' is configured as '999999999999' but must be less than or equal to '999999999'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: sha2crypt: option 'salt_length' is configured as '99' but must be less than or equal to '16'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenPBKDF2OptionsTooLow() {
suite.config.File.Password.PBKDF2.Iterations = -1
suite.config.File.Password.PBKDF2.SaltLength = -1
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 2)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: pbkdf2: option 'iterations' is configured as '-1' but must be greater than or equal to '100000'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: pbkdf2: option 'salt_length' is configured as '-1' but must be greater than or equal to '8'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenPBKDF2OptionsTooHigh() {
suite.config.File.Password.PBKDF2.Iterations = 2147483649
suite.config.File.Password.PBKDF2.SaltLength = 2147483650
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 2)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: pbkdf2: option 'iterations' is configured as '2147483649' but must be less than or equal to '2147483647'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: pbkdf2: option 'salt_length' is configured as '2147483650' but must be less than or equal to '2147483647'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBCryptOptionsTooLow() {
suite.config.File.Password.BCrypt.Cost = -1
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: bcrypt: option 'cost' is configured as '-1' but must be greater than or equal to '10'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBCryptOptionsTooHigh() {
suite.config.File.Password.BCrypt.Cost = 900
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: bcrypt: option 'cost' is configured as '900' but must be less than or equal to '31'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSCryptOptionsTooLow() {
suite.config.File.Password.SCrypt.Iterations = -1
suite.config.File.Password.SCrypt.BlockSize = -21
suite.config.File.Password.SCrypt.Parallelism = -11
suite.config.File.Password.SCrypt.KeyLength = -77
suite.config.File.Password.SCrypt.SaltLength = 7
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 5)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: scrypt: option 'iterations' is configured as '-1' but must be greater than or equal to '1'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: scrypt: option 'block_size' is configured as '-21' but must be greater than or equal to '1'")
suite.Assert().EqualError(suite.validator.Errors()[2], "authentication_backend: file: password: scrypt: option 'parallelism' is configured as '-11' but must be greater than or equal to '1'")
suite.Assert().EqualError(suite.validator.Errors()[3], "authentication_backend: file: password: scrypt: option 'key_length' is configured as '-77' but must be greater than or equal to '1'")
suite.Assert().EqualError(suite.validator.Errors()[4], "authentication_backend: file: password: scrypt: option 'salt_length' is configured as '7' but must be greater than or equal to '8'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSCryptOptionsTooHigh() {
suite.config.File.Password.SCrypt.BlockSize = 360287970189639672
suite.config.File.Password.SCrypt.KeyLength = 1374389534409
suite.config.File.Password.SCrypt.SaltLength = 2147483647
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 3)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: scrypt: option 'block_size' is configured as '360287970189639672' but must be less than or equal to '36028797018963967'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: scrypt: option 'key_length' is configured as '1374389534409' but must be less than or equal to '137438953440'")
suite.Assert().EqualError(suite.validator.Errors()[2], "authentication_backend: file: password: scrypt: option 'salt_length' is configured as '2147483647' but must be less than or equal to '1024'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenArgon2OptionsTooLow() {
suite.config.File.Password.Argon2.Iterations = -1
suite.config.File.Password.Argon2.Memory = -1
suite.config.File.Password.Argon2.Parallelism = -1
suite.config.File.Password.Argon2.KeyLength = 1
suite.config.File.Password.Argon2.SaltLength = -1
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 5)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: argon2: option 'iterations' is configured as '-1' but must be greater than or equal to '1'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: argon2: option 'parallelism' is configured as '-1' but must be greater than or equal to '1'")
suite.Assert().EqualError(suite.validator.Errors()[2], "authentication_backend: file: password: argon2: option 'memory' is configured as '-1' but must be greater than or equal to '1'")
suite.Assert().EqualError(suite.validator.Errors()[3], "authentication_backend: file: password: argon2: option 'key_length' is configured as '1' but must be greater than or equal to '4'")
suite.Assert().EqualError(suite.validator.Errors()[4], "authentication_backend: file: password: argon2: option 'salt_length' is configured as '-1' but must be greater than or equal to '1'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenArgon2OptionsTooHigh() {
suite.config.File.Password.Argon2.Iterations = 9999999999
suite.config.File.Password.Argon2.Parallelism = 16777216
suite.config.File.Password.Argon2.KeyLength = 9999999998
suite.config.File.Password.Argon2.SaltLength = 9999999997
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 5)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: argon2: option 'iterations' is configured as '9999999999' but must be less than or equal to '2147483647'")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: file: password: argon2: option 'parallelism' is configured as '16777216' but must be less than or equal to '16777215'")
suite.Assert().EqualError(suite.validator.Errors()[3], "authentication_backend: file: password: argon2: option 'key_length' is configured as '9999999998' but must be less than or equal to '2147483647'")
suite.Assert().EqualError(suite.validator.Errors()[4], "authentication_backend: file: password: argon2: option 'salt_length' is configured as '9999999997' but must be less than or equal to '2147483647'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenArgon2MemoryTooLow() {
suite.config.File.Password.Argon2.Memory = 4
suite.config.File.Password.Argon2.Parallelism = 4
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: argon2: option 'memory' is configured as '4' but must be greater than or equal to '32' or '4' (the value of 'parallelism) multiplied by '8'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() {
@ -156,29 +478,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorith
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'algorithm' must be either 'argon2id' or 'sha512' but it is configured as 'bogus'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() {
suite.config.File.Password.Iterations = -1
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'iterations' must be 1 or more but it is configured as '-1'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() {
suite.config.File.Password.Parallelism = -1
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'parallelism' must be 1 or more when using algorithm 'argon2id' but it is configured as '-1'")
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'algorithm' is configured as 'bogus' but must be one of the following values: 'sha2crypt', 'pbkdf2', 'scrypt', 'bcrypt', 'argon2'")
}
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() {
@ -193,42 +493,11 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() {
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.config.File.Password.Parallelism)
}
func TestFileBasedAuthenticationBackend(t *testing.T) {
suite.Run(t, new(FileBasedAuthenticationBackend))
}
type LDAPAuthenticationBackendSuite struct {
suite.Suite
config schema.AuthenticationBackendConfiguration
validator *schema.StructValidator
}
func (suite *LDAPAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.config = schema.AuthenticationBackendConfiguration{}
suite.config.LDAP = &schema.LDAPAuthenticationBackendConfiguration{}
suite.config.LDAP.Implementation = schema.LDAPImplementationCustom
suite.config.LDAP.URL = testLDAPURL
suite.config.LDAP.User = testLDAPUser
suite.config.LDAP.Password = testLDAPPassword
suite.config.LDAP.BaseDN = testLDAPBaseDN
suite.config.LDAP.UsernameAttribute = "uid"
suite.config.LDAP.UsersFilter = "({username_attribute}={input})"
suite.config.LDAP.GroupsFilter = "(cn={input})"
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() {
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordConfig.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordConfig.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordConfig.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordConfig.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordConfig.Parallelism, suite.config.File.Password.Parallelism)
}
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenResetURLIsInvalid() {
@ -270,6 +539,37 @@ func (suite *FileBasedAuthenticationBackend) TestShouldConfigureDisableResetPass
suite.Assert().False(suite.config.PasswordReset.Disable)
}
func TestFileBasedAuthenticationBackend(t *testing.T) {
suite.Run(t, new(FileBasedAuthenticationBackend))
}
type LDAPAuthenticationBackendSuite struct {
suite.Suite
config schema.AuthenticationBackend
validator *schema.StructValidator
}
func (suite *LDAPAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.config = schema.AuthenticationBackend{}
suite.config.LDAP = &schema.LDAPAuthenticationBackend{}
suite.config.LDAP.Implementation = schema.LDAPImplementationCustom
suite.config.LDAP.URL = testLDAPURL
suite.config.LDAP.User = testLDAPUser
suite.config.LDAP.Password = testLDAPPassword
suite.config.LDAP.BaseDN = testLDAPBaseDN
suite.config.LDAP.UsernameAttribute = "uid"
suite.config.LDAP.UsersFilter = "({username_attribute}={input})"
suite.config.LDAP.GroupsFilter = "(cn={input})"
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() {
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() {
suite.config.LDAP.Implementation = ""
suite.config.LDAP.UsernameAttribute = ""
@ -277,7 +577,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementa
suite.Assert().Equal(schema.LDAPImplementationCustom, suite.config.LDAP.Implementation)
suite.Assert().Equal(suite.config.LDAP.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute)
suite.Assert().Equal(suite.config.LDAP.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.UsernameAttribute)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
}
@ -490,7 +790,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultTLSMinimumVersi
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion, suite.config.LDAP.TLS.MinimumVersion)
suite.Assert().Equal(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS.MinimumVersion, suite.config.LDAP.TLS.MinimumVersion)
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldNotAllowInvalidTLSValue() {
@ -512,20 +812,20 @@ func TestLdapAuthenticationBackend(t *testing.T) {
type ActiveDirectoryAuthenticationBackendSuite struct {
suite.Suite
config schema.AuthenticationBackendConfiguration
config schema.AuthenticationBackend
validator *schema.StructValidator
}
func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.config = schema.AuthenticationBackendConfiguration{}
suite.config.LDAP = &schema.LDAPAuthenticationBackendConfiguration{}
suite.config = schema.AuthenticationBackend{}
suite.config.LDAP = &schema.LDAPAuthenticationBackend{}
suite.config.LDAP.Implementation = schema.LDAPImplementationActiveDirectory
suite.config.LDAP.URL = testLDAPURL
suite.config.LDAP.User = testLDAPUser
suite.config.LDAP.Password = testLDAPPassword
suite.config.LDAP.BaseDN = testLDAPBaseDN
suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS
}
func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirectoryDefaults() {
@ -535,25 +835,25 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirec
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter,
suite.config.LDAP.UsersFilter)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsernameAttribute,
suite.config.LDAP.UsernameAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.DisplayNameAttribute,
suite.config.LDAP.DisplayNameAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.MailAttribute,
suite.config.LDAP.MailAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupsFilter,
suite.config.LDAP.GroupsFilter)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupNameAttribute,
suite.config.LDAP.GroupNameAttribute)
}
@ -569,25 +869,25 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefault
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter,
suite.config.LDAP.UsersFilter)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsernameAttribute,
suite.config.LDAP.UsernameAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.DisplayNameAttribute,
suite.config.LDAP.DisplayNameAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.MailAttribute,
suite.config.LDAP.MailAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupsFilter,
suite.config.LDAP.GroupsFilter)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute,
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupNameAttribute,
suite.config.LDAP.GroupNameAttribute)
}

View File

@ -18,7 +18,7 @@ func newDefaultConfig() schema.Configuration {
config.Log.Level = "info"
config.Log.Format = "text"
config.JWTSecret = testJWTSecret
config.AuthenticationBackend.File = &schema.FileAuthenticationBackendConfiguration{
config.AuthenticationBackend.File = &schema.FileAuthenticationBackend{
Path: "/a/path",
}
config.AccessControl = schema.AccessControlConfiguration{

View File

@ -21,10 +21,24 @@ const (
policyDeny = "deny"
)
const (
digestSHA1 = "sha1"
digestSHA224 = "sha224"
digestSHA256 = "sha256"
digestSHA384 = "sha384"
digestSHA512 = "sha512"
)
// Hashing constants.
const (
hashArgon2id = "argon2id"
hashSHA512 = "sha512"
hashLegacyArgon2id = "argon2id"
hashLegacySHA512 = digestSHA512
hashArgon2 = "argon2"
hashSHA2Crypt = "sha2crypt"
hashPBKDF2 = "pbkdf2"
hashSCrypt = "scrypt"
hashBCrypt = "bcrypt"
)
// Scheme constants.
@ -35,18 +49,6 @@ const (
schemeHTTPS = "https"
)
// Test constants.
const (
testInvalidPolicy = "invalid"
testJWTSecret = "a_secret"
testLDAPBaseDN = "base_dn"
testLDAPPassword = "password"
testLDAPURL = "ldap://ldap"
testLDAPUser = "user"
testModeDisabled = "disable"
testEncryptionKey = "a_not_so_secure_encryption_key"
)
// Notifier Error constants.
const (
errFmtNotifierMultipleConfigured = "notifier: please ensure only one of the 'smtp' or 'filesystem' notifier is configured"
@ -59,6 +61,10 @@ const (
errFmtNotifierStartTlsDisabled = "Notifier SMTP connection has opportunistic STARTTLS explicitly disabled which means all emails will be sent insecurely over plain text and this setting is only necessary for non-compliant SMTP servers which advertise they support STARTTLS when they actually don't support STARTTLS"
)
const (
errSuffixMustBeOneOf = "is configured as '%s' but must be one of the following values: '%s'"
)
// Authentication Backend Error constants.
const (
errFmtAuthBackendNotConfigured = "authentication_backend: you must ensure either the 'file' or 'ldap' " +
@ -71,19 +77,16 @@ const (
" configured to '%s' which has the scheme '%s' but the scheme must be either 'http' or 'https'"
errFmtFileAuthBackendPathNotConfigured = "authentication_backend: file: option 'path' is required"
errFmtFileAuthBackendPasswordSaltLength = "authentication_backend: file: password: option 'salt_length' " +
"must be 2 or more but it is configured a '%d'"
errFmtFileAuthBackendPasswordUnknownAlg = "authentication_backend: file: password: option 'algorithm' " +
"must be either 'argon2id' or 'sha512' but it is configured as '%s'"
errFmtFileAuthBackendPasswordInvalidIterations = "authentication_backend: file: password: option " +
"'iterations' must be 1 or more but it is configured as '%d'"
errFmtFileAuthBackendPasswordArgon2idInvalidKeyLength = "authentication_backend: file: password: option " +
"'key_length' must be 16 or more when using algorithm 'argon2id' but it is configured as '%d'"
errFmtFileAuthBackendPasswordArgon2idInvalidParallelism = "authentication_backend: file: password: option " +
"'parallelism' must be 1 or more when using algorithm 'argon2id' but it is configured as '%d'"
errFmtFileAuthBackendPasswordArgon2idInvalidMemory = "authentication_backend: file: password: option 'memory' " +
"must at least be parallelism multiplied by 8 when using algorithm 'argon2id' " +
"with parallelism %d it should be at least %d but it is configured as '%d'"
errSuffixMustBeOneOf
errFmtFileAuthBackendPasswordInvalidVariant = "authentication_backend: file: password: %s: " +
"option 'variant' " + errSuffixMustBeOneOf
errFmtFileAuthBackendPasswordOptionTooLarge = "authentication_backend: file: password: %s: " +
"option '%s' is configured as '%d' but must be less than or equal to '%d'"
errFmtFileAuthBackendPasswordOptionTooSmall = "authentication_backend: file: password: %s: " +
"option '%s' is configured as '%d' but must be greater than or equal to '%d'"
errFmtFileAuthBackendPasswordArgon2MemoryTooLow = "authentication_backend: file: password: argon2: " +
"option 'memory' is configured as '%d' but must be greater than or equal to '%d' or '%d' (the value of 'parallelism) multiplied by '%d'"
errFmtLDAPAuthBackendUnauthenticatedBindWithPassword = "authentication_backend: ldap: option 'permit_unauthenticated_bind' can't be enabled when a password is specified"
errFmtLDAPAuthBackendUnauthenticatedBindWithResetEnabled = "authentication_backend: ldap: option 'permit_unauthenticated_bind' can't be enabled when password reset is enabled"
@ -92,7 +95,7 @@ const (
errFmtLDAPAuthBackendTLSMinVersion = "authentication_backend: ldap: tls: option " +
"'minimum_tls_version' is invalid: %s: %w"
errFmtLDAPAuthBackendImplementation = "authentication_backend: ldap: option 'implementation' " +
"is configured as '%s' but must be one of the following values: '%s'"
errSuffixMustBeOneOf
errFmtLDAPAuthBackendFilterReplacedPlaceholders = "authentication_backend: ldap: option " +
"'%s' has an invalid placeholder: '%s' has been removed, please use '%s' instead"
errFmtLDAPAuthBackendURLNotParsable = "authentication_backend: ldap: option " +
@ -288,7 +291,17 @@ const (
errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password"
)
var validStoragePostgreSQLSSLModes = []string{testModeDisabled, "require", "verify-ca", "verify-full"}
var validArgon2Variants = []string{"argon2id", "id", "argon2i", "i", "argon2d", "d"}
var validSHA2CryptVariants = []string{digestSHA256, digestSHA512}
var validPBKDF2Variants = []string{digestSHA1, digestSHA224, digestSHA256, digestSHA384, digestSHA512}
var validBCryptVariants = []string{"standard", digestSHA256}
var validHashAlgorithms = []string{hashSHA2Crypt, hashPBKDF2, hashSCrypt, hashBCrypt, hashArgon2}
var validStoragePostgreSQLSSLModes = []string{"disable", "require", "verify-ca", "verify-full"}
var validThemeNames = []string{"light", "dark", "grey", "auto"}

View File

@ -0,0 +1,12 @@
package validator
// Test constants.
const (
testInvalid = "invalid"
testJWTSecret = "a_secret"
testLDAPBaseDN = "base_dn"
testLDAPPassword = "password"
testLDAPURL = "ldap://ldap"
testLDAPUser = "user"
testEncryptionKey = "a_not_so_secure_encryption_key"
)

View File

@ -88,7 +88,7 @@ func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) {
OIDC: &schema.OpenIDConnectConfiguration{
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
IssuerPrivateKey: &rsa.PrivateKey{},
EnforcePKCE: "invalid",
EnforcePKCE: testInvalid,
},
}

View File

@ -29,7 +29,7 @@ func (suite *Theme) TestShouldValidateCompleteConfiguration() {
}
func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() {
suite.config.Theme = "invalid"
suite.config.Theme = testInvalid
ValidateTheme(suite.config, suite.validator)

View File

@ -30,7 +30,7 @@ func TestValidateTOTP(t *testing.T) {
{
desc: "ShouldNormalizeTOTPAlgorithm",
have: schema.TOTPConfiguration{
Algorithm: "sha1",
Algorithm: digestSHA1,
Digits: 6,
Period: 30,
SecretSize: 32,

View File

@ -373,7 +373,7 @@ func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *ur
return nil
}
func getProfileRefreshSettings(cfg schema.AuthenticationBackendConfiguration) (refresh bool, refreshInterval time.Duration) {
func getProfileRefreshSettings(cfg schema.AuthenticationBackend) (refresh bool, refreshInterval time.Duration) {
if cfg.LDAP != nil {
if cfg.RefreshInterval == schema.ProfileRefreshDisabled {
refresh = false
@ -433,7 +433,7 @@ func verifyAuth(ctx *middlewares.AutheliaCtx, targetURL *url.URL, refreshProfile
}
// VerifyGET returns the handler verifying if a request is allowed to go through.
func VerifyGET(cfg schema.AuthenticationBackendConfiguration) middlewares.RequestHandler {
func VerifyGET(cfg schema.AuthenticationBackend) middlewares.RequestHandler {
refreshProfile, refreshProfileInterval := getProfileRefreshSettings(cfg)
return func(ctx *middlewares.AutheliaCtx) {

View File

@ -22,9 +22,9 @@ import (
"github.com/authelia/authelia/v4/internal/utils"
)
var verifyGetCfg = schema.AuthenticationBackendConfiguration{
var verifyGetCfg = schema.AuthenticationBackend{
RefreshInterval: schema.RefreshIntervalDefault,
LDAP: &schema.LDAPAuthenticationBackendConfiguration{},
LDAP: &schema.LDAPAuthenticationBackend{},
}
func TestShouldRaiseWhenTargetUrlIsMalformed(t *testing.T) {

View File

@ -73,16 +73,16 @@ func createMIMEBytes(include8bit, crlf bool, lines, length int) []byte {
for j := 0; j < length/100; j++ {
switch {
case include8bit:
buf.Write(utils.RandomBytes(50, utils.AlphaNumericCharacters, false))
buf.Write(utils.RandomBytes(50, utils.CharSetAlphaNumeric, false))
buf.Write([]byte("£"))
buf.Write(utils.RandomBytes(49, utils.AlphaNumericCharacters, false))
buf.Write(utils.RandomBytes(49, utils.CharSetAlphabetic, false))
default:
buf.Write(utils.RandomBytes(100, utils.AlphaNumericCharacters, false))
buf.Write(utils.RandomBytes(100, utils.CharSetAlphaNumeric, false))
}
}
if n := length % 100; n != 0 {
buf.Write(utils.RandomBytes(n, utils.AlphaNumericCharacters, false))
buf.Write(utils.RandomBytes(n, utils.CharSetAlphaNumeric, false))
}
switch {

View File

@ -64,7 +64,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
}
baseURL := scheme + "://" + string(ctx.XForwardedHost()) + base + "/"
nonce := utils.RandomString(32, utils.AlphaNumericCharacters, true)
nonce := utils.RandomString(32, utils.CharSetAlphaNumeric, true)
switch extension := filepath.Ext(file); extension {
case ".html":

View File

@ -10,7 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/suite"
"gopkg.in/yaml.v3"
yaml "gopkg.in/yaml.v3"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/storage"
@ -84,16 +84,180 @@ func (s *CLISuite) TestShouldFailValidateConfig() {
s.Assert().Contains(output, "failed to load configuration from yaml file(/config/invalid.yml) source: open /config/invalid.yml: no such file or directory")
}
func (s *CLISuite) TestShouldHashPasswordArgon2id() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-m", "32", "-s", "test1234"})
func (s *CLISuite) TestShouldHashPasswordArgon2idLegacy() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-m", "32"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Password hash: $argon2id$v=19$m=32768,t=3,p=4$")
s.Assert().Contains(output, "Digest: $argon2id$v=19$m=32768,t=3,p=4$")
}
func (s *CLISuite) TestShouldHashPasswordSHA512() {
func (s *CLISuite) TestShouldHashPasswordSHA512Legacy() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-z"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Password hash: $6$rounds=50000")
s.Assert().Contains(output, "Digest: $6$rounds=50000")
}
func (s *CLISuite) TestShouldHashPasswordArgon2() {
var (
output string
err error
)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--password=apple123", "-m=32768"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $argon2id$v=19$m=32768,t=3,p=4$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--password=apple123", "-m", "32768", "-v=argon2i"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $argon2i$v=19$m=32768,t=3,p=4$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--password=apple123", "-m=32768", "-v=argon2d"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $argon2d$v=19$m=32768,t=3,p=4$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--random", "-m=32"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Random Password: ")
s.Assert().Contains(output, "Digest: $argon2id$v=19$m=32,t=3,p=4$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--password=apple123", "-p=1"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $argon2id$v=19$m=65536,t=3,p=1$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--password=apple123", "-i=1"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $argon2id$v=19$m=65536,t=1,p=4$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--password=apple123", "-s=64"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $argon2id$v=19$m=65536,t=3,p=4$")
s.Assert().GreaterOrEqual(len(output), 169)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "argon2", "--password=apple123", "-k=128"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $argon2id$v=19$m=65536,t=3,p=4$")
s.Assert().GreaterOrEqual(len(output), 233)
}
func (s *CLISuite) TestShouldHashPasswordSHA2Crypt() {
var (
output string
err error
)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "sha2crypt", "--password=apple123", "-v=sha256"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $5$rounds=50000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "sha2crypt", "--password=apple123", "-v=sha512"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $6$rounds=50000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "sha2crypt", "--random", "-s=8"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $6$rounds=50000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "sha2crypt", "--password=apple123", "-i=10000"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $6$rounds=10000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "sha2crypt", "--password=apple123", "-s=20"})
s.Assert().NotNil(err)
s.Assert().Contains(output, "Error: errors occurred validating the password configuration: authentication_backend: file: password: sha2crypt: option 'salt_length' is configured as '20' but must be less than or equal to '16'")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "sha2crypt", "--password=apple123", "-i=20"})
s.Assert().NotNil(err)
s.Assert().Contains(output, "Error: errors occurred validating the password configuration: authentication_backend: file: password: sha2crypt: option 'iterations' is configured as '20' but must be greater than or equal to '1000'")
}
func (s *CLISuite) TestShouldHashPasswordSHA2CryptSHA512() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "sha2crypt", "--password=apple123", "-v=sha512"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $6$rounds=50000$")
}
func (s *CLISuite) TestShouldHashPasswordPBKDF2() {
var (
output string
err error
)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "pbkdf2", "--password=apple123", "-v=sha1"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $pbkdf2$310000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "pbkdf2", "--random", "-v=sha256", "-i=100000"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Random Password: ")
s.Assert().Contains(output, "Digest: $pbkdf2-sha256$100000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "pbkdf2", "--password=apple123", "-v=sha512", "-i=100000"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $pbkdf2-sha512$100000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "pbkdf2", "--password=apple123", "-v=sha224", "-i=100000"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $pbkdf2-sha224$100000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "pbkdf2", "--password=apple123", "-v=sha384", "-i=100000"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $pbkdf2-sha384$100000$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "pbkdf2", "--password=apple123", "-s=32", "-i=100000"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $pbkdf2-sha512$100000$")
}
func (s *CLISuite) TestShouldHashPasswordBCrypt() {
var (
output string
err error
)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "bcrypt", "--password=apple123"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $2b$12$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "bcrypt", "--random", "-i=10"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Random Password: ")
s.Assert().Contains(output, "Digest: $2b$10$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "bcrypt", "--password=apple123", "-v=sha256"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $bcrypt-sha256$v=2,t=2b,r=12$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "bcrypt", "--random", "-v=sha256", "-i=10"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Random Password: ")
s.Assert().Contains(output, "Digest: $bcrypt-sha256$v=2,t=2b,r=10$")
}
func (s *CLISuite) TestShouldHashPasswordSCrypt() {
var (
output string
err error
)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "scrypt", "--password=apple123"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $scrypt$ln=16,r=8,p=1$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "scrypt", "--random"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Random Password: ")
s.Assert().Contains(output, "Digest: $scrypt$ln=16,r=8,p=1$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "scrypt", "--password=apple123", "-i=1"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $scrypt$ln=1,r=8,p=1$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "scrypt", "--password=apple123", "-i=1", "-p=2"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $scrypt$ln=1,r=8,p=2$")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "crypto", "hash", "generate", "scrypt", "--password=apple123", "-i=1", "-r=2"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Digest: $scrypt$ln=1,r=2,p=1$")
}
func (s *CLISuite) TestShouldGenerateRSACertificateRequest() {

View File

@ -110,9 +110,24 @@ const (
HoursInYear = HoursInDay * 365
)
var (
// AlphaNumericCharacters are literally just valid alphanumeric chars.
AlphaNumericCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const (
// CharSetAlphabetic are literally just valid alphabetic printable ASCII chars.
CharSetAlphabetic = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// CharSetNumeric are literally just valid numeric chars.
CharSetNumeric = "0123456789"
// CharSetNumericHex are literally just valid hexadecimal printable ASCII chars.
CharSetNumericHex = CharSetNumeric + "ABCDEF"
// CharSetSymbolic are literally just valid symbolic printable ASCII chars.
CharSetSymbolic = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
// CharSetAlphaNumeric are literally just valid alphanumeric printable ASCII chars.
CharSetAlphaNumeric = CharSetAlphabetic + CharSetNumeric
// CharSetASCII are literally just valid printable ASCII chars.
CharSetASCII = CharSetAlphabetic + CharSetNumeric + CharSetSymbolic
)
var htmlEscaper = strings.NewReplacer(

View File

@ -22,7 +22,7 @@ func TestShouldHashString(t *testing.T) {
assert.Equal(t, "ae448ac86c4e8e4dec645729708ef41873ae79c6dff84eff73360989487f08e5", anotherSum)
assert.NotEqual(t, sum, anotherSum)
randomInput := RandomString(40, AlphaNumericCharacters, false)
randomInput := RandomString(40, CharSetAlphaNumeric, false)
randomSum := HashSHA256FromString(randomInput)
assert.NotEqual(t, randomSum, sum)
@ -39,7 +39,7 @@ func TestShouldHashPath(t *testing.T) {
err = os.WriteFile(filepath.Join(dir, "anotherfile"), []byte("another\n"), 0600)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "randomfile"), []byte(RandomString(40, AlphaNumericCharacters, true)+"\n"), 0600)
err = os.WriteFile(filepath.Join(dir, "randomfile"), []byte(RandomString(40, CharSetAlphaNumeric, true)+"\n"), 0600)
assert.NoError(t, err)
sum, err := HashSHA256FromPath(filepath.Join(dir, "myfile"))

View File

@ -54,11 +54,11 @@ func TestStringJoinDelimitedEscaped(t *testing.T) {
}
func TestShouldNotGenerateSameRandomString(t *testing.T) {
randomStringOne := RandomString(10, AlphaNumericCharacters, false)
randomStringTwo := RandomString(10, AlphaNumericCharacters, false)
randomStringOne := RandomString(10, CharSetAlphaNumeric, false)
randomStringTwo := RandomString(10, CharSetAlphaNumeric, false)
randomCryptoStringOne := RandomString(10, AlphaNumericCharacters, true)
randomCryptoStringTwo := RandomString(10, AlphaNumericCharacters, true)
randomCryptoStringOne := RandomString(10, CharSetAlphaNumeric, true)
randomCryptoStringTwo := RandomString(10, CharSetAlphaNumeric, true)
assert.NotEqual(t, randomStringOne, randomStringTwo)
assert.NotEqual(t, randomCryptoStringOne, randomCryptoStringTwo)