feat(authentication): file password algorithms (#3848)
This adds significant enhancements to the file auth provider including multiple additional algorithms.pull/4189/head
parent
8eadf72dc7
commit
3a70f6739b
|
@ -7,5 +7,5 @@
|
|||
package cmd
|
||||
|
||||
const (
|
||||
versionSwaggerUI = "4.14.2"
|
||||
versionSwaggerUI = "4.14.3"
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: "authelia crypto hash validate"
|
||||
description: "Reference for the authelia crypto hash validate command."
|
||||
lead: ""
|
||||
date: 2022-08-27T10:46:58+10:00
|
||||
draft: false
|
||||
images: []
|
||||
menu:
|
||||
reference:
|
||||
parent: "cli-authelia"
|
||||
weight: 330
|
||||
toc: true
|
||||
---
|
||||
|
||||
## authelia crypto hash validate
|
||||
|
||||
Perform cryptographic hash validations
|
||||
|
||||
### Synopsis
|
||||
|
||||
Perform cryptographic hash validations.
|
||||
|
||||
This subcommand allows preforming cryptographic hash validations. i.e. checking hash digests against a password.
|
||||
|
||||
```
|
||||
authelia crypto hash validate [flags] -- <digest>
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
authelia crypto hash validate --help
|
||||
authelia crypto hash validate '$5$rounds=500000$WFjMpdCQxIkbNl0k$M0qZaZoK8Gwdh8Cw5diHgGfe5pE0iJvxcVG3.CVnQe.' -- 'p@ssw0rd'
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for validate
|
||||
--password string manually supply the password rather than using the terminal prompt
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [authelia crypto hash](authelia_crypto_hash.md) - Perform cryptographic hash operations
|
||||
|
|
@ -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
|
||||
|
|
@ -21,7 +21,7 @@ Hash a password to be used in file-based users database
|
|||
Hash a password to be used in file-based users database.
|
||||
|
||||
```
|
||||
authelia hash-password [flags] -- <password>
|
||||
authelia hash-password [flags] -- [password]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
@ -38,13 +38,13 @@ authelia hash-password --key-length=64 -- 'mypass'
|
|||
### Options
|
||||
|
||||
```
|
||||
-c, --config strings Configuration files
|
||||
-c, --config strings configuration files to load (default [configuration.yml])
|
||||
-h, --help help for hash-password
|
||||
-i, --iterations int set the number of hashing iterations (default 3)
|
||||
-k, --key-length int [argon2id] set the key length param (default 32)
|
||||
-m, --memory int [argon2id] set the amount of memory param (in MB) (default 64)
|
||||
-m, --memory int [argon2id] set the amount of memory param (in MB) (default 65536)
|
||||
--no-confirm skip the password confirmation prompt
|
||||
-p, --parallelism int [argon2id] set the parallelism param (default 4)
|
||||
-s, --salt string set the salt string
|
||||
-l, --salt-length int set the auto-generated salt length (default 16)
|
||||
-z, --sha512 use sha512 as the algorithm (changes iterations to 50000, change with -i)
|
||||
```
|
||||
|
|
|
@ -52,25 +52,33 @@ users:
|
|||
|
||||
The file contains hashed passwords instead of plain text passwords for security reasons.
|
||||
|
||||
You can use Authelia binary or docker image to generate the hash of any password. The [hash-password] command has many
|
||||
tunable options, you can view them with the `authelia hash-password --help` command. For example if you wanted to
|
||||
improve the entropy you could generate a 16 byte salt and provide it with the `--salt` flag.
|
||||
You can use Authelia binary or docker image to generate the hash of any password. The [crypt hash generate] command has
|
||||
many supported algorithms. To view them run the `authelia crypto hash generate --help` command. To see the tunable
|
||||
options for an algorithm subcommand include that command before `--help`. For example for the [Argon2] algorithm use the
|
||||
`authelia crypto hash generate argon2 --help` command to see the available options.
|
||||
|
||||
Example: `authelia hash-password --salt abcdefghijklhijl -- 'password'`.
|
||||
|
||||
Passwords passed to [hash-password] should be single quoted if using special characters to prevent parameter
|
||||
substitution. In addition the password should be the last parameter, and should be after a `--`. For instance to
|
||||
generate a hash with the docker image just run:
|
||||
Passwords passed to [crypt hash generate] should be single quoted if using the `--password` parameter instead of the
|
||||
console prompt, especially if it has special characters to prevent parameter substitution. For instance to generate an
|
||||
[Argon2] hash with the docker image just run:
|
||||
|
||||
```bash
|
||||
$ docker run authelia/authelia:latest authelia hash-password -- 'password'
|
||||
Password hash: $argon2id$v=19$m=65536$3oc26byQuSkQqksq$zM1QiTvVPrMfV6BVLs2t4gM+af5IN7euO0VB6+Q8ZFs
|
||||
$ docker run authelia/authelia:latest authelia crypto hash generate argon2 --password 'password'
|
||||
Digest: $argon2id$v=19$m=65536,t=3,p=4$Hjc8e7WYcBFcJmEDUOsS9A$ozM7RyZR1EyDR8cuyVpDDfmLrGPGFgo5E2NNqRumui4
|
||||
```
|
||||
|
||||
You may also use the `--config` flag to point to your existing configuration. When used, the values defined in the
|
||||
config will be used instead.
|
||||
config will be used instead. For example to generate the password with a configuration file named `configuration.yml`
|
||||
in the current directory:
|
||||
|
||||
See the [full CLI reference documentation](../cli/authelia/authelia_hash-password.md).
|
||||
```bash
|
||||
$ docker run -v ./configuration.yml:/configuration.yml -it authelia/authelia:latest authelia crypto hash generate --config /configuration.yml
|
||||
Enter Password:
|
||||
Confirm Password:
|
||||
|
||||
Digest: $argon2id$v=19$m=65536,t=3,p=4$Hjc8e7WYcBFcJmEDUOsS9A$ozM7RyZR1EyDR8cuyVpDDfmLrGPGFgo5E2NNqRumui4
|
||||
```
|
||||
|
||||
See the [full CLI reference documentation](../cli/authelia/authelia_crypto_hash_generate.md).
|
||||
|
||||
### Cost
|
||||
|
||||
|
@ -88,11 +96,11 @@ all algorithms. The main cost type measurements are:
|
|||
* CPU
|
||||
* Memory
|
||||
|
||||
*__Important Note:__ When using algorithms that use a memory cost like [Argon2] it should be noted that this memory is
|
||||
released by Go after the hashing process completes, however the operating system may not reclaim the memory until a
|
||||
later time such as when the system is experiencing memory pressure which may cause the appearance of more memory being
|
||||
in use than Authelia is actually actively using. Authelia will typically reuse this memory if it has not be reclaimed as
|
||||
long as another hashing calculation is not still utilizing it.*
|
||||
*__Important Note:__ When using algorithms that use a memory cost like [Argon2] and [Scrypt] it should be noted that
|
||||
this memory is released by Go after the hashing process completes, however the operating system may not reclaim the
|
||||
memory until a later time such as when the system is experiencing memory pressure which may cause the appearance of more
|
||||
memory being in use than Authelia is actually actively using. Authelia will typically reuse this memory if it has not be
|
||||
reclaimed as long as another hashing calculation is not still utilizing it.*
|
||||
|
||||
To get a rough estimate of how much memory should be utilized with these algorithms you can utilize the following
|
||||
command:
|
||||
|
@ -114,7 +122,7 @@ widely considered to be the best hashing algorithm, and in 2015 won the [Passwor
|
|||
customizable parameters including a memory parameter allowing the [cost](#cost) of computing a hash to scale into the
|
||||
future with better hardware which makes it harder to brute-force.
|
||||
|
||||
For backwards compatibility and user choice support for the [SHA Crypt] algorithm (`SHA512` variant) is still available.
|
||||
For backwards compatibility and user choice support for the [SHA2 Crypt] algorithm (`SHA512` variant) is still available.
|
||||
While it's a reasonable hashing function given high enough iterations, as hardware improves it has a higher chance of
|
||||
being brute-forced since it only allows scaling the CPU [cost](#cost) whereas [Argon2] allows scaling both for CPU and
|
||||
Memory [cost](#cost).
|
||||
|
@ -123,10 +131,21 @@ Memory [cost](#cost).
|
|||
|
||||
The algorithm that a hash is utilizing is identifiable by its prefix:
|
||||
|
||||
| Algorithm | Variant | Prefix |
|
||||
|:-----------:|:--------:|:------------:|
|
||||
| [Argon2] | `id` | `$argon2id$` |
|
||||
| [SHA Crypt] | `SHA512` | `$6$` |
|
||||
| Algorithm | Variant | Prefix |
|
||||
|:------------:|:----------:|:-----------------:|
|
||||
| [Argon2] | `argon2id` | `$argon2id$` |
|
||||
| [Argon2] | `argon2i` | `$argon2i$` |
|
||||
| [Argon2] | `argon2d` | `$argon2d$` |
|
||||
| [Scrypt] | N/A | `$scrypt$` |
|
||||
| [PBKDF2] | `sha1` | `$pbkdf2$` |
|
||||
| [PBKDF2] | `sha224` | `$pbkdf2-sha224$` |
|
||||
| [PBKDF2] | `sha256` | `$pbkdf2-sha256$` |
|
||||
| [PBKDF2] | `sha384` | `$pbkdf2-sha384$` |
|
||||
| [PBKDF2] | `sha512` | `$pbkdf2-sha512$` |
|
||||
| [SHA2 Crypt] | `SHA256` | `$5$` |
|
||||
| [SHA2 Crypt] | `SHA512` | `$6$` |
|
||||
| [Bcrypt] | `standard` | `$2b$` |
|
||||
| [Bcrypt] | `sha256` | `$bcrypt-sha256$` |
|
||||
|
||||
See the [Crypt (C) Wiki page](https://en.wikipedia.org/wiki/Crypt_(C)) for more information.
|
||||
|
||||
|
@ -140,27 +159,44 @@ adequately determine the [cost](#cost).
|
|||
While there are recommended parameters for each algorithm it's your responsibility to tune these individually for your
|
||||
particular system.
|
||||
|
||||
#### Recommended Parameters: Argon2id
|
||||
#### Algorithm Choice
|
||||
|
||||
We generally discourage [Bcrypt] except when needed for interoperability with legacy systems. The `argon2id` variant of
|
||||
the [Argon2] algorithm is the best choice of the algorithms available, but it's important to note that the `argon2id`
|
||||
variant is the most resilient variant, followed by the `argon2d` variant and the `argon2i` variant not being recommended.
|
||||
It's strongly recommended if you're unsure that you use `argon2id`. [Scrypt] is a likely second best algorithm. [PBKDF2]
|
||||
is practically the only choice when it comes to [FIPS-140 compliance]. The `sha512` variant of the [SHA2 Crypt]
|
||||
algorithm is also a reasonable option, but is mainly available for backwards compatability.
|
||||
|
||||
All other algorithms and variants available exist only for interoperability and we discourage their use if a better
|
||||
algorithm is available in your scenario.
|
||||
|
||||
#### Recommended Parameters: Argon2
|
||||
|
||||
This table adapts the [RFC9106 Parameter Choice] recommendations to our configuration options:
|
||||
|
||||
| Situation | Iterations (t) | Parallelism (p) | Memory (m) | Salt Size | Key Size |
|
||||
|:-----------:|:--------------:|:---------------:|:----------:|:---------:|:--------:|
|
||||
| Low Memory | 3 | 4 | 64 | 16 | 32 |
|
||||
| Recommended | 1 | 4 | 2048 | 16 | 32 |
|
||||
| Situation | Variant | Iterations (t) | Parallelism (p) | Memory (m) | Salt Size | Key Size |
|
||||
|:-----------:|:--------:|:--------------:|:---------------:|:----------:|:---------:|:--------:|
|
||||
| Low Memory | argon2id | 3 | 4 | 65536 | 16 | 32 |
|
||||
| Recommended | argon2id | 1 | 4 | 2097152 | 16 | 32 |
|
||||
|
||||
#### Recommended Parameters: SHA512
|
||||
#### Recommended Parameters: SHA2 Crypt
|
||||
|
||||
This table suggests the parameters for the [SHA Crypt] (`SHA512` variant) algorithm:
|
||||
This table suggests the parameters for the [SHA2 Crypt] algorithm:
|
||||
|
||||
| Situation | Iterations (rounds) | Salt Size |
|
||||
|:------------:|:-------------------:|:---------:|
|
||||
| Standard CPU | 50000 | 16 |
|
||||
| High End CPU | 150000 | 16 |
|
||||
| Situation | Variant | Iterations (rounds) | Salt Size |
|
||||
|:------------:|:-------:|:-------------------:|:---------:|
|
||||
| Standard CPU | sha512 | 50000 | 16 |
|
||||
| High End CPU | sha512 | 150000 | 16 |
|
||||
|
||||
[Argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
|
||||
[Scrypt]: https://en.wikipedia.org/wiki/Scrypt
|
||||
[PBKDF2]: https://www.ietf.org/rfc/rfc2898.html
|
||||
[SHA2 Crypt]: https://www.akkadia.org/drepper/SHA-crypt.txt
|
||||
[Bcrypt]: https://en.wikipedia.org/wiki/Bcrypt
|
||||
[FIPS-140 compliance]: https://csrc.nist.gov/publications/detail/fips/140/2/final
|
||||
|
||||
[RFC9106 Parameter Choice]: https://www.rfc-editor.org/rfc/rfc9106.html#section-4
|
||||
[YAML]: https://yaml.org/
|
||||
[Argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
|
||||
[SHA Crypt]: https://www.akkadia.org/drepper/SHA-crypt.txt
|
||||
[hash-password]: ../cli/authelia/authelia_hash-password.md
|
||||
[crypt hash generate]: ../cli/authelia/authelia_crypto_hash_generate.md
|
||||
[Password Hashing Competition]: https://en.wikipedia.org/wiki/Password_Hashing_Competition
|
||||
|
|
File diff suppressed because one or more lines are too long
8
go.mod
8
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
|
||||
|
|
17
go.sum
17
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=
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -310,6 +310,57 @@ This subcommand allows preforming cryptographic certificate, key pair, etc tasks
|
|||
|
||||
cmdAutheliaCryptoExample = `authelia crypto --help`
|
||||
|
||||
cmdAutheliaCryptoRandShort = "Generate a cryptographically secure random string"
|
||||
|
||||
cmdAutheliaCryptoRandLong = `Generate a cryptographically secure random string.
|
||||
|
||||
This subcommand allows generating cryptographically secure random strings for use for encryption keys, HMAC keys, etc.`
|
||||
|
||||
cmdAutheliaCryptoRandExample = `authelia crypto rand --help
|
||||
authelia crypto rand --length 80
|
||||
authelia crypto rand -n 80
|
||||
authelia crypto rand --charset alphanumeric
|
||||
authelia crypto rand --charset alphabetic
|
||||
authelia crypto rand --charset ascii
|
||||
authelia crypto rand --charset numeric
|
||||
authelia crypto rand --charset numeric-hex
|
||||
authelia crypto rand --characters 0123456789ABCDEF`
|
||||
|
||||
cmdAutheliaCryptoHashShort = "Perform cryptographic hash operations"
|
||||
|
||||
cmdAutheliaCryptoHashLong = `Perform cryptographic hash operations.
|
||||
|
||||
This subcommand allows preforming hashing cryptographic tasks.`
|
||||
|
||||
cmdAutheliaCryptoHashExample = `authelia crypto hash --help`
|
||||
|
||||
cmdAutheliaCryptoHashValidateShort = "Perform cryptographic hash validations"
|
||||
|
||||
cmdAutheliaCryptoHashValidateLong = `Perform cryptographic hash validations.
|
||||
|
||||
This subcommand allows preforming cryptographic hash validations. i.e. checking hash digests against a password.`
|
||||
|
||||
cmdAutheliaCryptoHashValidateExample = `authelia crypto hash validate --help
|
||||
authelia crypto hash validate '$5$rounds=500000$WFjMpdCQxIkbNl0k$M0qZaZoK8Gwdh8Cw5diHgGfe5pE0iJvxcVG3.CVnQe.' -- 'p@ssw0rd'`
|
||||
|
||||
cmdAutheliaCryptoHashGenerateShort = "Generate cryptographic hash digests"
|
||||
|
||||
cmdAutheliaCryptoHashGenerateLong = `Generate cryptographic hash digests.
|
||||
|
||||
This subcommand allows generating cryptographic hash digests.
|
||||
|
||||
See the help for the subcommands if you want to override the configuration or defaults.`
|
||||
|
||||
cmdAutheliaCryptoHashGenerateExample = `authelia crypto hash generate --help`
|
||||
|
||||
fmtCmdAutheliaCryptoHashGenerateSubShort = "Generate cryptographic %s hash digests"
|
||||
|
||||
fmtCmdAutheliaCryptoHashGenerateSubLong = `Generate cryptographic %s hash digests.
|
||||
|
||||
This subcommand allows generating cryptographic %s hash digests.`
|
||||
|
||||
fmtCmdAutheliaCryptoHashGenerateSubExample = `authelia crypto hash generate %s --help`
|
||||
|
||||
cmdAutheliaCryptoCertificateShort = "Perform certificate cryptographic operations"
|
||||
|
||||
cmdAutheliaCryptoCertificateLong = `Perform certificate cryptographic operations.
|
||||
|
@ -324,11 +375,7 @@ This subcommand allows preforming certificate cryptographic tasks.`
|
|||
|
||||
This subcommand allows preforming %s certificate cryptographic tasks.`
|
||||
|
||||
cmdAutheliaCryptoCertificateRSAExample = `authelia crypto certificate rsa --help`
|
||||
|
||||
cmdAutheliaCryptoCertificateECDSAExample = `authelia crypto certificate ecdsa --help`
|
||||
|
||||
cmdAutheliaCryptoCertificateEd25519Example = `authelia crypto certificate ed25519 --help`
|
||||
fmtCmdAutheliaCryptoCertificateSubExample = `authelia crypto certificate %s --help`
|
||||
|
||||
fmtCmdAutheliaCryptoCertificateGenerateRequestShort = "Generate an %s private key and %s"
|
||||
|
||||
|
@ -444,11 +491,43 @@ const (
|
|||
cmdFlagNamePKCS8 = "pkcs8"
|
||||
cmdFlagNameBits = "bits"
|
||||
cmdFlagNameCurve = "curve"
|
||||
|
||||
cmdFlagNamePassword = "password"
|
||||
cmdFlagNameRandom = "random"
|
||||
cmdFlagNameRandomLength = "random.length"
|
||||
cmdFlagNameNoConfirm = "no-confirm"
|
||||
cmdFlagNameVariant = "variant"
|
||||
cmdFlagNameCost = "cost"
|
||||
cmdFlagNameIterations = "iterations"
|
||||
cmdFlagNameParallelism = "parallelism"
|
||||
cmdFlagNameBlockSize = "block-size"
|
||||
cmdFlagNameMemory = "memory"
|
||||
cmdFlagNameKeySize = "key-size"
|
||||
cmdFlagNameSaltSize = "salt-size"
|
||||
cmdFlagNameProfile = "profile"
|
||||
cmdFlagNameSHA512 = "sha512"
|
||||
cmdFlagNameConfig = "config"
|
||||
|
||||
cmdFlagNameCharSet = "charset"
|
||||
cmdFlagNameCharacters = "characters"
|
||||
cmdFlagNameLength = "length"
|
||||
)
|
||||
|
||||
const (
|
||||
cmdUseHashPassword = "hash-password [flags] -- [password]"
|
||||
cmdUseHash = "hash"
|
||||
cmdUseHashArgon2 = "argon2"
|
||||
cmdUseHashSHA2Crypt = "sha2crypt"
|
||||
cmdUseHashPBKDF2 = "pbkdf2"
|
||||
cmdUseHashBCrypt = "bcrypt"
|
||||
cmdUseHashSCrypt = "scrypt"
|
||||
|
||||
cmdUseCrypto = "crypto"
|
||||
cmdUseRand = "rand"
|
||||
cmdUseCertificate = "certificate"
|
||||
cmdUseGenerate = "generate"
|
||||
cmdUseValidate = "validate"
|
||||
cmdUseFmtValidate = "%s [flags] -- <digest>"
|
||||
cmdUseRequest = "request"
|
||||
cmdUsePair = "pair"
|
||||
cmdUseRSA = "rsa"
|
||||
|
@ -459,6 +538,8 @@ const (
|
|||
const (
|
||||
cryptoCertPubCertOut = "certificate"
|
||||
cryptoCertCSROut = "certificate signing request"
|
||||
|
||||
prefixFilePassword = "authentication_backend.file.password"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/simia-tech/crypt"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/configuration"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/validator"
|
||||
)
|
||||
|
||||
func newHashPasswordCmd() (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "hash-password [flags] -- <password>",
|
||||
Short: cmdAutheliaHashPasswordShort,
|
||||
Long: cmdAutheliaHashPasswordLong,
|
||||
Example: cmdAutheliaHashPasswordExample,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: cmdHashPasswordRunE,
|
||||
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
cmd.Flags().BoolP("sha512", "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordSHA512Configuration.Iterations))
|
||||
cmd.Flags().IntP("iterations", "i", schema.DefaultPasswordConfiguration.Iterations, "set the number of hashing iterations")
|
||||
cmd.Flags().StringP("salt", "s", "", "set the salt string")
|
||||
cmd.Flags().IntP("memory", "m", schema.DefaultPasswordConfiguration.Memory, "[argon2id] set the amount of memory param (in MB)")
|
||||
cmd.Flags().IntP("parallelism", "p", schema.DefaultPasswordConfiguration.Parallelism, "[argon2id] set the parallelism param")
|
||||
cmd.Flags().IntP("key-length", "k", schema.DefaultPasswordConfiguration.KeyLength, "[argon2id] set the key length param")
|
||||
cmd.Flags().IntP("salt-length", "l", schema.DefaultPasswordConfiguration.SaltLength, "set the auto-generated salt length")
|
||||
cmd.Flags().StringSliceP("config", "c", []string{}, "Configuration files")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdHashPasswordRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
salt, _ := cmd.Flags().GetString("salt")
|
||||
sha512, _ := cmd.Flags().GetBool("sha512")
|
||||
configs, _ := cmd.Flags().GetStringSlice("config")
|
||||
|
||||
mapDefaults := map[string]interface{}{
|
||||
"authentication_backend.file.password.algorithm": schema.DefaultPasswordConfiguration.Algorithm,
|
||||
"authentication_backend.file.password.iterations": schema.DefaultPasswordConfiguration.Iterations,
|
||||
"authentication_backend.file.password.key_length": schema.DefaultPasswordConfiguration.KeyLength,
|
||||
"authentication_backend.file.password.salt_length": schema.DefaultPasswordConfiguration.SaltLength,
|
||||
"authentication_backend.file.password.parallelism": schema.DefaultPasswordConfiguration.Parallelism,
|
||||
"authentication_backend.file.password.memory": schema.DefaultPasswordConfiguration.Memory,
|
||||
}
|
||||
|
||||
if sha512 {
|
||||
mapDefaults["authentication_backend.file.password.algorithm"] = schema.DefaultPasswordSHA512Configuration.Algorithm
|
||||
mapDefaults["authentication_backend.file.password.iterations"] = schema.DefaultPasswordSHA512Configuration.Iterations
|
||||
mapDefaults["authentication_backend.file.password.salt_length"] = schema.DefaultPasswordSHA512Configuration.SaltLength
|
||||
}
|
||||
|
||||
mapCLI := map[string]string{
|
||||
"iterations": "authentication_backend.file.password.iterations",
|
||||
"key-length": "authentication_backend.file.password.key_length",
|
||||
"salt-length": "authentication_backend.file.password.salt_length",
|
||||
"parallelism": "authentication_backend.file.password.parallelism",
|
||||
"memory": "authentication_backend.file.password.memory",
|
||||
}
|
||||
|
||||
sources := configuration.NewDefaultSourcesWithDefaults(configs,
|
||||
configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter,
|
||||
configuration.NewMapSource(mapDefaults),
|
||||
configuration.NewCommandLineSourceWithMapping(cmd.Flags(), mapCLI, false, false),
|
||||
)
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
|
||||
if _, config, err = configuration.Load(val, sources...); err != nil {
|
||||
return fmt.Errorf("error occurred loading configuration: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
hash string
|
||||
algorithm authentication.CryptAlgo
|
||||
)
|
||||
|
||||
p := config.AuthenticationBackend.File.Password
|
||||
|
||||
switch p.Algorithm {
|
||||
case "sha512":
|
||||
algorithm = authentication.HashingAlgorithmSHA512
|
||||
default:
|
||||
algorithm = authentication.HashingAlgorithmArgon2id
|
||||
}
|
||||
|
||||
validator.ValidatePasswordConfiguration(p, val)
|
||||
|
||||
errs := val.Errors()
|
||||
|
||||
if len(errs) != 0 {
|
||||
for i, e := range errs {
|
||||
if i == 0 {
|
||||
err = e
|
||||
continue
|
||||
}
|
||||
|
||||
err = fmt.Errorf("%v, %w", err, e)
|
||||
}
|
||||
|
||||
return fmt.Errorf("errors occurred validating the password configuration: %w", err)
|
||||
}
|
||||
|
||||
if salt != "" {
|
||||
salt = crypt.Base64Encoding.EncodeToString([]byte(salt))
|
||||
}
|
||||
|
||||
if hash, err = authentication.HashPassword(args[0], salt, algorithm, p.Iterations, p.Memory*1024, p.Parallelism, p.KeyLength, p.SaltLength); err != nil {
|
||||
return fmt.Errorf("error during password hashing: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Password hash: %s\n", hash)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -42,12 +42,12 @@ func NewRootCmd() (cmd *cobra.Command) {
|
|||
cmdWithConfigFlags(cmd, false, []string{})
|
||||
|
||||
cmd.AddCommand(
|
||||
newAccessControlCommand(),
|
||||
newBuildInfoCmd(),
|
||||
newCryptoCmd(),
|
||||
newHashPasswordCmd(),
|
||||
newStorageCmd(),
|
||||
newValidateConfigCmd(),
|
||||
newAccessControlCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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"}
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -88,7 +88,7 @@ func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) {
|
|||
OIDC: &schema.OpenIDConnectConfiguration{
|
||||
HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN",
|
||||
IssuerPrivateKey: &rsa.PrivateKey{},
|
||||
EnforcePKCE: "invalid",
|
||||
EnforcePKCE: testInvalid,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestValidateTOTP(t *testing.T) {
|
|||
{
|
||||
desc: "ShouldNormalizeTOTPAlgorithm",
|
||||
have: schema.TOTPConfiguration{
|
||||
Algorithm: "sha1",
|
||||
Algorithm: digestSHA1,
|
||||
Digits: 6,
|
||||
Period: 30,
|
||||
SecretSize: 32,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue