diff --git a/cmd/authelia-scripts/cmd/gen.go b/cmd/authelia-scripts/cmd/gen.go index f20f2dddc..16fa23923 100644 --- a/cmd/authelia-scripts/cmd/gen.go +++ b/cmd/authelia-scripts/cmd/gen.go @@ -7,5 +7,5 @@ package cmd const ( - versionSwaggerUI = "4.14.2" + versionSwaggerUI = "4.14.3" ) diff --git a/config.template.yml b/config.template.yml index ef0a3bd35..fee3739eb 100644 --- a/config.template.yml +++ b/config.template.yml @@ -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. diff --git a/docs/content/en/configuration/first-factor/file.md b/docs/content/en/configuration/first-factor/file.md index 786db99a4..d8c474b7b 100644 --- a/docs/content/en/configuration/first-factor/file.md +++ b/docs/content/en/configuration/first-factor/file.md @@ -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 diff --git a/docs/content/en/integration/proxies/nginx-proxy-manager/index.md b/docs/content/en/integration/proxies/nginx-proxy-manager/index.md index 32246329b..e0bdf2ceb 100644 --- a/docs/content/en/integration/proxies/nginx-proxy-manager/index.md +++ b/docs/content/en/integration/proxies/nginx-proxy-manager/index.md @@ -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: diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto.md b/docs/content/en/reference/cli/authelia/authelia_crypto.md index 8ce6ba2b0..f979b583f 100644 --- a/docs/content/en/reference/cli/authelia/authelia_crypto.md +++ b/docs/content/en/reference/cli/authelia/authelia_crypto.md @@ -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 diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash.md new file mode 100644 index 000000000..b03de6d36 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate.md new file mode 100644 index 000000000..b9753f1a3 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_argon2.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_argon2.md new file mode 100644 index 000000000..3f5b8f17a --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_argon2.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_bcrypt.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_bcrypt.md new file mode 100644 index 000000000..f2c0685e6 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_bcrypt.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_pbkdf2.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_pbkdf2.md new file mode 100644 index 000000000..b42daf681 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_pbkdf2.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_scrypt.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_scrypt.md new file mode 100644 index 000000000..d727d8d5d --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_scrypt.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_sha2crypt.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_sha2crypt.md new file mode 100644 index 000000000..2537ea192 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_generate_sha2crypt.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_hash_validate.md b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_validate.md new file mode 100644 index 000000000..a70bb80d8 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_hash_validate.md @@ -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] -- +``` + +### 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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_crypto_rand.md b/docs/content/en/reference/cli/authelia/authelia_crypto_rand.md new file mode 100644 index 000000000..764bfefd2 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_crypto_rand.md @@ -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 + diff --git a/docs/content/en/reference/cli/authelia/authelia_hash-password.md b/docs/content/en/reference/cli/authelia/authelia_hash-password.md index e0fc9c883..7c7f97048 100644 --- a/docs/content/en/reference/cli/authelia/authelia_hash-password.md +++ b/docs/content/en/reference/cli/authelia/authelia_hash-password.md @@ -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] -- +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) ``` diff --git a/docs/content/en/reference/guides/passwords.md b/docs/content/en/reference/guides/passwords.md index 87a56bd85..1e2fd4963 100644 --- a/docs/content/en/reference/guides/passwords.md +++ b/docs/content/en/reference/guides/passwords.md @@ -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 diff --git a/docs/data/configkeys.json b/docs/data/configkeys.json index c3bb2436f..015075211 100644 --- a/docs/data/configkeys.json +++ b/docs/data/configkeys.json @@ -1 +1 @@ -[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.endpoints","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ENDPOINTS"},{"path":"identity_providers.oidc.cors.allowed_origins","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc.clients","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me_duration","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME_DURATION"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.nodes","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_NODES"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"access_control.networks","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_NETWORKS"},{"path":"access_control.rules","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_RULES"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENABLE_PPROF"},{"path":"server.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENABLE_EXPVARS"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.tls.client_certificates","secret":false,"env":"AUTHELIA_SERVER_TLS_CLIENT_CERTIFICATES"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"}] \ No newline at end of file +[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.endpoints","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ENDPOINTS"},{"path":"identity_providers.oidc.cors.allowed_origins","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc.clients","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CLIENTS"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me_duration","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME_DURATION"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.nodes","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_NODES"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"access_control.networks","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_NETWORKS"},{"path":"access_control.rules","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_RULES"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENABLE_PPROF"},{"path":"server.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENABLE_EXPVARS"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.tls.client_certificates","secret":false,"env":"AUTHELIA_SERVER_TLS_CLIENT_CERTIFICATES"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"}] \ No newline at end of file diff --git a/go.mod b/go.mod index e8038dc4c..d21b81f6f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b130a365b..bfbd3e379 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/authentication/const.go b/internal/authentication/const.go index 61a5d246a..201f3fff2 100644 --- a/internal/authentication/const.go +++ b/internal/authentication/const.go @@ -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. diff --git a/internal/authentication/file_user_provider.go b/internal/authentication/file_user_provider.go index c4ea7af4e..6c81139e1 100644 --- a/internal/authentication/file_user_provider.go +++ b/internal/authentication/file_user_provider.go @@ -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 diff --git a/internal/authentication/file_user_provider_database.go b/internal/authentication/file_user_provider_database.go new file mode 100644 index 000000000..6c415ca17 --- /dev/null +++ b/internal/authentication/file_user_provider_database.go @@ -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 +} diff --git a/internal/authentication/file_user_provider_test.go b/internal/authentication/file_user_provider_test.go index 01c6baa87..4461d7f78 100644 --- a/internal/authentication/file_user_provider_test.go +++ b/internal/authentication/file_user_provider_test.go @@ -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: diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go index 0c4b2ed9f..46db74610 100644 --- a/internal/authentication/ldap_user_provider.go +++ b/internal/authentication/ldap_user_provider.go @@ -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 { diff --git a/internal/authentication/ldap_user_provider_startup.go b/internal/authentication/ldap_user_provider_startup.go index 723d4cb5c..2956fd42e 100644 --- a/internal/authentication/ldap_user_provider_startup.go +++ b/internal/authentication/ldap_user_provider_startup.go @@ -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 diff --git a/internal/authentication/ldap_user_provider_test.go b/internal/authentication/ldap_user_provider_test.go index aeaf5b835..a74e24477 100644 --- a/internal/authentication/ldap_user_provider_test.go +++ b/internal/authentication/ldap_user_provider_test.go @@ -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", diff --git a/internal/authentication/password_hash.go b/internal/authentication/password_hash.go deleted file mode 100644 index 31f78366f..000000000 --- a/internal/authentication/password_hash.go +++ /dev/null @@ -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 -} diff --git a/internal/authentication/password_hash_test.go b/internal/authentication/password_hash_test.go deleted file mode 100644 index 3cd3ade7a..000000000 --- a/internal/authentication/password_hash_test.go +++ /dev/null @@ -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) -} diff --git a/internal/commands/acl.go b/internal/commands/acl.go index 0b0851b55..ad6e7d3fc 100644 --- a/internal/commands/acl.go +++ b/internal/commands/acl.go @@ -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 } diff --git a/internal/commands/configuration.go b/internal/commands/configuration.go index fb65fea69..3bbaad050 100644 --- a/internal/commands/configuration.go +++ b/internal/commands/configuration.go @@ -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) } diff --git a/internal/commands/const.go b/internal/commands/const.go index ad9cc1879..b4fb7986b 100644 --- a/internal/commands/const.go +++ b/internal/commands/const.go @@ -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] -- " cmdUseRequest = "request" cmdUsePair = "pair" cmdUseRSA = "rsa" @@ -459,6 +538,8 @@ const ( const ( cryptoCertPubCertOut = "certificate" cryptoCertCSROut = "certificate signing request" + + prefixFilePassword = "authentication_backend.file.password" ) var ( diff --git a/internal/commands/crypto.go b/internal/commands/crypto.go index 0ffd245f1..60b33948b 100644 --- a/internal/commands/crypto.go +++ b/internal/commands/crypto.go @@ -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: diff --git a/internal/commands/crypto_hash.go b/internal/commands/crypto_hash.go new file mode 100644 index 000000000..72eb8f7d9 --- /dev/null +++ b/internal/commands/crypto_hash.go @@ -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") +} diff --git a/internal/commands/crypto_helper.go b/internal/commands/crypto_helper.go index 1a097f37c..d85c70798 100644 --- a/internal/commands/crypto_helper.go +++ b/internal/commands/crypto_helper.go @@ -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" diff --git a/internal/commands/hash.go b/internal/commands/hash.go deleted file mode 100644 index 33608c67f..000000000 --- a/internal/commands/hash.go +++ /dev/null @@ -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] -- ", - 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 -} diff --git a/internal/commands/root.go b/internal/commands/root.go index c532d562e..282c1df03 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -42,12 +42,12 @@ func NewRootCmd() (cmd *cobra.Command) { cmdWithConfigFlags(cmd, false, []string{}) cmd.AddCommand( + newAccessControlCommand(), newBuildInfoCmd(), newCryptoCmd(), newHashPasswordCmd(), newStorageCmd(), newValidateConfigCmd(), - newAccessControlCommand(), ) return cmd diff --git a/internal/commands/storage_run.go b/internal/commands/storage_run.go index b911fd64f..50b40a876 100644 --- a/internal/commands/storage_run.go +++ b/internal/commands/storage_run.go @@ -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) { diff --git a/internal/commands/util.go b/internal/commands/util.go index ef83e1627..96043bfba 100644 --- a/internal/commands/util.go +++ b/internal/commands/util.go @@ -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 +} diff --git a/internal/commands/validate.go b/internal/commands/validate.go index 8a8cbf4b9..b55377373 100644 --- a/internal/commands/validate.go +++ b/internal/commands/validate.go @@ -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 } diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index ef0a3bd35..fee3739eb 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -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. diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go index 9ed48bf07..1b286a3db 100644 --- a/internal/configuration/schema/authentication.go +++ b/internal/configuration/schema/authentication.go @@ -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", + }, } diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go index 27ed0f142..3dc891d9a 100644 --- a/internal/configuration/schema/configuration.go +++ b/internal/configuration/schema/configuration.go @@ -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"` } diff --git a/internal/configuration/schema/const.go b/internal/configuration/schema/const.go index c43eefb9d..23491b648 100644 --- a/internal/configuration/schema/const.go +++ b/internal/configuration/schema/const.go @@ -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" diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index d3cbdc8d6..2d64f1f7d 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -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", diff --git a/internal/configuration/validator/access_control_test.go b/internal/configuration/validator/access_control_test.go index 5af97a0a9..42ac82258 100644 --- a/internal/configuration/validator/access_control_test.go +++ b/internal/configuration/validator/access_control_test.go @@ -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, diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index 59ea0518a..280d383ec 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -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 - } -} diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index 9df81a38b..328e90590 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -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) } diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go index 4021c1f33..e212fbf95 100644 --- a/internal/configuration/validator/configuration_test.go +++ b/internal/configuration/validator/configuration_test.go @@ -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{ diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 5865968b4..448e02c23 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -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"} diff --git a/internal/configuration/validator/const_test.go b/internal/configuration/validator/const_test.go new file mode 100644 index 000000000..4fe7d56f0 --- /dev/null +++ b/internal/configuration/validator/const_test.go @@ -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" +) diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index e087c589b..e8f7d0404 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -88,7 +88,7 @@ func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) { OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: &rsa.PrivateKey{}, - EnforcePKCE: "invalid", + EnforcePKCE: testInvalid, }, } diff --git a/internal/configuration/validator/theme_test.go b/internal/configuration/validator/theme_test.go index 1b29d0e38..abe796611 100644 --- a/internal/configuration/validator/theme_test.go +++ b/internal/configuration/validator/theme_test.go @@ -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) diff --git a/internal/configuration/validator/totp_test.go b/internal/configuration/validator/totp_test.go index 85cd7471f..956f074c4 100644 --- a/internal/configuration/validator/totp_test.go +++ b/internal/configuration/validator/totp_test.go @@ -30,7 +30,7 @@ func TestValidateTOTP(t *testing.T) { { desc: "ShouldNormalizeTOTPAlgorithm", have: schema.TOTPConfiguration{ - Algorithm: "sha1", + Algorithm: digestSHA1, Digits: 6, Period: 30, SecretSize: 32, diff --git a/internal/handlers/handler_verify.go b/internal/handlers/handler_verify.go index eacd23b43..883852da7 100644 --- a/internal/handlers/handler_verify.go +++ b/internal/handlers/handler_verify.go @@ -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) { diff --git a/internal/handlers/handler_verify_test.go b/internal/handlers/handler_verify_test.go index ae2cc9d25..304339ed6 100644 --- a/internal/handlers/handler_verify_test.go +++ b/internal/handlers/handler_verify_test.go @@ -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) { diff --git a/internal/notification/smtp_util_test.go b/internal/notification/smtp_util_test.go index 62c33fd09..39c2d6617 100644 --- a/internal/notification/smtp_util_test.go +++ b/internal/notification/smtp_util_test.go @@ -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 { diff --git a/internal/server/template.go b/internal/server/template.go index fedf2df57..3f1d914f9 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -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": diff --git a/internal/suites/suite_cli_test.go b/internal/suites/suite_cli_test.go index 0fd51c2cc..0357b89b1 100644 --- a/internal/suites/suite_cli_test.go +++ b/internal/suites/suite_cli_test.go @@ -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() { diff --git a/internal/utils/const.go b/internal/utils/const.go index d72aa5cd7..d55fab0f8 100644 --- a/internal/utils/const.go +++ b/internal/utils/const.go @@ -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( diff --git a/internal/utils/hashing_test.go b/internal/utils/hashing_test.go index 81ed04299..5a86c83be 100644 --- a/internal/utils/hashing_test.go +++ b/internal/utils/hashing_test.go @@ -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")) diff --git a/internal/utils/strings_test.go b/internal/utils/strings_test.go index 0e2b92651..c086dd0ae 100644 --- a/internal/utils/strings_test.go +++ b/internal/utils/strings_test.go @@ -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)