Merge remote-tracking branch 'origin/master' into feat-settings-ui

# Conflicts:
#	web/package.json
#	web/pnpm-lock.yaml
feat-settings-ui
James Elliott 2023-05-30 09:15:20 +10:00
commit 53d3cdb271
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
78 changed files with 2963 additions and 1232 deletions

View File

@ -2,7 +2,7 @@
title: "OpenID Connect 1.0" title: "OpenID Connect 1.0"
description: "" description: ""
lead: "" lead: ""
date: 2023-05-08T13:38:08+10:00 date: 2023-05-15T10:32:10+10:00
lastmod: 2022-01-18T20:07:56+01:00 lastmod: 2022-01-18T20:07:56+01:00
draft: false draft: false
images: [] images: []

View File

@ -2,7 +2,7 @@
title: "OpenID Connect 1.0 Clients" title: "OpenID Connect 1.0 Clients"
description: "OpenID Connect 1.0 Registered Clients Configuration" description: "OpenID Connect 1.0 Registered Clients Configuration"
lead: "Authelia can operate as an OpenID Connect 1.0 Provider. This section describes how to configure the registered clients." lead: "Authelia can operate as an OpenID Connect 1.0 Provider. This section describes how to configure the registered clients."
date: 2023-05-08T13:38:08+10:00 date: 2023-05-15T10:32:10+10:00
draft: false draft: false
images: [] images: []
menu: menu:
@ -28,39 +28,41 @@ intended for production use it's used to provide context and an indentation exam
identity_providers: identity_providers:
oidc: oidc:
clients: clients:
- id: myapp - id: 'myapp'
description: My Application description: 'My Application'
secret: '$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng' # The digest of 'insecure_secret'. secret: '$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng' # The digest of 'insecure_secret'.
sector_identifier: '' sector_identifier: ''
public: false public: false
redirect_uris: redirect_uris:
- https://oidc.example.com:8080/oauth2/callback - 'https://oidc.example.com:8080/oauth2/callback'
audience: [] audience: []
scopes: scopes:
- openid - 'openid'
- groups - 'groups'
- email - 'email'
- profile - 'profile'
grant_types: grant_types:
- refresh_token - 'refresh_token'
- authorization_code - 'authorization_code'
response_types: response_types:
- code - 'code'
response_modes: response_modes:
- form_post - 'form_post'
- query - 'query'
- fragment - 'fragment'
authorization_policy: two_factor authorization_policy: 'two_factor'
consent_mode: explicit consent_mode: 'explicit'
pre_configured_consent_duration: 1w pre_configured_consent_duration: '1 week'
enforce_par: false enforce_par: false
enforce_pkce: false enforce_pkce: false
pkce_challenge_method: S256 pkce_challenge_method: 'S256'
id_token_signing_alg: 'RS256'
id_token_signing_key_id: ''
userinfo_signing_alg: 'none'
userinfo_signing_key_id: ''
request_object_signing_alg: 'RS256'
token_endpoint_auth_signing_alg: 'RS256'
token_endpoint_auth_method: '' token_endpoint_auth_method: ''
token_endpoint_auth_signing_alg: RS256
id_token_signing_alg: RS256
request_object_signing_alg: RS256
userinfo_signing_alg: none
``` ```
## Options ## Options
@ -270,6 +272,65 @@ effectively enables the [enforce_pkce](#enforcepkce) option for this client.
Valid values are an empty string, `plain`, or `S256`. It should be noted that `S256` is strongly recommended if the Valid values are an empty string, `plain`, or `S256`. It should be noted that `S256` is strongly recommended if the
relying party supports it. relying party supports it.
### id_token_signing_alg
{{< confkey type="string" default="RS256" required="no" >}}
The algorithm used to sign the ID Tokens in the token responses.
See the response object section of the
[integration guide](../../../integration/openid-connect/introduction.md#response-object) for more information including
the algorithm column for supported values. In addition to the values listed we also support `none` as a value for this
endpoint.
The algorithm chosen must have a key configured in the [issuer_private_keys](provider.md#issuerprivatekeys) section to
be considered valid.
This option has no effect if the [id_token_signing_key_id](#idtokensigningkid) is specified as the algorithm is
automatically assumed by the configured key.
### id_token_signing_key_id
{{< confkey type="string" required="no" >}}
The key id of the JWK used to sign the ID Tokens in the token responses. This option takes precedence over
[id_token_signing_alg](#idtokensigningalg). The value of this must one of those provided or calculated in the
[issuer_private_keys](provider.md#issuerprivatekeys).
### userinfo_signing_alg
{{< confkey type="string" default="none" required="no" >}}
The algorithm used to sign the userinfo endpoint responses.
See the response object section of the [integration guide](../../../integration/openid-connect/introduction.md#response-object)
for more information including the algorithm column for supported values. In addition to the values listed we also
support `none` as a value for this endpoint.
The algorithm chosen must have a key configured in the [issuer_private_keys](provider.md#issuerprivatekeys) section to
be considered valid.
This option has no effect if the [userinfo_signing_key_id](#userinfosigningkeyid) is specified as the algorithm is
automatically assumed by the configured key.
### userinfo_signing_key_id
{{< confkey type="string" required="no" >}}
The key id of the JWK used to sign the userinfo endpoint responses in the token responses. This option takes precedence
over [userinfo_signing_alg](#userinfosigningalg). The value of this must one of those provided or calculated in the
[issuer_private_keys](provider.md#issuerprivatekeys).
### request_object_signing_alg
{{< confkey type="string" default="RSA256" required="no" >}}
The JWT signing algorithm accepted for request objects.
See the request object section of the
[integration guide](../../../integration/openid-connect/introduction.md#request-object) for more information including
the algorithm column for supported values.
### token_endpoint_auth_method ### token_endpoint_auth_method
{{< confkey type="string" default="" required="no" >}} {{< confkey type="string" default="" required="no" >}}
@ -300,35 +361,6 @@ otherwise we assume the default value:
| [token_endpoint_auth_method](#tokenendpointauthsigningalg) | `private_key_jwt` | `RS256` | | [token_endpoint_auth_method](#tokenendpointauthsigningalg) | `private_key_jwt` | `RS256` |
| [token_endpoint_auth_method](#tokenendpointauthsigningalg) | `client_secret_jwt` | `HS256` | | [token_endpoint_auth_method](#tokenendpointauthsigningalg) | `client_secret_jwt` | `HS256` |
### request_object_signing_alg
{{< confkey type="string" default="RSA256" required="no" >}}
The JWT signing algorithm accepted for request objects.
See the request object section of the [integration guide](../../../integration/openid-connect/introduction.md#request-object)
for more information including the algorithm column for supported values.
### id_token_signing_alg
{{< confkey type="string" default="RS256" required="no" >}}
The algorithm used to sign the ID Tokens in the token responses.
See the response object section of the [integration guide](../../../integration/openid-connect/introduction.md#response-object)
for more information including the algorithm column for supported values. In addition to the values listed we also
support `none` as a value for this endpoint.
### userinfo_signing_alg
{{< confkey type="string" default="none" required="no" >}}
The algorithm used to sign the userinfo endpoint responses.
See the response object section of the [integration guide](../../../integration/openid-connect/introduction.md#response-object)
for more information including the algorithm column for supported values. In addition to the values listed we also
support `none` as a value for this endpoint.
### public_keys ### public_keys
This section configures the trusted JSON Web Keys or JWKS for this registered client. This can either be static values This section configures the trusted JSON Web Keys or JWKS for this registered client. This can either be static values
@ -368,11 +400,29 @@ A list of static keys.
The Key ID used to match the request object's JWT header `kid` value against. The Key ID used to match the request object's JWT header `kid` value against.
##### use
{{< confkey type="string" default="sig" required="no" >}}
The key usage. Defaults to `sig` which is the only available option at this time.
##### algorithm
{{< confkey type="string" default="RS256" required="situational" >}}
The algorithm for this key. This value typically optional as it can be automatically detected based on the type of key
in some situations. It is however strongly recommended this is set.
See the request object table in the [integration guide](../../../integration/openid-connect/introduction.md#request-object)
for more information. The `Algorithm` column lists supported values, the `Key` column references the required
[key](#key) type constraints that exist for the algorithm, and the `JWK Default Conditions` column briefly explains the
conditions under which it's the default algorithm.
##### key ##### key
{{< confkey type="string" required="yes" >}} {{< confkey type="string" required="yes" >}}
The public key portion of the JSON Web Key The public key portion of the JSON Web Key.
The public key the clients use to sign/encrypt the [OpenID Connect 1.0] asserted [JWT]'s. The key is generated by the The public key the clients use to sign/encrypt the [OpenID Connect 1.0] asserted [JWT]'s. The key is generated by the
client application or the administrator of the client application. client application or the administrator of the client application.
@ -388,9 +438,15 @@ The key *__MUST__*:
* A P-384 elliptical curve. * A P-384 elliptical curve.
* A P-512 elliptical curve. * A P-512 elliptical curve.
If the [issuer_certificate_chain](#issuercertificatechain) is provided the private key must include matching public If the [certificate_chain](#certificatechain) is provided the private key must include matching public
key data for the first certificate in the chain. key data for the first certificate in the chain.
##### certificate_chain
{{< confkey type="string" required="no" >}}
The certificate chain/bundle to be used with the [key](#key) DER base64 ([RFC4648])
encoded PEM format used to sign/encrypt the [OpenID Connect 1.0] [JWT]'s.
## Integration ## Integration

View File

@ -2,7 +2,7 @@
title: "OpenID Connect 1.0 Provider" title: "OpenID Connect 1.0 Provider"
description: "OpenID Connect 1.0 Provider Configuration" description: "OpenID Connect 1.0 Provider Configuration"
lead: "Authelia can operate as an OpenID Connect 1.0 Provider. This section describes how to configure this." lead: "Authelia can operate as an OpenID Connect 1.0 Provider. This section describes how to configure this."
date: 2023-05-08T13:38:08+10:00 date: 2023-05-15T10:32:10+10:00
draft: false draft: false
images: [] images: []
menu: menu:
@ -135,41 +135,31 @@ with 64 or more characters.
### issuer_private_keys ### issuer_private_keys
The key *__MUST__*:
* Be a PEM block encoded in the DER base64 format ([RFC4648]).
* Be either:
* An RSA public key:
* With a key size of at least 2048 bits.
* An ECDSA public key with one of:
* A P-256 elliptical curve.
* A P-384 elliptical curve.
* A P-512 elliptical curve.
### issuer_private_keys
{{< confkey type="list(object" required="no" >}} {{< confkey type="list(object" required="no" >}}
The list of JWKS instead of or in addition to the [issuer_private_key](#issuerprivatekey) and The list of JWKS instead of or in addition to the [issuer_private_key](#issuerprivatekey) and
[issuer_certificate_chain](#issuercertificatechain). Can also accept ECDSA Private Key's and Certificates. [issuer_certificate_chain](#issuercertificatechain). Can also accept ECDSA Private Key's and Certificates.
The default key for each algorithm is is decided based on the order of this list. The first key for each algorithm is
considered the default if a client is not configured to use a specific key id. For example if a client has
[id_token_signing_alg](clients.md#idtokensigningalg) `ES256` and [id_token_signing_key_id](clients.md#idtokensigningkid) is
not specified then the first `ES256` key in this list is used.
#### key_id #### key_id
{{< confkey type="string" default="<thumbprint of public key>" required="no" >}} {{< confkey type="string" default="<thumbprint of public key>" required="no" >}}
Completely optional, and generally discouraged unless there is a collision between the automatically generated key id's. Completely optional, and generally discouraged unless there is a collision between the automatically generated key id's.
If provided must be a unique string with 7 or less alphanumeric characters. If provided must be a unique string with 100 or less characters, with a recommendation to use a length less
than 10. In addition it must meet the following rules:
This value is the first 7 characters of the public key thumbprint (SHA1) encoded into hexadecimal. - Match the regular expression `^[a-zA-Z0-9](([a-zA-Z0-9._~-]*)([a-zA-Z0-9]))?$` which should enforce the following rules:
- Start with an alphanumeric character.
- End with an alphanumeric character.
- Only contain the [RFC3986 Unreserved Characters](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
#### algorithm The default if this value is omitted is the first 7 characters of the public key SHA256 thumbprint encoded into
hexadecimal.
{{< confkey type="string" default="RS256" required="no" >}}
The algorithm for this key. This value must be unique. It's automatically detected based on the type of key.
See the response object table in the [integration guide](../../../integration/openid-connect/introduction.md#response-object)
including the algorithm column for the supported values and the key type column for the default algorithm value.
#### use #### use
@ -177,14 +167,28 @@ including the algorithm column for the supported values and the key type column
The key usage. Defaults to `sig` which is the only available option at this time. The key usage. Defaults to `sig` which is the only available option at this time.
#### algorithm
{{< confkey type="string" default="RS256" required="situational" >}}
The algorithm for this key. This value typically optional as it can be automatically detected based on the type of key
in some situations.
See the response object table in the [integration guide](../../../integration/openid-connect/introduction.md#response-object)
for more information. The `Algorithm` column lists supported values, the `Key` column references the required
[key](#key) type constraints that exist for the algorithm, and the `JWK Default Conditions` column briefly explains the
conditions under which it's the default algorithm.
At least one `RSA256` key must be provided.
#### key #### key
{{< confkey type="string" required="yes" >}} {{< confkey type="string" required="yes" >}}
The private key associated with this key entry. The private key associated with this key entry.
The private key used to sign/encrypt the [OpenID Connect 1.0] issued [JWT]'s. The key must be generated by the administrator The private key used to sign/encrypt the [OpenID Connect 1.0] issued [JWT]'s. The key must be generated by the
and can be done by following the administrator and can be done by following the
[Generating an RSA Keypair](../../../reference/guides/generating-secure-values.md#generating-an-rsa-keypair) guide. [Generating an RSA Keypair](../../../reference/guides/generating-secure-values.md#generating-an-rsa-keypair) guide.
The private key *__MUST__*: The private key *__MUST__*:
@ -217,15 +221,13 @@ it if present.
{{< confkey type="string" required="yes" >}} {{< confkey type="string" required="yes" >}}
*__Important Note:__ This can also be defined using a [secret](../../methods/secrets.md) which is __strongly recommended__ The private key used to sign/encrypt the [OpenID Connect 1.0] issued [JWT]'s. The key must be generated by the
especially for containerized deployments.* administrator and can be done by following the
The private key used to sign/encrypt the [OpenID Connect 1.0] issued [JWT]'s. The key must be generated by the administrator
and can be done by following the
[Generating an RSA Keypair](../../../reference/guides/generating-secure-values.md#generating-an-rsa-keypair) guide. [Generating an RSA Keypair](../../../reference/guides/generating-secure-values.md#generating-an-rsa-keypair) guide.
This private key is automatically appended to the [issuer_private_keys](#issuerprivatekeys) and assumed to be for the This private key is automatically appended to the [issuer_private_keys](#issuerprivatekeys) and assumed to be for the
RS256 algorithm. As such no other key in this list should be RS256 if this is configured. `RS256` algorithm. If provided it is always the first key in this list. As such this key is assumed to be the default
for `RS256` if provided.
The issuer private key *__MUST__*: The issuer private key *__MUST__*:
@ -240,7 +242,7 @@ key data for the first certificate in the chain.
{{< confkey type="string" required="no" >}} {{< confkey type="string" required="no" >}}
The certificate chain/bundle to be used with the [issuer_private_key](#issuer_private_key) DER base64 ([RFC4648]) The certificate chain/bundle to be used with the [issuer_private_key](#issuerprivatekey) DER base64 ([RFC4648])
encoded PEM format used to sign/encrypt the [OpenID Connect 1.0] [JWT]'s. When configured it enables the [x5c] and [x5t] encoded PEM format used to sign/encrypt the [OpenID Connect 1.0] [JWT]'s. When configured it enables the [x5c] and [x5t]
JSON key's in the JWKs [Discoverable Endpoint](../../../integration/openid-connect/introduction.md#discoverable-endpoints) JSON key's in the JWKs [Discoverable Endpoint](../../../integration/openid-connect/introduction.md#discoverable-endpoints)
as per [RFC7517]. as per [RFC7517].
@ -303,6 +305,8 @@ make certain scenarios less secure. It is highly encouraged that if your OpenID
these parameters or sends parameters with a lower length than the default that they implement a change rather than these parameters or sends parameters with a lower length than the default that they implement a change rather than
changing this value. changing this value.
This restriction can also be disabled entirely when set to `-1`.
### enforce_pkce ### enforce_pkce
{{< confkey type="string" default="public_clients_only" required="no" >}} {{< confkey type="string" default="public_clients_only" required="no" >}}
@ -411,7 +415,7 @@ See the [OpenID Connect 1.0 Registered Clients](clients.md) documentation for co
## Integration ## Integration
To integrate Authelia's [OpenID Connect 1.0] implementation with a relying party please see the To integrate Authelia's [OpenID Connect 1.0] implementation with a relying party please see the
[integration docs](../../integration/openid-connect/introduction.md). [integration docs](../../../integration/openid-connect/introduction.md).
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration [token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
[OpenID Connect 1.0]: https://openid.net/connect/ [OpenID Connect 1.0]: https://openid.net/connect/

View File

@ -15,6 +15,11 @@ aliases:
- /docs/configuration/access-control.html - /docs/configuration/access-control.html
--- ---
*__Important Note:__ This section does not apply to OpenID Connect 1.0. See the [Frequently Asked Questions] for more
information.*
[Frequently Asked Questions]: ../../integration/openid-connect/frequently-asked-questions.md#why-doesnt-the-access-control-configuration-work-with-openid-connect-10
## Configuration ## Configuration
{{< config-alert-example >}} {{< config-alert-example >}}

View File

@ -32,6 +32,7 @@ storage:
schema: 'public' schema: 'public'
username: 'authelia' username: 'authelia'
password: 'mypassword' password: 'mypassword'
timeout: '5s'
tls: tls:
server_name: 'postgres.example.com' server_name: 'postgres.example.com'
skip_verify: false skip_verify: false

View File

@ -88,6 +88,26 @@ If you've configured Authelia alongside a proxy and are making a request directl
request via the proxy. If you're avoiding the proxy due to a DNS limitation see request via the proxy. If you're avoiding the proxy due to a DNS limitation see
[Solution: Configure DNS Appropriately](#configure-dns-appropriately) section. [Solution: Configure DNS Appropriately](#configure-dns-appropriately) section.
### Why doesn't the access control configuration work with OpenID Connect 1.0?
The [access control](../../configuration/security/access-control.md) configuration contains several elements which are
not very compatible with OpenID Connect 1.0. They were designed with per-request authorizations in mind. In particular
the [resources](../../configuration/security/access-control.md#resources),
[query](../../configuration/security/access-control.md#query),
[methods](../../configuration/security/access-control.md#methods), and
[networks](../../configuration/security/access-control.md#networks) criteria are very specific to each request and to
some degree so are the [domain](../../configuration/security/access-control.md#domain) and
[domain regex](../../configuration/security/access-control.md#domainregex) criteria as the token is issued to the client
not a specific domain.
For these reasons we implemented the
[authorization policy](../../configuration/identity-providers/openid-connect/clients.md#authorizationpolicy) as a direct
option in the client. It's likely in the future that we'll expand this option to encompass the features that work well
with OpenID Connect 1.0 such as the [subject](../../configuration/security/access-control.md#subject) criteria which
reasonably be matched to an individual authorization policy. Because the other criteria are mostly geared towards
per-request authorization these criteria types are fairly unlikely to become part of OpenID Connect 1.0 as there are no
ways to apply these criteria except during the initial authorization request.
## Solutions ## Solutions
The following section details solutions for multiple of the questions above. The following section details solutions for multiple of the questions above.

View File

@ -36,7 +36,7 @@ In addition to the `https` scheme requirement for Authelia itself:
1. Due to the fact a cookie is used, it's an intentional design decision that *__ALL__* applications/domains protected via 1. Due to the fact a cookie is used, it's an intentional design decision that *__ALL__* applications/domains protected via
this method *__MUST__* use secure schemes (`https` and `wss`) for all of their communication. this method *__MUST__* use secure schemes (`https` and `wss`) for all of their communication.
### OpenID Connect ### OpenID Connect 1.0
No additional requirements other than the use of the `https` scheme for Authelia itself exist excluding those mandated No additional requirements other than the use of the `https` scheme for Authelia itself exist excluding those mandated
by the relevant specifications. by the relevant specifications.
@ -93,6 +93,11 @@ recommended that you read the relevant [Proxy Integration Documentation](../prox
recommend viewing the dedicated [Kubernetes Documentation](../kubernetes/introduction.md) prior to viewing the recommend viewing the dedicated [Kubernetes Documentation](../kubernetes/introduction.md) prior to viewing the
[Proxy Integration Documentation](../proxies/introduction.md).* [Proxy Integration Documentation](../proxies/introduction.md).*
## Additional Useful Links
See the [Frequently Asked Questions](../../reference/guides/frequently-asked-questions.md) for helpful sections of the
documentation which may answer specific questions.
## Moving to Production ## Moving to Production
We consider it important to do several things in moving to a production environment. We consider it important to do several things in moving to a production environment.

8
go.mod
View File

@ -11,9 +11,9 @@ require (
github.com/fasthttp/session/v2 v2.5.0 github.com/fasthttp/session/v2 v2.5.0
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.6.0
github.com/go-asn1-ber/asn1-ber v1.5.4 github.com/go-asn1-ber/asn1-ber v1.5.4
github.com/go-crypt/crypt v0.2.7 github.com/go-crypt/crypt v0.2.9
github.com/go-ldap/ldap/v3 v3.4.5-0.20230506142018-039466e6b835 github.com/go-ldap/ldap/v3 v3.4.5-0.20230521105649-cdb0754f6668
github.com/go-rod/rod v0.113.0 github.com/go-rod/rod v0.113.1
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/go-webauthn/webauthn v0.8.2 github.com/go-webauthn/webauthn v0.8.2
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.0
@ -70,7 +70,7 @@ require (
github.com/ecordell/optgen v0.0.6 // indirect github.com/ecordell/optgen v0.0.6 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-crypt/x v0.2.0 // indirect github.com/go-crypt/x v0.2.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-webauthn/revoke v0.1.9 // indirect github.com/go-webauthn/revoke v0.1.9 // indirect
github.com/golang/glog v1.0.0 // indirect github.com/golang/glog v1.0.0 // indirect

16
go.sum
View File

@ -130,22 +130,22 @@ github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrt
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= 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-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-crypt/crypt v0.2.7 h1:Ir6E59c1wrskJhpJXMqaynHA2xAxpGN7nQXlLkbpzR0= github.com/go-crypt/crypt v0.2.9 h1:5gWWTId2Qyqs9ROIsegt5pnqo9wUSRLbhpkR6JSftjg=
github.com/go-crypt/crypt v0.2.7/go.mod h1:ulieouNs4qwFCq4wF61oyTQYXAXSoOv995EU4hcHwMU= github.com/go-crypt/crypt v0.2.9/go.mod h1:JjzdTYE2mArb6nBoIvvpF7o46/rK/1pfmlArCRMTFUk=
github.com/go-crypt/x v0.2.0 h1:rHMiKRAu6kFc+xAnQywDb3iHGpvrFbIGXnP3IfCZ+2U= github.com/go-crypt/x v0.2.1 h1:OGw78Bswme9lffCOX6tyuC280ouU5391glsvThMtM5U=
github.com/go-crypt/x v0.2.0/go.mod h1:uLo5o+Cc8nvahDASQpntR1g3ZMUoq2LM/859PkhykC4= github.com/go-crypt/x v0.2.1/go.mod h1:Q/y9rms7yw4/1CavBlNGn0Itg4HqwNpe1N9FX0TxXrc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 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-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.4.5-0.20230506142018-039466e6b835 h1:XgBmN9yZXIh9vJGzs2qYPb5ee8/VnOWLHHYcKXGXKME= github.com/go-ldap/ldap/v3 v3.4.5-0.20230521105649-cdb0754f6668 h1:qbWHOCDBT8m2I1tDGP7S58dgi/xaDDCKuR5dbarLGOU=
github.com/go-ldap/ldap/v3 v3.4.5-0.20230506142018-039466e6b835/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs= github.com/go-ldap/ldap/v3 v3.4.5-0.20230521105649-cdb0754f6668/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-rod/rod v0.113.0 h1:E7+GLjYVZnScewIB2u8+66joQLaDGbOLzSOT4orNHms= github.com/go-rod/rod v0.113.1 h1:+Qb4K/vkR7BOhW6FhfhtLzUD3l11+0XlF4do+27sOQk=
github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= github.com/go-rod/rod v0.113.1/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=

View File

@ -2,20 +2,8 @@ package authentication
import ( import (
"errors" "errors"
)
// Level is the type representing a level of authentication. "golang.org/x/text/encoding/unicode"
type Level int
const (
// NotAuthenticated if the user is not authenticated yet.
NotAuthenticated Level = iota
// OneFactor if the user has passed first factor only.
OneFactor
// TwoFactor if the user has passed two factors.
TwoFactor
) )
const ( const (
@ -109,3 +97,7 @@ const fileAuthenticationMode = 0600
// OWASP recommends to escape some special characters. // OWASP recommends to escape some special characters.
// https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.md // https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.md
const specialLDAPRunes = ",#+<>;\"=" const specialLDAPRunes = ",#+<>;\"="
var (
encodingUTF16LittleEndian = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
)

View File

@ -23,7 +23,7 @@ import (
type FileUserProvider struct { type FileUserProvider struct {
config *schema.FileAuthenticationBackend config *schema.FileAuthenticationBackend
hash algorithm.Hash hash algorithm.Hash
database *FileUserDatabase database FileUserDatabase
mutex *sync.Mutex mutex *sync.Mutex
timeoutReload time.Time timeoutReload time.Time
} }
@ -34,6 +34,7 @@ func NewFileUserProvider(config *schema.FileAuthenticationBackend) (provider *Fi
config: config, config: config,
mutex: &sync.Mutex{}, mutex: &sync.Mutex{},
timeoutReload: time.Now().Add(-1 * time.Second), timeoutReload: time.Now().Add(-1 * time.Second),
database: NewYAMLUserDatabase(config.Path, config.Search.Email, config.Search.CaseInsensitive),
} }
} }
@ -136,7 +137,9 @@ func (p *FileUserProvider) StartupCheck() (err error) {
return err return err
} }
p.database = NewFileUserDatabase(p.config.Path, p.config.Search.Email, p.config.Search.CaseInsensitive) if p.database == nil {
p.database = NewYAMLUserDatabase(p.config.Path, p.config.Search.Email, p.config.Search.CaseInsensitive)
}
if err = p.database.Load(); err != nil { if err = p.database.Load(); err != nil {
return err return err
@ -194,10 +197,6 @@ func NewFileCryptoHashFromConfig(config schema.Password) (hash algorithm.Hash, e
return nil, fmt.Errorf("failed to initialize hash settings: %w", err) return nil, fmt.Errorf("failed to initialize hash settings: %w", err)
} }
if err = hash.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate hash settings: %w", err)
}
return hash, nil return hash, nil
} }

View File

@ -12,9 +12,16 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// NewFileUserDatabase creates a new FileUserDatabase. type FileUserDatabase interface {
func NewFileUserDatabase(filePath string, searchEmail, searchCI bool) (database *FileUserDatabase) { Save() (err error)
return &FileUserDatabase{ Load() (err error)
GetUserDetails(username string) (user DatabaseUserDetails, err error)
SetUserDetails(username string, details *DatabaseUserDetails)
}
// NewYAMLUserDatabase creates a new YAMLUserDatabase.
func NewYAMLUserDatabase(filePath string, searchEmail, searchCI bool) (database *YAMLUserDatabase) {
return &YAMLUserDatabase{
RWMutex: &sync.RWMutex{}, RWMutex: &sync.RWMutex{},
Path: filePath, Path: filePath,
Users: map[string]DatabaseUserDetails{}, Users: map[string]DatabaseUserDetails{},
@ -25,8 +32,8 @@ func NewFileUserDatabase(filePath string, searchEmail, searchCI bool) (database
} }
} }
// FileUserDatabase is a user details database that is concurrency safe database and can be reloaded. // YAMLUserDatabase is a user details database that is concurrency safe database and can be reloaded.
type FileUserDatabase struct { type YAMLUserDatabase struct {
*sync.RWMutex *sync.RWMutex
Path string Path string
@ -39,7 +46,7 @@ type FileUserDatabase struct {
} }
// Save the database to disk. // Save the database to disk.
func (m *FileUserDatabase) Save() (err error) { func (m *YAMLUserDatabase) Save() (err error) {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
@ -52,7 +59,7 @@ func (m *FileUserDatabase) Save() (err error) {
} }
// Load the database from disk. // Load the database from disk.
func (m *FileUserDatabase) Load() (err error) { func (m *YAMLUserDatabase) Load() (err error) {
yml := &DatabaseModel{Users: map[string]UserDetailsModel{}} yml := &DatabaseModel{Users: map[string]UserDetailsModel{}}
if err = yml.Read(m.Path); err != nil { if err = yml.Read(m.Path); err != nil {
@ -71,7 +78,7 @@ func (m *FileUserDatabase) Load() (err error) {
} }
// LoadAliases performs the loading of alias information from the database. // LoadAliases performs the loading of alias information from the database.
func (m *FileUserDatabase) LoadAliases() (err error) { func (m *YAMLUserDatabase) LoadAliases() (err error) {
if m.SearchEmail || m.SearchCI { if m.SearchEmail || m.SearchCI {
for k, user := range m.Users { for k, user := range m.Users {
if m.SearchEmail && user.Email != "" { if m.SearchEmail && user.Email != "" {
@ -91,7 +98,7 @@ func (m *FileUserDatabase) LoadAliases() (err error) {
return nil return nil
} }
func (m *FileUserDatabase) loadAlias(k string) (err error) { func (m *YAMLUserDatabase) loadAlias(k string) (err error) {
u := strings.ToLower(k) u := strings.ToLower(k)
if u != k { if u != k {
@ -113,7 +120,7 @@ func (m *FileUserDatabase) loadAlias(k string) (err error) {
return nil return nil
} }
func (m *FileUserDatabase) loadAliasEmail(k string, user DatabaseUserDetails) (err error) { func (m *YAMLUserDatabase) loadAliasEmail(k string, user DatabaseUserDetails) (err error) {
e := strings.ToLower(user.Email) e := strings.ToLower(user.Email)
var duplicates []string var duplicates []string
@ -145,7 +152,7 @@ func (m *FileUserDatabase) loadAliasEmail(k string, user DatabaseUserDetails) (e
// GetUserDetails get a DatabaseUserDetails given a username as a value type where the username must be the users actual // GetUserDetails get a DatabaseUserDetails given a username as a value type where the username must be the users actual
// username. // username.
func (m *FileUserDatabase) GetUserDetails(username string) (user DatabaseUserDetails, err error) { func (m *YAMLUserDatabase) GetUserDetails(username string) (user DatabaseUserDetails, err error) {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
@ -172,7 +179,7 @@ func (m *FileUserDatabase) GetUserDetails(username string) (user DatabaseUserDet
} }
// SetUserDetails sets the DatabaseUserDetails for a given user. // SetUserDetails sets the DatabaseUserDetails for a given user.
func (m *FileUserDatabase) SetUserDetails(username string, details *DatabaseUserDetails) { func (m *YAMLUserDatabase) SetUserDetails(username string, details *DatabaseUserDetails) {
if details == nil { if details == nil {
return return
} }
@ -184,8 +191,8 @@ func (m *FileUserDatabase) SetUserDetails(username string, details *DatabaseUser
m.Unlock() m.Unlock()
} }
// ToDatabaseModel converts the FileUserDatabase into the DatabaseModel for saving. // ToDatabaseModel converts the YAMLUserDatabase into the DatabaseModel for saving.
func (m *FileUserDatabase) ToDatabaseModel() (model *DatabaseModel) { func (m *YAMLUserDatabase) ToDatabaseModel() (model *DatabaseModel) {
model = &DatabaseModel{ model = &DatabaseModel{
Users: map[string]UserDetailsModel{}, Users: map[string]UserDetailsModel{},
} }
@ -236,8 +243,8 @@ type DatabaseModel struct {
Users map[string]UserDetailsModel `yaml:"users" valid:"required"` Users map[string]UserDetailsModel `yaml:"users" valid:"required"`
} }
// ReadToFileUserDatabase reads the DatabaseModel into a FileUserDatabase. // ReadToFileUserDatabase reads the DatabaseModel into a YAMLUserDatabase.
func (m *DatabaseModel) ReadToFileUserDatabase(db *FileUserDatabase) (err error) { func (m *DatabaseModel) ReadToFileUserDatabase(db *YAMLUserDatabase) (err error) {
users := map[string]DatabaseUserDetails{} users := map[string]DatabaseUserDetails{}
var udm *DatabaseUserDetails var udm *DatabaseUserDetails

View File

@ -0,0 +1,89 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/authelia/authelia/v4/internal/authentication (interfaces: FileUserDatabase)
// Package authentication is a generated GoMock package.
package authentication
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockFileUserDatabase is a mock of FileUserDatabase interface.
type MockFileUserDatabase struct {
ctrl *gomock.Controller
recorder *MockFileUserDatabaseMockRecorder
}
// MockFileUserDatabaseMockRecorder is the mock recorder for MockFileUserDatabase.
type MockFileUserDatabaseMockRecorder struct {
mock *MockFileUserDatabase
}
// NewMockFileUserDatabase creates a new mock instance.
func NewMockFileUserDatabase(ctrl *gomock.Controller) *MockFileUserDatabase {
mock := &MockFileUserDatabase{ctrl: ctrl}
mock.recorder = &MockFileUserDatabaseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFileUserDatabase) EXPECT() *MockFileUserDatabaseMockRecorder {
return m.recorder
}
// GetUserDetails mocks base method.
func (m *MockFileUserDatabase) GetUserDetails(arg0 string) (DatabaseUserDetails, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserDetails", arg0)
ret0, _ := ret[0].(DatabaseUserDetails)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserDetails indicates an expected call of GetUserDetails.
func (mr *MockFileUserDatabaseMockRecorder) GetUserDetails(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserDetails", reflect.TypeOf((*MockFileUserDatabase)(nil).GetUserDetails), arg0)
}
// Load mocks base method.
func (m *MockFileUserDatabase) Load() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Load")
ret0, _ := ret[0].(error)
return ret0
}
// Load indicates an expected call of Load.
func (mr *MockFileUserDatabaseMockRecorder) Load() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Load", reflect.TypeOf((*MockFileUserDatabase)(nil).Load))
}
// Save mocks base method.
func (m *MockFileUserDatabase) Save() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save")
ret0, _ := ret[0].(error)
return ret0
}
// Save indicates an expected call of Save.
func (mr *MockFileUserDatabaseMockRecorder) Save() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockFileUserDatabase)(nil).Save))
}
// SetUserDetails mocks base method.
func (m *MockFileUserDatabase) SetUserDetails(arg0 string, arg1 *DatabaseUserDetails) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetUserDetails", arg0, arg1)
}
// SetUserDetails indicates an expected call of SetUserDetails.
func (mr *MockFileUserDatabaseMockRecorder) SetUserDetails(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserDetails", reflect.TypeOf((*MockFileUserDatabase)(nil).SetUserDetails), arg0, arg1)
}

View File

@ -0,0 +1,39 @@
package authentication
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDatabaseModel_Read(t *testing.T) {
model := &DatabaseModel{}
dir := t.TempDir()
_, err := os.Create(filepath.Join(dir, "users_database.yml"))
assert.NoError(t, err)
assert.EqualError(t, model.Read(filepath.Join(dir, "users_database.yml")), "no file content")
assert.NoError(t, os.Mkdir(filepath.Join(dir, "x"), 0000))
f := filepath.Join(dir, "x", "users_database.yml")
assert.EqualError(t, model.Read(f), fmt.Sprintf("failed to read the '%s' file: open %s: permission denied", f, f))
f = filepath.Join(dir, "schema.yml")
file, err := os.Create(f)
assert.NoError(t, err)
_, err = file.WriteString("users:\n\tjohn: {}")
assert.NoError(t, err)
assert.EqualError(t, model.Read(f), "could not parse the YAML database: yaml: line 2: found character that cannot start any token")
}

View File

@ -0,0 +1,93 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/go-crypt/crypt/algorithm (interfaces: Hash)
// Package authentication is a generated GoMock package.
package authentication
import (
reflect "reflect"
algorithm "github.com/go-crypt/crypt/algorithm"
gomock "github.com/golang/mock/gomock"
)
// MockHash is a mock of Hash interface.
type MockHash struct {
ctrl *gomock.Controller
recorder *MockHashMockRecorder
}
// MockHashMockRecorder is the mock recorder for MockHash.
type MockHashMockRecorder struct {
mock *MockHash
}
// NewMockHash creates a new mock instance.
func NewMockHash(ctrl *gomock.Controller) *MockHash {
mock := &MockHash{ctrl: ctrl}
mock.recorder = &MockHashMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHash) EXPECT() *MockHashMockRecorder {
return m.recorder
}
// Hash mocks base method.
func (m *MockHash) Hash(arg0 string) (algorithm.Digest, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Hash", arg0)
ret0, _ := ret[0].(algorithm.Digest)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Hash indicates an expected call of Hash.
func (mr *MockHashMockRecorder) Hash(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hash", reflect.TypeOf((*MockHash)(nil).Hash), arg0)
}
// HashWithSalt mocks base method.
func (m *MockHash) HashWithSalt(arg0 string, arg1 []byte) (algorithm.Digest, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HashWithSalt", arg0, arg1)
ret0, _ := ret[0].(algorithm.Digest)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HashWithSalt indicates an expected call of HashWithSalt.
func (mr *MockHashMockRecorder) HashWithSalt(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HashWithSalt", reflect.TypeOf((*MockHash)(nil).HashWithSalt), arg0, arg1)
}
// MustHash mocks base method.
func (m *MockHash) MustHash(arg0 string) algorithm.Digest {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MustHash", arg0)
ret0, _ := ret[0].(algorithm.Digest)
return ret0
}
// MustHash indicates an expected call of MustHash.
func (mr *MockHashMockRecorder) MustHash(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MustHash", reflect.TypeOf((*MockHash)(nil).MustHash), arg0)
}
// Validate mocks base method.
func (m *MockHash) Validate() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Validate")
ret0, _ := ret[0].(error)
return ret0
}
// Validate indicates an expected call of Validate.
func (mr *MockHashMockRecorder) Validate() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockHash)(nil).Validate))
}

View File

@ -1,59 +1,167 @@
package authentication package authentication
import ( import (
"log" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
"sync"
"testing" "testing"
"time"
"github.com/go-crypt/crypt/algorithm/bcrypt"
"github.com/go-crypt/crypt/algorithm/pbkdf2"
"github.com/go-crypt/crypt/algorithm/scrypt"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
func WithDatabase(content []byte, f func(path string)) {
tmpfile, err := os.CreateTemp("", "users_database.*.yaml")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // Clean up.
if _, err := tmpfile.Write(content); err != nil {
tmpfile.Close()
log.Panic(err)
}
f(tmpfile.Name())
if err := tmpfile.Close(); err != nil {
log.Panic(err)
}
}
func TestShouldErrorPermissionsOnLocalFS(t *testing.T) { func TestShouldErrorPermissionsOnLocalFS(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("skipping test due to being on windows") t.Skip("skipping test due to being on windows")
} }
_ = os.Mkdir("/tmp/noperms/", 0000) dir := t.TempDir()
err := checkDatabase("/tmp/noperms/users_database.yml")
require.EqualError(t, err, "error checking user authentication database file: stat /tmp/noperms/users_database.yml: permission denied") _ = os.Mkdir(filepath.Join(dir, "noperms"), 0000)
f := filepath.Join(dir, "noperms", "users_database.yml")
require.EqualError(t, checkDatabase(f), fmt.Sprintf("error checking user authentication database file: stat %s: permission denied", f))
} }
func TestShouldErrorAndGenerateUserDB(t *testing.T) { func TestShouldErrorAndGenerateUserDB(t *testing.T) {
err := checkDatabase("./nonexistent.yml") dir := t.TempDir()
_ = os.Remove("./nonexistent.yml")
require.EqualError(t, err, "user authentication database file doesn't exist at path './nonexistent.yml' and has been generated") f := filepath.Join(dir, "users_database.yml")
require.EqualError(t, checkDatabase(f), fmt.Sprintf("user authentication database file doesn't exist at path '%s' and has been generated", f))
}
func TestShouldErrorFailCreateDB(t *testing.T) {
dir := t.TempDir()
assert.NoError(t, os.Mkdir(filepath.Join(dir, "x"), 0000))
f := filepath.Join(dir, "x", "users.yml")
provider := NewFileUserProvider(&schema.FileAuthenticationBackend{Path: f, Password: schema.DefaultPasswordConfig})
require.NotNil(t, provider)
assert.EqualError(t, provider.StartupCheck(), "one or more errors occurred checking the authentication database")
assert.NotNil(t, provider.database)
reloaded, err := provider.Reload()
assert.False(t, reloaded)
assert.EqualError(t, err, fmt.Sprintf("failed to reload: error reading the authentication database: failed to read the '%s' file: open %s: permission denied", f, f))
}
func TestShouldErrorBadPasswordConfig(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "users.yml")
require.NoError(t, os.WriteFile(f, UserDatabaseContent, 0600))
provider := NewFileUserProvider(&schema.FileAuthenticationBackend{Path: f})
require.NotNil(t, provider)
assert.EqualError(t, provider.StartupCheck(), "failed to initialize hash settings: argon2 validation error: parameter is invalid: parameter 't' must be between 1 and 2147483647 but is set to '0'")
}
func TestShouldNotPanicOnNilDB(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "users.yml")
assert.NoError(t, os.WriteFile(f, UserDatabaseContent, 0600))
provider := &FileUserProvider{
config: &schema.FileAuthenticationBackend{Path: f, Password: schema.DefaultPasswordConfig},
mutex: &sync.Mutex{},
timeoutReload: time.Now().Add(-1 * time.Second),
}
assert.NoError(t, provider.StartupCheck())
}
func TestShouldReloadDatabase(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "users.yml")
testCases := []struct {
name string
setup func(t *testing.T, provider *FileUserProvider)
expected bool
err string
}{
{
"ShouldSkipReloadRecentlyReloaded",
func(t *testing.T, provider *FileUserProvider) {
provider.timeoutReload = time.Now().Add(time.Minute)
},
false,
"",
},
{
"ShouldReloadWithoutError",
func(t *testing.T, provider *FileUserProvider) {
provider.timeoutReload = time.Now().Add(time.Minute * -1)
},
true,
"",
},
{
"ShouldNotReloadWithNoContent",
func(t *testing.T, provider *FileUserProvider) {
p := filepath.Join(dir, "empty.yml")
_, _ = os.Create(p)
provider.timeoutReload = time.Now().Add(time.Minute * -1)
provider.config.Path = p
provider.database = NewYAMLUserDatabase(p, provider.config.Search.Email, provider.config.Search.CaseInsensitive)
},
false,
"",
},
}
require.NoError(t, os.WriteFile(path, UserDatabaseContent, 0600))
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provider := NewFileUserProvider(&schema.FileAuthenticationBackend{
Path: path,
Password: schema.DefaultPasswordConfig,
})
tc.setup(t, provider)
actual, theError := provider.Reload()
assert.Equal(t, tc.expected, actual)
if tc.err == "" {
assert.NoError(t, theError)
} else {
assert.EqualError(t, theError, tc.err)
}
})
}
} }
func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) { func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
provider := NewFileUserProvider(&config) provider := NewFileUserProvider(&config)
@ -68,7 +176,7 @@ func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
} }
func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) { func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -84,7 +192,7 @@ func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) {
} }
func TestShouldCheckUserPasswordIsWrong(t *testing.T) { func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -100,7 +208,7 @@ func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
} }
func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) { func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -115,7 +223,7 @@ func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) {
} }
func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) { func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -131,7 +239,7 @@ func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) {
} }
func TestShouldRetrieveUserDetails(t *testing.T) { func TestShouldRetrieveUserDetails(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -147,8 +255,27 @@ func TestShouldRetrieveUserDetails(t *testing.T) {
}) })
} }
func TestShouldErrOnUserDetailsNoUser(t *testing.T) {
WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
details, err := provider.GetDetails("nouser")
assert.Nil(t, details)
assert.Equal(t, err, ErrUserNotFound)
details, err = provider.GetDetails("dis")
assert.Nil(t, details)
assert.Equal(t, err, ErrUserNotFound)
})
}
func TestShouldUpdatePassword(t *testing.T) { func TestShouldUpdatePassword(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -172,7 +299,7 @@ func TestShouldUpdatePassword(t *testing.T) {
// Checks both that the hashing algo changes and that it removes {CRYPT} from the start. // Checks both that the hashing algo changes and that it removes {CRYPT} from the start.
func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) { func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -180,7 +307,10 @@ func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) {
assert.NoError(t, provider.StartupCheck()) assert.NoError(t, provider.StartupCheck())
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].Digest.Encode(), "$6$")) db, ok := provider.database.(*YAMLUserDatabase)
require.True(t, ok)
assert.True(t, strings.HasPrefix(db.Users["harry"].Digest.Encode(), "$6$"))
err := provider.UpdatePassword("harry", "newpassword") err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err) assert.NoError(t, err)
@ -189,15 +319,15 @@ func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) {
assert.NoError(t, provider.StartupCheck()) assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("harry", "newpassword") ok, err = provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, ok) assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].Digest.Encode(), "$argon2id$")) assert.True(t, strings.HasPrefix(db.Users["harry"].Digest.Encode(), "$argon2id$"))
}) })
} }
func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) { func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Password.Algorithm = "sha2crypt" config.Password.Algorithm = "sha2crypt"
@ -207,7 +337,10 @@ func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
assert.NoError(t, provider.StartupCheck()) assert.NoError(t, provider.StartupCheck())
assert.True(t, strings.HasPrefix(provider.database.Users["john"].Digest.Encode(), "$argon2id$")) db, ok := provider.database.(*YAMLUserDatabase)
require.True(t, ok)
assert.True(t, strings.HasPrefix(db.Users["john"].Digest.Encode(), "$argon2id$"))
err := provider.UpdatePassword("john", "newpassword") err := provider.UpdatePassword("john", "newpassword")
assert.NoError(t, err) assert.NoError(t, err)
@ -216,15 +349,29 @@ func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
assert.NoError(t, provider.StartupCheck()) assert.NoError(t, provider.StartupCheck())
ok, err := provider.CheckUserPassword("john", "newpassword") ok, err = provider.CheckUserPassword("john", "newpassword")
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, ok) assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].Digest.Encode(), "$6$")) assert.True(t, strings.HasPrefix(db.Users["john"].Digest.Encode(), "$6$"))
})
}
func TestShouldErrOnUpdatePasswordNoUser(t *testing.T) {
WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
assert.Equal(t, provider.UpdatePassword("nousers", "newpassword"), ErrUserNotFound)
assert.Equal(t, provider.UpdatePassword("dis", "example"), ErrUserNotFound)
}) })
} }
func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
WithDatabase(MalformedUserDatabaseContent, func(path string) { WithDatabase(t, MalformedUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -235,7 +382,7 @@ func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
} }
func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
WithDatabase(BadSchemaUserDatabaseContent, func(path string) { WithDatabase(t, BadSchemaUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -246,7 +393,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
} }
func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *testing.T) {
WithDatabase(BadSHA512HashContent, func(path string) { WithDatabase(t, BadSHA512HashContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -257,7 +404,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *tes
} }
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSettingsContent, func(path string) { WithDatabase(t, BadArgon2idHashSettingsContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -268,7 +415,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTim
} }
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashKeyContent, func(path string) { WithDatabase(t, BadArgon2idHashKeyContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -279,7 +426,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *
} }
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t *testing.T) { func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSaltContent, func(path string) { WithDatabase(t, BadArgon2idHashSaltContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -290,7 +437,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t
} }
func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) { func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
WithDatabase(UserDatabaseWithoutCryptContent, func(path string) { WithDatabase(t, UserDatabaseWithoutCryptContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -306,7 +453,7 @@ func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
} }
func TestShouldNotAllowLoginOfDisabledUsers(t *testing.T) { func TestShouldNotAllowLoginOfDisabledUsers(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
@ -322,7 +469,7 @@ func TestShouldNotAllowLoginOfDisabledUsers(t *testing.T) {
} }
func TestShouldErrorOnInvalidCaseSensitiveFile(t *testing.T) { func TestShouldErrorOnInvalidCaseSensitiveFile(t *testing.T) {
WithDatabase(UserDatabaseContentInvalidSearchCaseInsenstive, func(path string) { WithDatabase(t, UserDatabaseContentInvalidSearchCaseInsenstive, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Search.Email = false config.Search.Email = false
@ -335,7 +482,7 @@ func TestShouldErrorOnInvalidCaseSensitiveFile(t *testing.T) {
} }
func TestShouldErrorOnDuplicateEmail(t *testing.T) { func TestShouldErrorOnDuplicateEmail(t *testing.T) {
WithDatabase(UserDatabaseContentInvalidSearchEmail, func(path string) { WithDatabase(t, UserDatabaseContentInvalidSearchEmail, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Search.Email = true config.Search.Email = true
@ -349,7 +496,7 @@ func TestShouldErrorOnDuplicateEmail(t *testing.T) {
} }
func TestShouldNotErrorOnEmailAsUsername(t *testing.T) { func TestShouldNotErrorOnEmailAsUsername(t *testing.T) {
WithDatabase(UserDatabaseContentSearchEmailAsUsername, func(path string) { WithDatabase(t, UserDatabaseContentSearchEmailAsUsername, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Search.Email = true config.Search.Email = true
@ -362,7 +509,7 @@ func TestShouldNotErrorOnEmailAsUsername(t *testing.T) {
} }
func TestShouldErrorOnEmailAsUsernameWithDuplicateEmail(t *testing.T) { func TestShouldErrorOnEmailAsUsernameWithDuplicateEmail(t *testing.T) {
WithDatabase(UserDatabaseContentInvalidSearchEmailAsUsername, func(path string) { WithDatabase(t, UserDatabaseContentInvalidSearchEmailAsUsername, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Search.Email = true config.Search.Email = true
@ -375,7 +522,7 @@ func TestShouldErrorOnEmailAsUsernameWithDuplicateEmail(t *testing.T) {
} }
func TestShouldErrorOnEmailAsUsernameWithDuplicateEmailCase(t *testing.T) { func TestShouldErrorOnEmailAsUsernameWithDuplicateEmailCase(t *testing.T) {
WithDatabase(UserDatabaseContentInvalidSearchEmailAsUsernameCase, func(path string) { WithDatabase(t, UserDatabaseContentInvalidSearchEmailAsUsernameCase, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Search.Email = false config.Search.Email = false
@ -388,7 +535,7 @@ func TestShouldErrorOnEmailAsUsernameWithDuplicateEmailCase(t *testing.T) {
} }
func TestShouldAllowLookupByEmail(t *testing.T) { func TestShouldAllowLookupByEmail(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Search.Email = true config.Search.Email = true
@ -415,7 +562,7 @@ func TestShouldAllowLookupByEmail(t *testing.T) {
} }
func TestShouldAllowLookupCI(t *testing.T) { func TestShouldAllowLookupCI(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) { WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration config := DefaultFileAuthenticationBackendConfiguration
config.Path = path config.Path = path
config.Search.CaseInsensitive = true config.Search.CaseInsensitive = true
@ -436,6 +583,139 @@ func TestShouldAllowLookupCI(t *testing.T) {
}) })
} }
func TestNewFileCryptoHashFromConfig(t *testing.T) {
testCases := []struct {
name string
have schema.Password
expected any
err string
}{
{
"ShouldCreatePBKDF2",
schema.Password{
Algorithm: "pbkdf2",
PBKDF2: schema.PBKDF2Password{
Variant: "sha256",
Iterations: 100000,
SaltLength: 16,
},
},
&pbkdf2.Hasher{},
"",
},
{
"ShouldCreateSCrypt",
schema.Password{
Algorithm: "scrypt",
SCrypt: schema.SCryptPassword{
Iterations: 12,
SaltLength: 16,
Parallelism: 1,
BlockSize: 1,
KeyLength: 32,
},
},
&scrypt.Hasher{},
"",
},
{
"ShouldCreateBCrypt",
schema.Password{
Algorithm: "bcrypt",
BCrypt: schema.BCryptPassword{
Variant: "standard",
Cost: 12,
},
},
&bcrypt.Hasher{},
"",
},
{
"ShouldFailToCreateSCryptInvalidParameter",
schema.Password{
Algorithm: "scrypt",
},
nil,
"failed to initialize hash settings: scrypt validation error: parameter is invalid: parameter 'iterations' must be between 1 and 58 but is set to '0'",
},
{
"ShouldFailUnknown",
schema.Password{
Algorithm: "unknown",
},
nil,
"algorithm 'unknown' is unknown",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual, theError := NewFileCryptoHashFromConfig(tc.have)
if tc.err == "" {
assert.NoError(t, theError)
require.NotNil(t, actual)
assert.IsType(t, tc.expected, actual)
} else {
assert.EqualError(t, theError, tc.err)
assert.Nil(t, actual)
}
})
}
}
func TestHashError(t *testing.T) {
WithDatabase(t, UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Search.CaseInsensitive = true
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := NewMockHash(ctrl)
provider.hash = mock
mock.EXPECT().Hash("apple123").Return(nil, fmt.Errorf("failed to mock hash"))
assert.EqualError(t, provider.UpdatePassword("john", "apple123"), "failed to mock hash")
})
}
func TestDatabaseError(t *testing.T) {
WithDatabase(t, UserDatabaseContent, func(path string) {
db := NewYAMLUserDatabase(path, false, false)
assert.NoError(t, db.Load())
config := DefaultFileAuthenticationBackendConfiguration
config.Search.CaseInsensitive = true
config.Path = path
provider := NewFileUserProvider(&config)
assert.NoError(t, provider.StartupCheck())
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := NewMockFileUserDatabase(ctrl)
provider.database = mock
gomock.InOrder(
mock.EXPECT().GetUserDetails("john").Return(db.GetUserDetails("john")),
mock.EXPECT().SetUserDetails("john", gomock.Any()),
mock.EXPECT().Save().Return(fmt.Errorf("failed to mock save")),
)
assert.EqualError(t, provider.UpdatePassword("john", "apple123"), "failed to mock save")
})
}
var ( var (
DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackend{ DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackend{
Path: "", Path: "",
@ -657,3 +937,17 @@ users:
- admins - admins
- dev - dev
`) `)
func WithDatabase(t *testing.T, content []byte, f func(path string)) {
dir := t.TempDir()
db, err := os.CreateTemp(dir, "users_database.*.yaml")
require.NoError(t, err)
_, err = db.Write(content)
require.NoError(t, err)
f(db.Name())
require.NoError(t, db.Close())
}

View File

@ -5,3 +5,5 @@ package authentication
//go:generate mockgen -package authentication -destination ldap_client_mock.go -mock_names LDAPClient=MockLDAPClient github.com/authelia/authelia/v4/internal/authentication LDAPClient //go:generate mockgen -package authentication -destination ldap_client_mock.go -mock_names LDAPClient=MockLDAPClient github.com/authelia/authelia/v4/internal/authentication LDAPClient
//go:generate mockgen -package authentication -destination ldap_client_factory_mock.go -mock_names LDAPClientFactory=MockLDAPClientFactory github.com/authelia/authelia/v4/internal/authentication LDAPClientFactory //go:generate mockgen -package authentication -destination ldap_client_factory_mock.go -mock_names LDAPClientFactory=MockLDAPClientFactory github.com/authelia/authelia/v4/internal/authentication LDAPClientFactory
//go:generate mockgen -package authentication -destination file_user_provider_database_mock_test.go -mock_names FileUserDatabase=MockFileUserDatabase github.com/authelia/authelia/v4/internal/authentication FileUserDatabase
//go:generate mockgen -package authentication -destination file_user_provider_hash_mock_test.go -mock_names Hash=MockHash github.com/go-crypt/crypt/algorithm Hash

View File

@ -215,7 +215,7 @@ func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error)
modifyRequest := ldap.NewModifyRequest(profile.DN, controls) modifyRequest := ldap.NewModifyRequest(profile.DN, controls)
// The password needs to be enclosed in quotes // The password needs to be enclosed in quotes
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2 // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2
pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", password)) pwdEncoded, _ := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", password))
modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded})
err = p.modify(client, modifyRequest) err = p.modify(client, modifyRequest)

View File

@ -1604,7 +1604,7 @@ func TestShouldUpdateUserPasswordMSAD(t *testing.T) {
[]ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}},
) )
pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) pwdEncoded, _ := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password"))
modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded})
dialURLOIDs := mockFactory.EXPECT(). dialURLOIDs := mockFactory.EXPECT().
@ -1715,7 +1715,7 @@ func TestShouldUpdateUserPasswordMSADWithReferrals(t *testing.T) {
[]ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}},
) )
pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) pwdEncoded, _ := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password"))
modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded})
dialURLOIDs := mockFactory.EXPECT(). dialURLOIDs := mockFactory.EXPECT().
@ -1843,7 +1843,7 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralConnectErr(t *test
[]ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}},
) )
pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) pwdEncoded, _ := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password"))
modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded})
dialURLOIDs := mockFactory.EXPECT(). dialURLOIDs := mockFactory.EXPECT().
@ -1962,7 +1962,7 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralModifyErr(t *testi
[]ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}},
) )
pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) pwdEncoded, _ := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password"))
modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded})
dialURLOIDs := mockFactory.EXPECT(). dialURLOIDs := mockFactory.EXPECT().
@ -2094,7 +2094,7 @@ func TestShouldUpdateUserPasswordMSADWithoutReferrals(t *testing.T) {
[]ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}},
) )
pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) pwdEncoded, _ := encodingUTF16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password"))
modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded})
dialURLOIDs := mockFactory.EXPECT(). dialURLOIDs := mockFactory.EXPECT().

View File

@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"golang.org/x/text/encoding/unicode"
) )
// LDAPClientFactory an interface of factory of LDAP clients. // LDAPClientFactory an interface of factory of LDAP clients.
@ -103,4 +102,30 @@ type LDAPSupportedControlTypes struct {
MsftPwdPolHintsDeprecated bool MsftPwdPolHintsDeprecated bool
} }
var utf16LittleEndian = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) // Level is the type representing a level of authentication.
type Level int
const (
// NotAuthenticated if the user is not authenticated yet.
NotAuthenticated Level = iota
// OneFactor if the user has passed first factor only.
OneFactor
// TwoFactor if the user has passed two factors.
TwoFactor
)
// String returns a string representation of an authentication.Level.
func (l Level) String() string {
switch l {
case NotAuthenticated:
return "not_authenticated"
case OneFactor:
return "one_factor"
case TwoFactor:
return "two_factor"
default:
return "invalid"
}
}

View File

@ -0,0 +1,42 @@
package authentication
import (
"net/mail"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserDetails_Addresses(t *testing.T) {
details := &UserDetails{}
assert.Equal(t, []mail.Address(nil), details.Addresses())
details = &UserDetails{
DisplayName: "Example",
Emails: []string{"abc@123.com"},
}
assert.Equal(t, []mail.Address{{Name: "Example", Address: "abc@123.com"}}, details.Addresses())
details = &UserDetails{
DisplayName: "Example",
Emails: []string{"abc@123.com", "two@apple.com"},
}
assert.Equal(t, []mail.Address{{Name: "Example", Address: "abc@123.com"}, {Name: "Example", Address: "two@apple.com"}}, details.Addresses())
details = &UserDetails{
DisplayName: "",
Emails: []string{"abc@123.com"},
}
assert.Equal(t, []mail.Address{{Address: "abc@123.com"}}, details.Addresses())
}
func TestLevel_String(t *testing.T) {
assert.Equal(t, "one_factor", OneFactor.String())
assert.Equal(t, "two_factor", TwoFactor.String())
assert.Equal(t, "not_authenticated", NotAuthenticated.String())
assert.Equal(t, "invalid", Level(-1).String())
}

View File

@ -1,15 +0,0 @@
package authentication
// String returns a string representation of an authentication.Level.
func (l Level) String() string {
switch l {
case NotAuthenticated:
return "not_authenticated"
case OneFactor:
return "one_factor"
case TwoFactor:
return "two_factor"
default:
return "invalid"
}
}

View File

@ -66,13 +66,13 @@ func (m AccessControlDomainMatcher) IsMatch(domain string, subject Subject) (mat
return strings.HasSuffix(domain, m.Name) return strings.HasSuffix(domain, m.Name)
case m.UserWildcard: case m.UserWildcard:
if subject.IsAnonymous() && strings.HasSuffix(domain, m.Name) { if subject.IsAnonymous() && strings.HasSuffix(domain, m.Name) {
return true return len(domain) > len(m.Name)
} }
return domain == fmt.Sprintf("%s%s", subject.Username, m.Name) return domain == fmt.Sprintf("%s%s", subject.Username, m.Name)
case m.GroupWildcard: case m.GroupWildcard:
if subject.IsAnonymous() && strings.HasSuffix(domain, m.Name) { if subject.IsAnonymous() && strings.HasSuffix(domain, m.Name) {
return true return len(domain) > len(m.Name)
} }
i := strings.Index(domain, ".") i := strings.Index(domain, ".")

View File

@ -0,0 +1,64 @@
package authorization
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAccessControlDomain_IsMatch(t *testing.T) {
testCases := []struct {
name string
have *AccessControlDomainMatcher
domain string
subject Subject
expected bool
}{
{
"ShouldMatchDomainSuffixUserWildcard",
&AccessControlDomainMatcher{
Name: "-user.domain.com",
UserWildcard: true,
},
"a-user.domain.com",
Subject{},
true,
},
{
"ShouldMatchDomainSuffixGroupWildcard",
&AccessControlDomainMatcher{
Name: "-group.domain.com",
GroupWildcard: true,
},
"a-group.domain.com",
Subject{},
true,
},
{
"ShouldNotMatchExactDomainWithUserWildcard",
&AccessControlDomainMatcher{
Name: "-user.domain.com",
UserWildcard: true,
},
"-user.domain.com",
Subject{},
false,
},
{
"ShouldMatchWildcard",
&AccessControlDomainMatcher{
Name: "user.domain.com",
Wildcard: true,
},
"abc.user.domain.com",
Subject{},
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.have.IsMatch(tc.domain, tc.subject))
})
}
}

View File

@ -0,0 +1,62 @@
package authorization
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func TestNewAccessControlQuery(t *testing.T) {
testCases := []struct {
name string
have [][]schema.ACLQueryRule
expected []AccessControlQuery
matches [][]Object
}{
{
"ShouldSkipInvalidTypeEqual",
[][]schema.ACLQueryRule{
{
{Operator: operatorEqual, Key: "example", Value: 1},
},
},
[]AccessControlQuery{{Rules: []ObjectMatcher(nil)}},
[][]Object{{{}}},
},
{
"ShouldSkipInvalidTypePattern",
[][]schema.ACLQueryRule{
{
{Operator: operatorPattern, Key: "example", Value: 1},
},
},
[]AccessControlQuery{{Rules: []ObjectMatcher(nil)}},
[][]Object{{{}}},
},
{
"ShouldSkipInvalidOperator",
[][]schema.ACLQueryRule{
{
{Operator: "nop", Key: "example", Value: 1},
},
},
[]AccessControlQuery{{Rules: []ObjectMatcher(nil)}},
[][]Object{{{}}},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := NewAccessControlQuery(tc.have)
assert.Equal(t, tc.expected, actual)
for i, rule := range actual {
for _, object := range tc.matches[i] {
assert.True(t, rule.IsMatch(object))
}
}
})
}
}

View File

@ -0,0 +1,33 @@
package authorization
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAccessControlRule_MatchesSubjectExact(t *testing.T) {
testCases := []struct {
name string
have *AccessControlRule
subject Subject
expected bool
}{
{
"ShouldNotMatchAnonymous",
&AccessControlRule{
Subjects: []AccessControlSubjects{
{[]SubjectMatcher{schemaSubjectToACLSubject("user:john")}},
},
},
Subject{},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.have.MatchesSubjectExact(tc.subject))
})
}
}

View File

@ -1069,9 +1069,9 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithOIDC(t *testing.T) {
}, },
}, },
}, },
IdentityProviders: schema.IdentityProvidersConfiguration{ IdentityProviders: schema.IdentityProviders{
OIDC: &schema.OpenIDConnectConfiguration{ OIDC: &schema.OpenIDConnect{
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
Policy: oneFactor, Policy: oneFactor,
}, },

View File

@ -16,7 +16,8 @@ type RegexpGroupStringSubjectMatcher struct {
// IsMatch returns true if the underlying pattern matches the input given the subject. // IsMatch returns true if the underlying pattern matches the input given the subject.
func (r RegexpGroupStringSubjectMatcher) IsMatch(input string, subject Subject) (match bool) { func (r RegexpGroupStringSubjectMatcher) IsMatch(input string, subject Subject) (match bool) {
if !r.Pattern.MatchString(input) { matches := r.Pattern.FindStringSubmatch(input)
if matches == nil {
return false return false
} }
@ -24,16 +25,11 @@ func (r RegexpGroupStringSubjectMatcher) IsMatch(input string, subject Subject)
return true return true
} }
matches := r.Pattern.FindAllStringSubmatch(input, -1) if r.SubexpNameUser != -1 && !strings.EqualFold(subject.Username, matches[r.SubexpNameUser]) {
if matches == nil {
return false return false
} }
if r.SubexpNameUser != -1 && !strings.EqualFold(subject.Username, matches[0][r.SubexpNameUser]) { if r.SubexpNameGroup != -1 && !utils.IsStringInSliceFold(matches[r.SubexpNameGroup], subject.Groups) {
return false
}
if r.SubexpNameGroup != -1 && !utils.IsStringInSliceFold(matches[0][r.SubexpNameGroup], subject.Groups) {
return false return false
} }

View File

@ -0,0 +1,42 @@
package authorization
import (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRegexpGroupStringSubjectMatcher_IsMatch(t *testing.T) {
testCases := []struct {
name string
have *RegexpGroupStringSubjectMatcher
input string
subject Subject
expected bool
}{
{
"Abc",
&RegexpGroupStringSubjectMatcher{
MustCompileRegexNoPtr(`^(?P<User>[a-zA-Z0-9]+)\.regex.com$`),
1,
0,
},
"example.com",
Subject{Username: "a-user"},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.have.IsMatch(tc.input, tc.subject))
})
}
}
func MustCompileRegexNoPtr(input string) regexp.Regexp {
out := regexp.MustCompile(input)
return *out
}

View File

@ -85,3 +85,33 @@ func TestShouldCleanURL(t *testing.T) {
}) })
} }
} }
func TestRuleMatchResult_IsPotentialMatch(t *testing.T) {
testCases := []struct {
name string
have RuleMatchResult
expected bool
}{
{
"ShouldNotMatch",
RuleMatchResult{},
false,
},
{
"ShouldMatch",
RuleMatchResult{nil, true, true, true, true, true, true, true, false},
true,
},
{
"ShouldMatchExact",
RuleMatchResult{nil, true, true, true, true, true, true, true, true},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.have.IsPotentialMatch())
})
}
}

View File

@ -2,6 +2,7 @@ package authorization
import ( import (
"net" "net"
"regexp"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -217,3 +218,76 @@ func TestIsAuthLevelSufficient(t *testing.T) {
assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, TwoFactor)) assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, TwoFactor))
assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, TwoFactor)) assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, TwoFactor))
} }
func TestStringSliceToRegexpSlice(t *testing.T) {
testCases := []struct {
name string
have []string
expected []regexp.Regexp
err string
}{
{
"ShouldNotParseBadRegex",
[]string{`\q`},
[]regexp.Regexp(nil),
"error parsing regexp: invalid escape sequence: `\\q`",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual, theError := stringSliceToRegexpSlice(tc.have)
assert.Equal(t, tc.expected, actual)
if tc.err == "" {
assert.NoError(t, theError)
} else {
assert.EqualError(t, theError, tc.err)
}
})
}
}
func TestSchemaNetworksToACL(t *testing.T) {
testCases := []struct {
name string
have []string
globals map[string][]*net.IPNet
cache map[string]*net.IPNet
expected []*net.IPNet
}{
{
"ShouldLoadFromCache",
[]string{"192.168.0.0/24"},
nil,
map[string]*net.IPNet{"192.168.0.0/24": MustParseCIDR("192.168.0.0/24")},
[]*net.IPNet{MustParseCIDR("192.168.0.0/24")},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.globals == nil {
tc.globals = map[string][]*net.IPNet{}
}
if tc.cache == nil {
tc.cache = map[string]*net.IPNet{}
}
actual := schemaNetworksToACL(tc.have, tc.globals, tc.cache)
assert.Equal(t, tc.expected, actual)
})
}
}
func MustParseCIDR(input string) *net.IPNet {
_, out, err := net.ParseCIDR(input)
if err != nil {
panic(err)
}
return out
}

View File

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

View File

@ -6,13 +6,13 @@ import (
"time" "time"
) )
// IdentityProvidersConfiguration represents the IdentityProviders 2.0 configuration for Authelia. // IdentityProviders represents the Identity Providers configuration for Authelia.
type IdentityProvidersConfiguration struct { type IdentityProviders struct {
OIDC *OpenIDConnectConfiguration `koanf:"oidc"` OIDC *OpenIDConnect `koanf:"oidc"`
} }
// OpenIDConnectConfiguration configuration for OpenID Connect. // OpenIDConnect configuration for OpenID Connect 1.0.
type OpenIDConnectConfiguration struct { type OpenIDConnect struct {
HMACSecret string `koanf:"hmac_secret"` HMACSecret string `koanf:"hmac_secret"`
IssuerPrivateKeys []JWK `koanf:"issuer_private_keys"` IssuerPrivateKeys []JWK `koanf:"issuer_private_keys"`
@ -30,36 +30,39 @@ type OpenIDConnectConfiguration struct {
EnforcePKCE string `koanf:"enforce_pkce"` EnforcePKCE string `koanf:"enforce_pkce"`
EnablePKCEPlainChallenge bool `koanf:"enable_pkce_plain_challenge"` EnablePKCEPlainChallenge bool `koanf:"enable_pkce_plain_challenge"`
PAR OpenIDConnectPARConfiguration `koanf:"pushed_authorizations"` PAR OpenIDConnectPAR `koanf:"pushed_authorizations"`
CORS OpenIDConnectCORSConfiguration `koanf:"cors"` CORS OpenIDConnectCORS `koanf:"cors"`
Clients []OpenIDConnectClientConfiguration `koanf:"clients"` Clients []OpenIDConnectClient `koanf:"clients"`
Discovery OpenIDConnectDiscovery // MetaData value. Not configurable by users. Discovery OpenIDConnectDiscovery // MetaData value. Not configurable by users.
} }
// OpenIDConnectDiscovery is information discovered during validation reused for the discovery handlers.
type OpenIDConnectDiscovery struct { type OpenIDConnectDiscovery struct {
DefaultKeyID string DefaultKeyIDs map[string]string
ResponseObjectSigningAlgs []string DefaultKeyID string
RequestObjectSigningAlgs []string ResponseObjectSigningKeyIDs []string
ResponseObjectSigningAlgs []string
RequestObjectSigningAlgs []string
} }
// OpenIDConnectPARConfiguration represents an OpenID Connect PAR config. // OpenIDConnectPAR represents an OpenID Connect 1.0 PAR config.
type OpenIDConnectPARConfiguration struct { type OpenIDConnectPAR struct {
Enforce bool `koanf:"enforce"` Enforce bool `koanf:"enforce"`
ContextLifespan time.Duration `koanf:"context_lifespan"` ContextLifespan time.Duration `koanf:"context_lifespan"`
} }
// OpenIDConnectCORSConfiguration represents an OpenID Connect CORS config. // OpenIDConnectCORS represents an OpenID Connect 1.0 CORS config.
type OpenIDConnectCORSConfiguration struct { type OpenIDConnectCORS struct {
Endpoints []string `koanf:"endpoints"` Endpoints []string `koanf:"endpoints"`
AllowedOrigins []url.URL `koanf:"allowed_origins"` AllowedOrigins []url.URL `koanf:"allowed_origins"`
AllowedOriginsFromClientRedirectURIs bool `koanf:"allowed_origins_from_client_redirect_uris"` AllowedOriginsFromClientRedirectURIs bool `koanf:"allowed_origins_from_client_redirect_uris"`
} }
// OpenIDConnectClientConfiguration configuration for an OpenID Connect client. // OpenIDConnectClient represents a configuration for an OpenID Connect 1.0 client.
type OpenIDConnectClientConfiguration struct { type OpenIDConnectClient struct {
ID string `koanf:"id"` ID string `koanf:"id"`
Description string `koanf:"description"` Description string `koanf:"description"`
Secret *PasswordDigest `koanf:"secret"` Secret *PasswordDigest `koanf:"secret"`
@ -84,25 +87,28 @@ type OpenIDConnectClientConfiguration struct {
PKCEChallengeMethod string `koanf:"pkce_challenge_method"` PKCEChallengeMethod string `koanf:"pkce_challenge_method"`
TokenEndpointAuthMethod string `koanf:"token_endpoint_auth_method"`
TokenEndpointAuthSigningAlg string `koanf:"token_endpoint_auth_signing_alg"`
RequestObjectSigningAlg string `koanf:"request_object_signing_alg"`
IDTokenSigningAlg string `koanf:"id_token_signing_alg"` IDTokenSigningAlg string `koanf:"id_token_signing_alg"`
IDTokenSigningKeyID string `koanf:"id_token_signing_key_id"`
UserinfoSigningAlg string `koanf:"userinfo_signing_alg"` UserinfoSigningAlg string `koanf:"userinfo_signing_alg"`
UserinfoSigningKeyID string `koanf:"userinfo_signing_key_id"`
RequestObjectSigningAlg string `koanf:"request_object_signing_alg"`
TokenEndpointAuthSigningAlg string `koanf:"token_endpoint_auth_signing_alg"`
TokenEndpointAuthMethod string `koanf:"token_endpoint_auth_method"`
PublicKeys OpenIDConnectClientPublicKeys `koanf:"public_keys"` PublicKeys OpenIDConnectClientPublicKeys `koanf:"public_keys"`
Discovery OpenIDConnectDiscovery Discovery OpenIDConnectDiscovery
} }
// OpenIDConnectClientPublicKeys represents the Client Public Keys configuration for an OpenID Connect 1.0 client.
type OpenIDConnectClientPublicKeys struct { type OpenIDConnectClientPublicKeys struct {
URI *url.URL `koanf:"uri"` URI *url.URL `koanf:"uri"`
Values []JWK `koanf:"values"` Values []JWK `koanf:"values"`
} }
// DefaultOpenIDConnectConfiguration contains defaults for OIDC. // DefaultOpenIDConnectConfiguration contains defaults for OIDC.
var DefaultOpenIDConnectConfiguration = OpenIDConnectConfiguration{ var DefaultOpenIDConnectConfiguration = OpenIDConnect{
AccessTokenLifespan: time.Hour, AccessTokenLifespan: time.Hour,
AuthorizeCodeLifespan: time.Minute, AuthorizeCodeLifespan: time.Minute,
IDTokenLifespan: time.Hour, IDTokenLifespan: time.Hour,
@ -113,12 +119,11 @@ var DefaultOpenIDConnectConfiguration = OpenIDConnectConfiguration{
var defaultOIDCClientConsentPreConfiguredDuration = time.Hour * 24 * 7 var defaultOIDCClientConsentPreConfiguredDuration = time.Hour * 24 * 7
// DefaultOpenIDConnectClientConfiguration contains defaults for OIDC Clients. // DefaultOpenIDConnectClientConfiguration contains defaults for OIDC Clients.
var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{ var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClient{
Policy: "two_factor", Policy: "two_factor",
Scopes: []string{"openid", "groups", "profile", "email"}, Scopes: []string{"openid", "groups", "profile", "email"},
ResponseTypes: []string{"code"}, ResponseTypes: []string{"code"},
ResponseModes: []string{"form_post"}, ResponseModes: []string{"form_post"},
IDTokenSigningAlg: "RS256", IDTokenSigningAlg: "RS256",
UserinfoSigningAlg: "none", UserinfoSigningAlg: "none",
ConsentMode: "auto", ConsentMode: "auto",

View File

@ -20,7 +20,7 @@ var Keys = []string{
"identity_providers.oidc.hmac_secret", "identity_providers.oidc.hmac_secret",
"identity_providers.oidc.issuer_private_keys", "identity_providers.oidc.issuer_private_keys",
"identity_providers.oidc.issuer_private_keys[].key_id", "identity_providers.oidc.issuer_private_keys[].key_id",
"identity_providers.oidc.issuer_private_keys[]", "identity_providers.oidc.issuer_private_keys[].use",
"identity_providers.oidc.issuer_private_keys[].algorithm", "identity_providers.oidc.issuer_private_keys[].algorithm",
"identity_providers.oidc.issuer_private_keys[].key", "identity_providers.oidc.issuer_private_keys[].key",
"identity_providers.oidc.issuer_private_keys[].certificate_chain", "identity_providers.oidc.issuer_private_keys[].certificate_chain",
@ -57,15 +57,17 @@ var Keys = []string{
"identity_providers.oidc.clients[].enforce_par", "identity_providers.oidc.clients[].enforce_par",
"identity_providers.oidc.clients[].enforce_pkce", "identity_providers.oidc.clients[].enforce_pkce",
"identity_providers.oidc.clients[].pkce_challenge_method", "identity_providers.oidc.clients[].pkce_challenge_method",
"identity_providers.oidc.clients[].token_endpoint_auth_method",
"identity_providers.oidc.clients[].token_endpoint_auth_signing_alg",
"identity_providers.oidc.clients[].request_object_signing_alg",
"identity_providers.oidc.clients[].id_token_signing_alg", "identity_providers.oidc.clients[].id_token_signing_alg",
"identity_providers.oidc.clients[].id_token_signing_key_id",
"identity_providers.oidc.clients[].userinfo_signing_alg", "identity_providers.oidc.clients[].userinfo_signing_alg",
"identity_providers.oidc.clients[].userinfo_signing_key_id",
"identity_providers.oidc.clients[].request_object_signing_alg",
"identity_providers.oidc.clients[].token_endpoint_auth_signing_alg",
"identity_providers.oidc.clients[].token_endpoint_auth_method",
"identity_providers.oidc.clients[].public_keys.uri", "identity_providers.oidc.clients[].public_keys.uri",
"identity_providers.oidc.clients[].public_keys.values", "identity_providers.oidc.clients[].public_keys.values",
"identity_providers.oidc.clients[].public_keys.values[].key_id", "identity_providers.oidc.clients[].public_keys.values[].key_id",
"identity_providers.oidc.clients[].public_keys.values[]", "identity_providers.oidc.clients[].public_keys.values[].use",
"identity_providers.oidc.clients[].public_keys.values[].algorithm", "identity_providers.oidc.clients[].public_keys.values[].algorithm",
"identity_providers.oidc.clients[].public_keys.values[].key", "identity_providers.oidc.clients[].public_keys.values[].key",
"identity_providers.oidc.clients[].public_keys.values[].certificate_chain", "identity_providers.oidc.clients[].public_keys.values[].certificate_chain",

View File

@ -37,8 +37,8 @@ type ServerBuffers struct {
// JWK represents a JWK. // JWK represents a JWK.
type JWK struct { type JWK struct {
KeyID string `koanf:"key_id"` KeyID string `koanf:"key_id"`
Use string Use string `koanf:"use"`
Algorithm string `koanf:"algorithm"` Algorithm string `koanf:"algorithm"`
Key CryptographicKey `koanf:"key"` Key CryptographicKey `koanf:"key"`
CertificateChain X509CertificateChain `koanf:"certificate_chain"` CertificateChain X509CertificateChain `koanf:"certificate_chain"`

View File

@ -147,13 +147,15 @@ const (
errFmtOIDCProviderNoPrivateKey = "identity_providers: oidc: option `issuer_private_keys` or 'issuer_private_key' is required" errFmtOIDCProviderNoPrivateKey = "identity_providers: oidc: option `issuer_private_keys` or 'issuer_private_key' is required"
errFmtOIDCProviderEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " + errFmtOIDCProviderEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " +
"'public_clients_only' or 'always', but it's configured as '%s'" "'public_clients_only' or 'always', but it's configured as '%s'"
errFmtOIDCProviderInsecureParameterEntropy = "openid connect provider: SECURITY ISSUE - minimum parameter entropy is " + errFmtOIDCProviderInsecureParameterEntropy = "identity_providers: oidc: option 'minimum_parameter_entropy' is " +
"configured to an unsafe value, it should be above 8 but it's configured to %d" "configured to an unsafe and insecure value, it should at least be %d but it's configured to %d"
errFmtOIDCProviderInsecureDisabledParameterEntropy = "identity_providers: oidc: option 'minimum_parameter_entropy' is " +
"disabled which is considered unsafe and insecure"
errFmtOIDCProviderPrivateKeysInvalid = "identity_providers: oidc: issuer_private_keys: key #%d: option 'key' must be a valid private key but the provided data is malformed as it's missing the public key bits" errFmtOIDCProviderPrivateKeysInvalid = "identity_providers: oidc: issuer_private_keys: key #%d: option 'key' must be a valid private key but the provided data is malformed as it's missing the public key bits"
errFmtOIDCProviderPrivateKeysCalcThumbprint = "identity_providers: oidc: issuer_private_keys: key #%d: option 'key' failed to calculate thumbprint to configure key id value: %w" errFmtOIDCProviderPrivateKeysCalcThumbprint = "identity_providers: oidc: issuer_private_keys: key #%d: option 'key' failed to calculate thumbprint to configure key id value: %w"
errFmtOIDCProviderPrivateKeysKeyIDLength = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option `key_id`` must be 7 characters or less" errFmtOIDCProviderPrivateKeysKeyIDLength = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option `key_id` must be 100 characters or less"
errFmtOIDCProviderPrivateKeysAttributeNotUnique = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option '%s' must be unique" errFmtOIDCProviderPrivateKeysAttributeNotUnique = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option '%s' must be unique"
errFmtOIDCProviderPrivateKeysKeyIDNotAlphaNumeric = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option 'key_id' must only have alphanumeric characters" errFmtOIDCProviderPrivateKeysKeyIDNotValid = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option 'key_id' must only contain RFC3986 unreserved characters and must only start and end with alphanumeric characters"
errFmtOIDCProviderPrivateKeysProperties = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option 'key' failed to get key properties: %w" errFmtOIDCProviderPrivateKeysProperties = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option 'key' failed to get key properties: %w"
errFmtOIDCProviderPrivateKeysInvalidOptionOneOf = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option '%s' must be one of %s but it's configured as '%s'" errFmtOIDCProviderPrivateKeysInvalidOptionOneOf = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option '%s' must be one of %s but it's configured as '%s'"
errFmtOIDCProviderPrivateKeysRSAKeyLessThan2048Bits = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option 'key' is an RSA %d bit private key but it must at minimum be a RSA 2048 bit private key" errFmtOIDCProviderPrivateKeysRSAKeyLessThan2048Bits = "identity_providers: oidc: issuer_private_keys: key #%d with key id '%s': option 'key' is an RSA %d bit private key but it must at minimum be a RSA 2048 bit private key"
@ -436,7 +438,9 @@ const (
attrOIDCRedirectURIs = "redirect_uris" attrOIDCRedirectURIs = "redirect_uris"
attrOIDCTokenAuthMethod = "token_endpoint_auth_method" attrOIDCTokenAuthMethod = "token_endpoint_auth_method"
attrOIDCUsrSigAlg = "userinfo_signing_alg" attrOIDCUsrSigAlg = "userinfo_signing_alg"
attrOIDCUsrSigKID = "userinfo_signing_key_id"
attrOIDCIDTokenSigAlg = "id_token_signing_alg" attrOIDCIDTokenSigAlg = "id_token_signing_alg"
attrOIDCIDTokenSigKID = "id_token_signing_key_id"
attrOIDCPKCEChallengeMethod = "pkce_challenge_method" attrOIDCPKCEChallengeMethod = "pkce_challenge_method"
) )
@ -462,6 +466,7 @@ var (
reKeyReplacer = regexp.MustCompile(`\[\d+]`) reKeyReplacer = regexp.MustCompile(`\[\d+]`)
reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`) reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`)
reAuthzEndpointName = regexp.MustCompile(`^[a-zA-Z](([a-zA-Z0-9/._-]*)([a-zA-Z]))?$`) reAuthzEndpointName = regexp.MustCompile(`^[a-zA-Z](([a-zA-Z0-9/._-]*)([a-zA-Z]))?$`)
reOpenIDConnectKID = regexp.MustCompile(`^([a-zA-Z0-9](([a-zA-Z0-9._~-]*)([a-zA-Z0-9]))?)?$`)
) )
var replacedKeys = map[string]string{ var replacedKeys = map[string]string{

View File

@ -1,9 +1,7 @@
package validator package validator
import ( import (
"crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa" "crypto/rsa"
"fmt" "fmt"
"net/url" "net/url"
@ -12,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
jose "gopkg.in/square/go-jose.v2" "github.com/ory/fosite"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
@ -20,11 +18,11 @@ import (
) )
// ValidateIdentityProviders validates and updates the IdentityProviders configuration. // ValidateIdentityProviders validates and updates the IdentityProviders configuration.
func ValidateIdentityProviders(config *schema.IdentityProvidersConfiguration, val *schema.StructValidator) { func ValidateIdentityProviders(config *schema.IdentityProviders, val *schema.StructValidator) {
validateOIDC(config.OIDC, val) validateOIDC(config.OIDC, val)
} }
func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDC(config *schema.OpenIDConnect, val *schema.StructValidator) {
if config == nil { if config == nil {
return return
} }
@ -35,8 +33,13 @@ func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructV
sort.Sort(oidc.SortedSigningAlgs(config.Discovery.ResponseObjectSigningAlgs)) sort.Sort(oidc.SortedSigningAlgs(config.Discovery.ResponseObjectSigningAlgs))
if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 { switch {
val.PushWarning(fmt.Errorf(errFmtOIDCProviderInsecureParameterEntropy, config.MinimumParameterEntropy)) case config.MinimumParameterEntropy == -1:
val.PushWarning(fmt.Errorf(errFmtOIDCProviderInsecureDisabledParameterEntropy))
case config.MinimumParameterEntropy <= 0:
config.MinimumParameterEntropy = fosite.MinParameterEntropy
case config.MinimumParameterEntropy < fosite.MinParameterEntropy:
val.PushWarning(fmt.Errorf(errFmtOIDCProviderInsecureParameterEntropy, fosite.MinParameterEntropy, config.MinimumParameterEntropy))
} }
switch config.EnforcePKCE { switch config.EnforcePKCE {
@ -55,7 +58,7 @@ func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructV
} }
} }
func validateOIDCIssuer(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCIssuer(config *schema.OpenIDConnect, val *schema.StructValidator) {
switch { switch {
case config.IssuerPrivateKey != nil: case config.IssuerPrivateKey != nil:
validateOIDCIssuerPrivateKey(config) validateOIDCIssuerPrivateKey(config)
@ -68,7 +71,7 @@ func validateOIDCIssuer(config *schema.OpenIDConnectConfiguration, val *schema.S
} }
} }
func validateOIDCIssuerPrivateKey(config *schema.OpenIDConnectConfiguration) { func validateOIDCIssuerPrivateKey(config *schema.OpenIDConnect) {
config.IssuerPrivateKeys = append([]schema.JWK{{ config.IssuerPrivateKeys = append([]schema.JWK{{
Algorithm: oidc.SigningAlgRSAUsingSHA256, Algorithm: oidc.SigningAlgRSAUsingSHA256,
Use: oidc.KeyUseSignature, Use: oidc.KeyUseSignature,
@ -77,34 +80,14 @@ func validateOIDCIssuerPrivateKey(config *schema.OpenIDConnectConfiguration) {
}}, config.IssuerPrivateKeys...) }}, config.IssuerPrivateKeys...)
} }
func jwkCalculateThumbprint(key schema.CryptographicKey) (thumbprintStr string, err error) { func validateOIDCIssuerPrivateKeys(config *schema.OpenIDConnect, val *schema.StructValidator) {
j := jose.JSONWebKey{}
switch k := key.(type) {
case schema.CryptographicPrivateKey:
j.Key = k.Public()
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
j.Key = k
default:
return "", nil
}
var thumbprint []byte
if thumbprint, err = j.Thumbprint(crypto.SHA256); err != nil {
return "", err
}
return fmt.Sprintf("%x", thumbprint)[:6], nil
}
func validateOIDCIssuerPrivateKeys(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) {
var ( var (
props *JWKProperties props *JWKProperties
err error err error
) )
kids := make([]string, len(config.IssuerPrivateKeys)) config.Discovery.ResponseObjectSigningKeyIDs = make([]string, len(config.IssuerPrivateKeys))
config.Discovery.DefaultKeyIDs = map[string]string{}
for i := 0; i < len(config.IssuerPrivateKeys); i++ { for i := 0; i < len(config.IssuerPrivateKeys); i++ {
if key, ok := config.IssuerPrivateKeys[i].Key.(*rsa.PrivateKey); ok && key.PublicKey.N == nil { if key, ok := config.IssuerPrivateKeys[i].Key.(*rsa.PrivateKey); ok && key.PublicKey.N == nil {
@ -120,18 +103,18 @@ func validateOIDCIssuerPrivateKeys(config *schema.OpenIDConnectConfiguration, va
continue continue
} }
case n > 7: case n > 100:
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDLength, i+1, config.IssuerPrivateKeys[i].KeyID)) val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDLength, i+1, config.IssuerPrivateKeys[i].KeyID))
} }
if config.IssuerPrivateKeys[i].KeyID != "" && utils.IsStringInSlice(config.IssuerPrivateKeys[i].KeyID, kids) { if config.IssuerPrivateKeys[i].KeyID != "" && utils.IsStringInSlice(config.IssuerPrivateKeys[i].KeyID, config.Discovery.ResponseObjectSigningKeyIDs) {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysAttributeNotUnique, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCKeyID)) val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysAttributeNotUnique, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCKeyID))
} }
kids[i] = config.IssuerPrivateKeys[i].KeyID config.Discovery.ResponseObjectSigningKeyIDs[i] = config.IssuerPrivateKeys[i].KeyID
if !utils.IsStringAlphaNumeric(config.IssuerPrivateKeys[i].KeyID) { if !reOpenIDConnectKID.MatchString(config.IssuerPrivateKeys[i].KeyID) {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDNotAlphaNumeric, i+1, config.IssuerPrivateKeys[i].KeyID)) val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDNotValid, i+1, config.IssuerPrivateKeys[i].KeyID))
} }
if props, err = schemaJWKGetProperties(config.IssuerPrivateKeys[i]); err != nil { if props, err = schemaJWKGetProperties(config.IssuerPrivateKeys[i]); err != nil {
@ -149,7 +132,7 @@ func validateOIDCIssuerPrivateKeys(config *schema.OpenIDConnectConfiguration, va
} }
} }
func validateOIDCIssuerPrivateKeysUseAlg(i int, props *JWKProperties, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCIssuerPrivateKeysUseAlg(i int, props *JWKProperties, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch config.IssuerPrivateKeys[i].Use { switch config.IssuerPrivateKeys[i].Use {
case "": case "":
config.IssuerPrivateKeys[i].Use = props.Use config.IssuerPrivateKeys[i].Use = props.Use
@ -162,26 +145,26 @@ func validateOIDCIssuerPrivateKeysUseAlg(i int, props *JWKProperties, config *sc
switch { switch {
case config.IssuerPrivateKeys[i].Algorithm == "": case config.IssuerPrivateKeys[i].Algorithm == "":
config.IssuerPrivateKeys[i].Algorithm = props.Algorithm config.IssuerPrivateKeys[i].Algorithm = props.Algorithm
fallthrough
case utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, validOIDCIssuerJWKSigningAlgs): case utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, validOIDCIssuerJWKSigningAlgs):
break if config.IssuerPrivateKeys[i].KeyID != "" && config.IssuerPrivateKeys[i].Algorithm != "" {
if _, ok := config.Discovery.DefaultKeyIDs[config.IssuerPrivateKeys[i].Algorithm]; !ok {
config.Discovery.DefaultKeyIDs[config.IssuerPrivateKeys[i].Algorithm] = config.IssuerPrivateKeys[i].KeyID
}
}
default: default:
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysInvalidOptionOneOf, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCAlgorithm, strJoinOr(validOIDCIssuerJWKSigningAlgs), config.IssuerPrivateKeys[i].Algorithm)) val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysInvalidOptionOneOf, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCAlgorithm, strJoinOr(validOIDCIssuerJWKSigningAlgs), config.IssuerPrivateKeys[i].Algorithm))
} }
if config.IssuerPrivateKeys[i].Algorithm != "" { if config.IssuerPrivateKeys[i].Algorithm != "" {
if utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, config.Discovery.ResponseObjectSigningAlgs) { if !utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, config.Discovery.ResponseObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysAttributeNotUnique, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCAlgorithm))
} else {
config.Discovery.ResponseObjectSigningAlgs = append(config.Discovery.ResponseObjectSigningAlgs, config.IssuerPrivateKeys[i].Algorithm) config.Discovery.ResponseObjectSigningAlgs = append(config.Discovery.ResponseObjectSigningAlgs, config.IssuerPrivateKeys[i].Algorithm)
} }
} }
if config.IssuerPrivateKeys[i].Algorithm == oidc.SigningAlgRSAUsingSHA256 && config.Discovery.DefaultKeyID == "" {
config.Discovery.DefaultKeyID = config.IssuerPrivateKeys[i].KeyID
}
} }
func validateOIDCIssuerPrivateKeyPair(i int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCIssuerPrivateKeyPair(i int, config *schema.OpenIDConnect, val *schema.StructValidator) {
var ( var (
checkEqualKey bool checkEqualKey bool
err error err error
@ -213,7 +196,7 @@ func validateOIDCIssuerPrivateKeyPair(i int, config *schema.OpenIDConnectConfigu
} }
} }
func setOIDCDefaults(config *schema.OpenIDConnectConfiguration) { func setOIDCDefaults(config *schema.OpenIDConnect) {
if config.AccessTokenLifespan == time.Duration(0) { if config.AccessTokenLifespan == time.Duration(0) {
config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan
} }
@ -235,7 +218,7 @@ func setOIDCDefaults(config *schema.OpenIDConnectConfiguration) {
} }
} }
func validateOIDCOptionsCORS(config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { func validateOIDCOptionsCORS(config *schema.OpenIDConnect, validator *schema.StructValidator) {
validateOIDCOptionsCORSAllowedOrigins(config, validator) validateOIDCOptionsCORSAllowedOrigins(config, validator)
if config.CORS.AllowedOriginsFromClientRedirectURIs { if config.CORS.AllowedOriginsFromClientRedirectURIs {
@ -245,7 +228,7 @@ func validateOIDCOptionsCORS(config *schema.OpenIDConnectConfiguration, validato
validateOIDCOptionsCORSEndpoints(config, validator) validateOIDCOptionsCORSEndpoints(config, validator)
} }
func validateOIDCOptionsCORSAllowedOrigins(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCOptionsCORSAllowedOrigins(config *schema.OpenIDConnect, val *schema.StructValidator) {
for _, origin := range config.CORS.AllowedOrigins { for _, origin := range config.CORS.AllowedOrigins {
if origin.String() == "*" { if origin.String() == "*" {
if len(config.CORS.AllowedOrigins) != 1 { if len(config.CORS.AllowedOrigins) != 1 {
@ -269,7 +252,7 @@ func validateOIDCOptionsCORSAllowedOrigins(config *schema.OpenIDConnectConfigura
} }
} }
func validateOIDCOptionsCORSAllowedOriginsFromClientRedirectURIs(config *schema.OpenIDConnectConfiguration) { func validateOIDCOptionsCORSAllowedOriginsFromClientRedirectURIs(config *schema.OpenIDConnect) {
for _, client := range config.Clients { for _, client := range config.Clients {
for _, redirectURI := range client.RedirectURIs { for _, redirectURI := range client.RedirectURIs {
uri, err := url.ParseRequestURI(redirectURI) uri, err := url.ParseRequestURI(redirectURI)
@ -286,7 +269,7 @@ func validateOIDCOptionsCORSAllowedOriginsFromClientRedirectURIs(config *schema.
} }
} }
func validateOIDCOptionsCORSEndpoints(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCOptionsCORSEndpoints(config *schema.OpenIDConnect, val *schema.StructValidator) {
for _, endpoint := range config.CORS.Endpoints { for _, endpoint := range config.CORS.Endpoints {
if !utils.IsStringInSlice(endpoint, validOIDCCORSEndpoints) { if !utils.IsStringInSlice(endpoint, validOIDCCORSEndpoints) {
val.Push(fmt.Errorf(errFmtOIDCCORSInvalidEndpoint, endpoint, strJoinOr(validOIDCCORSEndpoints))) val.Push(fmt.Errorf(errFmtOIDCCORSInvalidEndpoint, endpoint, strJoinOr(validOIDCCORSEndpoints)))
@ -294,7 +277,7 @@ func validateOIDCOptionsCORSEndpoints(config *schema.OpenIDConnectConfiguration,
} }
} }
func validateOIDCClients(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClients(config *schema.OpenIDConnect, val *schema.StructValidator) {
var ( var (
errDeprecated bool errDeprecated bool
@ -336,7 +319,7 @@ func validateOIDCClients(config *schema.OpenIDConnectConfiguration, val *schema.
} }
} }
func validateOIDCClient(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator, errDeprecatedFunc func()) { func validateOIDCClient(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if config.Clients[c].Public { if config.Clients[c].Public {
if config.Clients[c].Secret != nil { if config.Clients[c].Secret != nil {
val.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, config.Clients[c].ID)) val.Push(fmt.Errorf(errFmtOIDCClientPublicInvalidSecret, config.Clients[c].ID))
@ -386,7 +369,7 @@ func validateOIDCClient(c int, config *schema.OpenIDConnectConfiguration, val *s
validateOIDCClientTokenEndpointAuth(c, config, val) validateOIDCClientTokenEndpointAuth(c, config, val)
} }
func validateOIDCClientPublicKeys(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClientPublicKeys(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch { switch {
case config.Clients[c].PublicKeys.URI != nil && len(config.Clients[c].PublicKeys.Values) != 0: case config.Clients[c].PublicKeys.URI != nil && len(config.Clients[c].PublicKeys.Values) != 0:
val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysBothURIAndValuesConfigured, config.Clients[c].ID)) val.Push(fmt.Errorf(errFmtOIDCClientPublicKeysBothURIAndValuesConfigured, config.Clients[c].ID))
@ -399,7 +382,7 @@ func validateOIDCClientPublicKeys(c int, config *schema.OpenIDConnectConfigurati
} }
} }
func validateOIDCClientJSONWebKeysList(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClientJSONWebKeysList(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
var ( var (
props *JWKProperties props *JWKProperties
err error err error
@ -457,7 +440,7 @@ func validateOIDCClientJSONWebKeysList(c int, config *schema.OpenIDConnectConfig
} }
} }
func validateOIDCClientJSONWebKeysListKeyUseAlg(c, i int, props *JWKProperties, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClientJSONWebKeysListKeyUseAlg(c, i int, props *JWKProperties, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch config.Clients[c].PublicKeys.Values[i].Use { switch config.Clients[c].PublicKeys.Values[i].Use {
case "": case "":
config.Clients[c].PublicKeys.Values[i].Use = props.Use config.Clients[c].PublicKeys.Values[i].Use = props.Use
@ -487,7 +470,7 @@ func validateOIDCClientJSONWebKeysListKeyUseAlg(c, i int, props *JWKProperties,
} }
} }
func validateOIDCClientSectorIdentifier(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClientSectorIdentifier(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
if config.Clients[c].SectorIdentifier.String() != "" { if config.Clients[c].SectorIdentifier.String() != "" {
if utils.IsURLHostComponent(config.Clients[c].SectorIdentifier) || utils.IsURLHostComponentWithPort(config.Clients[c].SectorIdentifier) { if utils.IsURLHostComponent(config.Clients[c].SectorIdentifier) || utils.IsURLHostComponentWithPort(config.Clients[c].SectorIdentifier) {
return return
@ -523,7 +506,7 @@ func validateOIDCClientSectorIdentifier(c int, config *schema.OpenIDConnectConfi
} }
} }
func validateOIDCClientConsentMode(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClientConsentMode(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch { switch {
case utils.IsStringInSlice(config.Clients[c].ConsentMode, []string{"", auto}): case utils.IsStringInSlice(config.Clients[c].ConsentMode, []string{"", auto}):
if config.Clients[c].ConsentPreConfiguredDuration != nil { if config.Clients[c].ConsentPreConfiguredDuration != nil {
@ -542,7 +525,7 @@ func validateOIDCClientConsentMode(c int, config *schema.OpenIDConnectConfigurat
} }
} }
func validateOIDCClientScopes(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator, errDeprecatedFunc func()) { func validateOIDCClientScopes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].Scopes) == 0 { if len(config.Clients[c].Scopes) == 0 {
config.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes config.Clients[c].Scopes = schema.DefaultOpenIDConnectClientConfiguration.Scopes
} }
@ -575,7 +558,7 @@ func validateOIDCClientScopes(c int, config *schema.OpenIDConnectConfiguration,
} }
} }
func validateOIDCClientResponseTypes(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator, errDeprecatedFunc func()) { func validateOIDCClientResponseTypes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].ResponseTypes) == 0 { if len(config.Clients[c].ResponseTypes) == 0 {
config.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes config.Clients[c].ResponseTypes = schema.DefaultOpenIDConnectClientConfiguration.ResponseTypes
} }
@ -593,7 +576,7 @@ func validateOIDCClientResponseTypes(c int, config *schema.OpenIDConnectConfigur
} }
} }
func validateOIDCClientResponseModes(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator, errDeprecatedFunc func()) { func validateOIDCClientResponseModes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].ResponseModes) == 0 { if len(config.Clients[c].ResponseModes) == 0 {
config.Clients[c].ResponseModes = schema.DefaultOpenIDConnectClientConfiguration.ResponseModes config.Clients[c].ResponseModes = schema.DefaultOpenIDConnectClientConfiguration.ResponseModes
@ -625,7 +608,7 @@ func validateOIDCClientResponseModes(c int, config *schema.OpenIDConnectConfigur
} }
} }
func validateOIDCClientGrantTypes(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator, errDeprecatedFunc func()) { func validateOIDCClientGrantTypes(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
if len(config.Clients[c].GrantTypes) == 0 { if len(config.Clients[c].GrantTypes) == 0 {
validateOIDCClientGrantTypesSetDefaults(c, config) validateOIDCClientGrantTypesSetDefaults(c, config)
} }
@ -645,7 +628,7 @@ func validateOIDCClientGrantTypes(c int, config *schema.OpenIDConnectConfigurati
} }
} }
func validateOIDCClientGrantTypesSetDefaults(c int, config *schema.OpenIDConnectConfiguration) { func validateOIDCClientGrantTypesSetDefaults(c int, config *schema.OpenIDConnect) {
for _, responseType := range config.Clients[c].ResponseTypes { for _, responseType := range config.Clients[c].ResponseTypes {
switch responseType { switch responseType {
case oidc.ResponseTypeAuthorizationCodeFlow: case oidc.ResponseTypeAuthorizationCodeFlow:
@ -668,7 +651,7 @@ func validateOIDCClientGrantTypesSetDefaults(c int, config *schema.OpenIDConnect
} }
} }
func validateOIDCClientGrantTypesCheckRelated(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator, errDeprecatedFunc func()) { func validateOIDCClientGrantTypesCheckRelated(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
for _, grantType := range config.Clients[c].GrantTypes { for _, grantType := range config.Clients[c].GrantTypes {
switch grantType { switch grantType {
case oidc.GrantTypeImplicit: case oidc.GrantTypeImplicit:
@ -703,7 +686,7 @@ func validateOIDCClientGrantTypesCheckRelated(c int, config *schema.OpenIDConnec
} }
} }
func validateOIDCClientRedirectURIs(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator, errDeprecatedFunc func()) { func validateOIDCClientRedirectURIs(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
var ( var (
parsedRedirectURI *url.URL parsedRedirectURI *url.URL
err error err error
@ -740,7 +723,7 @@ func validateOIDCClientRedirectURIs(c int, config *schema.OpenIDConnectConfigura
} }
} }
func validateOIDCClientTokenEndpointAuth(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClientTokenEndpointAuth(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
implcit := len(config.Clients[c].ResponseTypes) != 0 && utils.IsStringSliceContainsAll(config.Clients[c].ResponseTypes, validOIDCClientResponseTypesImplicitFlow) implcit := len(config.Clients[c].ResponseTypes) != 0 && utils.IsStringSliceContainsAll(config.Clients[c].ResponseTypes, validOIDCClientResponseTypesImplicitFlow)
switch { switch {
@ -767,7 +750,7 @@ func validateOIDCClientTokenEndpointAuth(c int, config *schema.OpenIDConnectConf
} }
} }
func validateOIDCClientTokenEndpointAuthClientSecretJWT(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDCClientTokenEndpointAuthClientSecretJWT(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
switch { switch {
case config.Clients[c].TokenEndpointAuthSigningAlg == "": case config.Clients[c].TokenEndpointAuthSigningAlg == "":
config.Clients[c].TokenEndpointAuthSigningAlg = oidc.SigningAlgHMACUsingSHA256 config.Clients[c].TokenEndpointAuthSigningAlg = oidc.SigningAlgHMACUsingSHA256
@ -776,7 +759,7 @@ func validateOIDCClientTokenEndpointAuthClientSecretJWT(c int, config *schema.Op
} }
} }
func validateOIDCClientTokenEndpointAuthPublicKeyJWT(config schema.OpenIDConnectClientConfiguration, val *schema.StructValidator) { func validateOIDCClientTokenEndpointAuthPublicKeyJWT(config schema.OpenIDConnectClient, val *schema.StructValidator) {
switch { switch {
case config.TokenEndpointAuthSigningAlg == "": case config.TokenEndpointAuthSigningAlg == "":
val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthSigAlgMissingPrivateKeyJWT, config.ID)) val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthSigAlgMissingPrivateKeyJWT, config.ID))
@ -793,18 +776,38 @@ func validateOIDCClientTokenEndpointAuthPublicKeyJWT(config schema.OpenIDConnect
} }
} }
func validateOIDDClientSigningAlgs(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { func validateOIDDClientSigningAlgs(c int, config *schema.OpenIDConnect, val *schema.StructValidator) {
if config.Clients[c].UserinfoSigningAlg == "" { switch config.Clients[c].UserinfoSigningKeyID {
config.Clients[c].UserinfoSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlg case "":
} else if config.Clients[c].UserinfoSigningAlg != oidc.SigningAlgNone && !utils.IsStringInSlice(config.Clients[c].UserinfoSigningAlg, config.Discovery.ResponseObjectSigningAlgs) { if config.Clients[c].UserinfoSigningAlg == "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].UserinfoSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlg
config.Clients[c].ID, attrOIDCUsrSigAlg, strJoinOr(append(config.Discovery.ResponseObjectSigningAlgs, oidc.SigningAlgNone)), config.Clients[c].UserinfoSigningAlg)) } else if config.Clients[c].UserinfoSigningAlg != oidc.SigningAlgNone && !utils.IsStringInSlice(config.Clients[c].UserinfoSigningAlg, config.Discovery.ResponseObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCUsrSigAlg, strJoinOr(append(config.Discovery.ResponseObjectSigningAlgs, oidc.SigningAlgNone)), config.Clients[c].UserinfoSigningAlg))
}
default:
if !utils.IsStringInSlice(config.Clients[c].UserinfoSigningKeyID, config.Discovery.ResponseObjectSigningKeyIDs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCUsrSigKID, strJoinOr(config.Discovery.ResponseObjectSigningKeyIDs), config.Clients[c].UserinfoSigningKeyID))
} else {
config.Clients[c].UserinfoSigningAlg = getResponseObjectAlgFromKID(config, config.Clients[c].UserinfoSigningKeyID, config.Clients[c].UserinfoSigningAlg)
}
} }
if config.Clients[c].IDTokenSigningAlg == "" { switch config.Clients[c].IDTokenSigningKeyID {
config.Clients[c].IDTokenSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.IDTokenSigningAlg case "":
} else if !utils.IsStringInSlice(config.Clients[c].IDTokenSigningAlg, config.Discovery.ResponseObjectSigningAlgs) { if config.Clients[c].IDTokenSigningAlg == "" {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].IDTokenSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.IDTokenSigningAlg
config.Clients[c].ID, attrOIDCIDTokenSigAlg, strJoinOr(config.Discovery.ResponseObjectSigningAlgs), config.Clients[c].IDTokenSigningAlg)) } else if !utils.IsStringInSlice(config.Clients[c].IDTokenSigningAlg, config.Discovery.ResponseObjectSigningAlgs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCIDTokenSigAlg, strJoinOr(config.Discovery.ResponseObjectSigningAlgs), config.Clients[c].IDTokenSigningAlg))
}
default:
if !utils.IsStringInSlice(config.Clients[c].IDTokenSigningKeyID, config.Discovery.ResponseObjectSigningKeyIDs) {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue,
config.Clients[c].ID, attrOIDCIDTokenSigKID, strJoinOr(config.Discovery.ResponseObjectSigningKeyIDs), config.Clients[c].IDTokenSigningKeyID))
} else {
config.Clients[c].IDTokenSigningAlg = getResponseObjectAlgFromKID(config, config.Clients[c].IDTokenSigningKeyID, config.Clients[c].IDTokenSigningAlg)
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package validator package validator
import ( import (
"crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519" "crypto/ed25519"
"crypto/elliptic" "crypto/elliptic"
@ -9,6 +10,7 @@ import (
"strings" "strings"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
"gopkg.in/square/go-jose.v2"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
@ -166,3 +168,34 @@ func schemaJWKGetProperties(jwk schema.JWK) (properties *JWKProperties, err erro
return nil, fmt.Errorf("the key type '%T' is unknown or not valid for the configuration", key) return nil, fmt.Errorf("the key type '%T' is unknown or not valid for the configuration", key)
} }
} }
func jwkCalculateThumbprint(key schema.CryptographicKey) (thumbprintStr string, err error) {
j := jose.JSONWebKey{}
switch k := key.(type) {
case schema.CryptographicPrivateKey:
j.Key = k.Public()
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
j.Key = k
default:
return "", nil
}
var thumbprint []byte
if thumbprint, err = j.Thumbprint(crypto.SHA256); err != nil {
return "", err
}
return fmt.Sprintf("%x", thumbprint)[:6], nil
}
func getResponseObjectAlgFromKID(config *schema.OpenIDConnect, kid, alg string) string {
for _, jwk := range config.IssuerPrivateKeys {
if kid == jwk.KeyID {
return jwk.Algorithm
}
}
return alg
}

View File

@ -79,3 +79,16 @@ func TestSchemaJWKGetPropertiesMissingTests(t *testing.T) {
assert.Equal(t, nil, props.Curve) assert.Equal(t, nil, props.Curve)
assert.Equal(t, 0, props.Bits) assert.Equal(t, 0, props.Bits)
} }
func TestGetResponseObjectAlgFromKID(t *testing.T) {
c := &schema.OpenIDConnect{
IssuerPrivateKeys: []schema.JWK{
{KeyID: "abc", Algorithm: "EX256"},
{KeyID: "123", Algorithm: "EX512"},
},
}
assert.Equal(t, "EX256", getResponseObjectAlgFromKID(c, "abc", "not"))
assert.Equal(t, "EX512", getResponseObjectAlgFromKID(c, "123", "not"))
assert.Equal(t, "not", getResponseObjectAlgFromKID(c, "111111", "not"))
}

View File

@ -0,0 +1,93 @@
package handlers
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func TestAuthzBuilder_WithConfig(t *testing.T) {
builder := NewAuthzBuilder()
builder.WithConfig(&schema.Configuration{
AuthenticationBackend: schema.AuthenticationBackend{
RefreshInterval: "always",
},
})
assert.Equal(t, time.Second*0, builder.config.RefreshInterval)
builder.WithConfig(&schema.Configuration{
AuthenticationBackend: schema.AuthenticationBackend{
RefreshInterval: "disable",
},
})
assert.Equal(t, time.Second*-1, builder.config.RefreshInterval)
builder.WithConfig(&schema.Configuration{
AuthenticationBackend: schema.AuthenticationBackend{
RefreshInterval: "1m",
},
})
assert.Equal(t, time.Minute, builder.config.RefreshInterval)
builder.WithConfig(nil)
assert.Equal(t, time.Minute, builder.config.RefreshInterval)
}
func TestAuthzBuilder_WithEndpointConfig(t *testing.T) {
builder := NewAuthzBuilder()
builder.WithEndpointConfig(schema.ServerAuthzEndpoint{
Implementation: "ExtAuthz",
})
assert.Equal(t, AuthzImplExtAuthz, builder.implementation)
builder.WithEndpointConfig(schema.ServerAuthzEndpoint{
Implementation: "ForwardAuth",
})
assert.Equal(t, AuthzImplForwardAuth, builder.implementation)
builder.WithEndpointConfig(schema.ServerAuthzEndpoint{
Implementation: "AuthRequest",
})
assert.Equal(t, AuthzImplAuthRequest, builder.implementation)
builder.WithEndpointConfig(schema.ServerAuthzEndpoint{
Implementation: "Legacy",
})
assert.Equal(t, AuthzImplLegacy, builder.implementation)
builder.WithEndpointConfig(schema.ServerAuthzEndpoint{
Implementation: "ExtAuthz",
AuthnStrategies: []schema.ServerAuthzEndpointAuthnStrategy{
{Name: "HeaderProxyAuthorization"},
{Name: "CookieSession"},
},
})
assert.Len(t, builder.strategies, 2)
builder.WithEndpointConfig(schema.ServerAuthzEndpoint{
Implementation: "ExtAuthz",
AuthnStrategies: []schema.ServerAuthzEndpointAuthnStrategy{
{Name: "HeaderAuthorization"},
{Name: "HeaderProxyAuthorization"},
{Name: "HeaderAuthRequestProxyAuthorization"},
{Name: "HeaderLegacy"},
{Name: "CookieSession"},
},
})
assert.Len(t, builder.strategies, 5)
}

View File

@ -0,0 +1,31 @@
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/session"
)
func TestAuthzImplementation(t *testing.T) {
assert.Equal(t, "Legacy", AuthzImplLegacy.String())
assert.Equal(t, "", AuthzImplementation(-1).String())
}
func TestFriendlyMethod(t *testing.T) {
assert.Equal(t, "unknown", friendlyMethod(""))
assert.Equal(t, "GET", friendlyMethod(fasthttp.MethodGet))
}
func TestGenerateVerifySessionHasUpToDateProfileTraceLogs(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
generateVerifySessionHasUpToDateProfileTraceLogs(mock.Ctx, &session.UserSession{Username: "john", DisplayName: "example", Groups: []string{"abc"}, Emails: []string{"user@example.com", "test@example.com"}}, &authentication.UserDetails{Username: "john", Groups: []string{"123"}, DisplayName: "notexample", Emails: []string{"notuser@example.com"}})
generateVerifySessionHasUpToDateProfileTraceLogs(mock.Ctx, &session.UserSession{Username: "john", DisplayName: "example"}, &authentication.UserDetails{Username: "john", DisplayName: "example"})
generateVerifySessionHasUpToDateProfileTraceLogs(mock.Ctx, &session.UserSession{Username: "john", DisplayName: "example", Emails: []string{"abc@example.com"}}, &authentication.UserDetails{Username: "john", DisplayName: "example"})
generateVerifySessionHasUpToDateProfileTraceLogs(mock.Ctx, &session.UserSession{Username: "john", DisplayName: "example"}, &authentication.UserDetails{Username: "john", DisplayName: "example", Emails: []string{"abc@example.com"}})
}

View File

@ -127,8 +127,7 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID) ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID)
session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKeyIDFromAlg(ctx, client.GetIDTokenSigningAlg()), session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKeyID(ctx, client.GetIDTokenSigningKeyID(), client.GetIDTokenSigningAlg()), userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester)
userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester)
ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v", ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v",
requester.GetID(), session.ClientID, session.Subject, session.Username, session.Claims) requester.GetID(), session.ClientID, session.Subject, session.Username, session.Claims)

View File

@ -105,7 +105,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter,
default: default:
var jwk *oidc.JWK var jwk *oidc.JWK
if jwk = ctx.Providers.OpenIDConnect.KeyManager.GetByAlg(ctx, alg); jwk == nil { if jwk = ctx.Providers.OpenIDConnect.KeyManager.Get(ctx, client.GetUserinfoSigningKeyID(), alg); jwk == nil {
ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHintf("Unsupported UserInfo signing algorithm '%s'.", alg))) ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHintf("Unsupported UserInfo signing algorithm '%s'.", alg)))
return return

View File

@ -0,0 +1,20 @@
package metrics
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewPrometheus(t *testing.T) {
p := NewPrometheus()
assert.NotNil(t, p)
p.RecordRequest("400", "GET", time.Second)
p.RecordAuthz("400")
p.RecordAuthn(true, false, "WebAuthn")
p.RecordAuthn(true, false, "1fa")
p.RecordAuthenticationDuration(true, time.Second)
}

View File

@ -154,6 +154,8 @@ func TestIssuerURL(t *testing.T) {
func TestShouldCallNextWithAutheliaCtx(t *testing.T) { func TestShouldCallNextWithAutheliaCtx(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := &fasthttp.RequestCtx{} ctx := &fasthttp.RequestCtx{}
configuration := schema.Configuration{} configuration := schema.Configuration{}
userProvider := mocks.NewMockUserProvider(ctrl) userProvider := mocks.NewMockUserProvider(ctrl)

View File

@ -199,12 +199,12 @@ func (s *ClientAuthenticationStrategySuite) SetupTest() {
secret := tOpenIDConnectPlainTextClientSecret secret := tOpenIDConnectPlainTextClientSecret
s.provider = oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ s.provider = oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{
IssuerPrivateKeys: []schema.JWK{ IssuerPrivateKeys: []schema.JWK{
{Key: keyRSA2048, CertificateChain: certRSA2048, Use: oidc.KeyUseSignature, Algorithm: oidc.SigningAlgRSAUsingSHA256}, {Key: keyRSA2048, CertificateChain: certRSA2048, Use: oidc.KeyUseSignature, Algorithm: oidc.SigningAlgRSAUsingSHA256},
}, },
HMACSecret: "abc123", HMACSecret: "abc123",
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: "hs256", ID: "hs256",
Secret: secret, Secret: secret,

View File

@ -13,7 +13,7 @@ import (
) )
// NewClient creates a new Client. // NewClient creates a new Client.
func NewClient(config schema.OpenIDConnectClientConfiguration) (client Client) { func NewClient(config schema.OpenIDConnectClient) (client Client) {
base := &BaseClient{ base := &BaseClient{
ID: config.ID, ID: config.ID,
Description: config.Description, Description: config.Description,
@ -34,8 +34,10 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client Client) {
EnforcePAR: config.EnforcePAR, EnforcePAR: config.EnforcePAR,
IDTokenSigningAlg: config.IDTokenSigningAlg, IDTokenSigningAlg: config.IDTokenSigningAlg,
UserinfoSigningAlg: config.UserinfoSigningAlg, IDTokenSigningKeyID: config.IDTokenSigningKeyID,
UserinfoSigningAlg: config.UserinfoSigningAlg,
UserinfoSigningKeyID: config.UserinfoSigningKeyID,
Policy: authorization.NewLevel(config.Policy), Policy: authorization.NewLevel(config.Policy),
@ -151,6 +153,11 @@ func (c *BaseClient) GetIDTokenSigningAlg() (alg string) {
return c.IDTokenSigningAlg return c.IDTokenSigningAlg
} }
// GetIDTokenSigningKeyID returns the IDTokenSigningKeyID.
func (c *BaseClient) GetIDTokenSigningKeyID() (alg string) {
return c.IDTokenSigningKeyID
}
// GetUserinfoSigningAlg returns the UserinfoSigningAlg. // GetUserinfoSigningAlg returns the UserinfoSigningAlg.
func (c *BaseClient) GetUserinfoSigningAlg() string { func (c *BaseClient) GetUserinfoSigningAlg() string {
if c.UserinfoSigningAlg == "" { if c.UserinfoSigningAlg == "" {
@ -160,6 +167,11 @@ func (c *BaseClient) GetUserinfoSigningAlg() string {
return c.UserinfoSigningAlg return c.UserinfoSigningAlg
} }
// GetUserinfoSigningKeyID returns the UserinfoSigningKeyID.
func (c *BaseClient) GetUserinfoSigningKeyID() (kid string) {
return c.UserinfoSigningKeyID
}
// GetPAREnforcement returns EnforcePAR. // GetPAREnforcement returns EnforcePAR.
func (c *BaseClient) GetPAREnforcement() bool { func (c *BaseClient) GetPAREnforcement() bool {
return c.EnforcePAR return c.EnforcePAR

View File

@ -18,7 +18,7 @@ import (
) )
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
config := schema.OpenIDConnectClientConfiguration{} config := schema.OpenIDConnectClient{}
client := oidc.NewClient(config) client := oidc.NewClient(config)
assert.Equal(t, "", client.GetID()) assert.Equal(t, "", client.GetID())
assert.Equal(t, "", client.GetDescription()) assert.Equal(t, "", client.GetDescription())
@ -30,11 +30,12 @@ func TestNewClient(t *testing.T) {
require.True(t, ok) require.True(t, ok)
assert.Equal(t, "", bclient.UserinfoSigningAlg) assert.Equal(t, "", bclient.UserinfoSigningAlg)
assert.Equal(t, oidc.SigningAlgNone, client.GetUserinfoSigningAlg()) assert.Equal(t, oidc.SigningAlgNone, client.GetUserinfoSigningAlg())
assert.Equal(t, "", client.GetUserinfoSigningKeyID())
_, ok = client.(*oidc.FullClient) _, ok = client.(*oidc.FullClient)
assert.False(t, ok) assert.False(t, ok)
config = schema.OpenIDConnectClientConfiguration{ config = schema.OpenIDConnectClient{
ID: myclient, ID: myclient,
Description: myclientdesc, Description: myclientdesc,
Policy: twofactor, Policy: twofactor,
@ -52,7 +53,7 @@ func TestNewClient(t *testing.T) {
assert.Equal(t, fosite.ResponseModeFormPost, client.GetResponseModes()[0]) assert.Equal(t, fosite.ResponseModeFormPost, client.GetResponseModes()[0])
assert.Equal(t, authorization.TwoFactor, client.GetAuthorizationPolicy()) assert.Equal(t, authorization.TwoFactor, client.GetAuthorizationPolicy())
config = schema.OpenIDConnectClientConfiguration{ config = schema.OpenIDConnectClient{
TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost,
} }
@ -64,12 +65,32 @@ func TestNewClient(t *testing.T) {
assert.Equal(t, "", fclient.UserinfoSigningAlg) assert.Equal(t, "", fclient.UserinfoSigningAlg)
assert.Equal(t, oidc.SigningAlgNone, client.GetUserinfoSigningAlg()) assert.Equal(t, oidc.SigningAlgNone, client.GetUserinfoSigningAlg())
assert.Equal(t, oidc.SigningAlgNone, fclient.GetUserinfoSigningAlg())
assert.Equal(t, oidc.SigningAlgNone, fclient.UserinfoSigningAlg) assert.Equal(t, oidc.SigningAlgNone, fclient.UserinfoSigningAlg)
assert.Equal(t, "", fclient.UserinfoSigningKeyID)
assert.Equal(t, "", client.GetUserinfoSigningKeyID())
assert.Equal(t, "", fclient.GetUserinfoSigningKeyID())
fclient.UserinfoSigningKeyID = "aukeyid"
assert.Equal(t, "aukeyid", client.GetUserinfoSigningKeyID())
assert.Equal(t, "aukeyid", fclient.GetUserinfoSigningKeyID())
assert.Equal(t, "", fclient.IDTokenSigningAlg) assert.Equal(t, "", fclient.IDTokenSigningAlg)
assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, client.GetIDTokenSigningAlg()) assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, client.GetIDTokenSigningAlg())
assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, fclient.GetIDTokenSigningAlg())
assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, fclient.IDTokenSigningAlg) assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, fclient.IDTokenSigningAlg)
assert.Equal(t, "", fclient.IDTokenSigningKeyID)
assert.Equal(t, "", client.GetIDTokenSigningKeyID())
assert.Equal(t, "", fclient.GetIDTokenSigningKeyID())
fclient.IDTokenSigningKeyID = "akeyid"
assert.Equal(t, "akeyid", client.GetIDTokenSigningKeyID())
assert.Equal(t, "akeyid", fclient.GetIDTokenSigningKeyID())
assert.Equal(t, oidc.ClientAuthMethodClientSecretPost, fclient.TokenEndpointAuthMethod) assert.Equal(t, oidc.ClientAuthMethodClientSecretPost, fclient.TokenEndpointAuthMethod)
assert.Equal(t, oidc.ClientAuthMethodClientSecretPost, fclient.GetTokenEndpointAuthMethod()) assert.Equal(t, oidc.ClientAuthMethodClientSecretPost, fclient.GetTokenEndpointAuthMethod())
@ -81,6 +102,7 @@ func TestNewClient(t *testing.T) {
assert.Equal(t, "", fclient.GetRequestObjectSigningAlgorithm()) assert.Equal(t, "", fclient.GetRequestObjectSigningAlgorithm())
fclient.RequestObjectSigningAlgorithm = oidc.SigningAlgRSAUsingSHA256 fclient.RequestObjectSigningAlgorithm = oidc.SigningAlgRSAUsingSHA256
assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, fclient.GetRequestObjectSigningAlgorithm()) assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, fclient.GetRequestObjectSigningAlgorithm())
assert.Equal(t, "", fclient.JSONWebKeysURI) assert.Equal(t, "", fclient.JSONWebKeysURI)
@ -355,7 +377,7 @@ func TestClient_GetResponseTypes(t *testing.T) {
func TestNewClientPKCE(t *testing.T) { func TestNewClientPKCE(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
have schema.OpenIDConnectClientConfiguration have schema.OpenIDConnectClient
expectedEnforcePKCE bool expectedEnforcePKCE bool
expectedEnforcePKCEChallengeMethod bool expectedEnforcePKCEChallengeMethod bool
expected string expected string
@ -365,7 +387,7 @@ func TestNewClientPKCE(t *testing.T) {
}{ }{
{ {
"ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest", "ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{}, schema.OpenIDConnectClient{},
false, false,
false, false,
"", "",
@ -375,7 +397,7 @@ func TestNewClientPKCE(t *testing.T) {
}, },
{ {
"ShouldEnforcePKCEAndErrorOnNonPKCERequest", "ShouldEnforcePKCEAndErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{EnforcePKCE: true}, schema.OpenIDConnectClient{EnforcePKCE: true},
true, true,
false, false,
"", "",
@ -385,7 +407,7 @@ func TestNewClientPKCE(t *testing.T) {
}, },
{ {
"ShouldEnforcePKCEAndNotErrorOnPKCERequest", "ShouldEnforcePKCEAndNotErrorOnPKCERequest",
schema.OpenIDConnectClientConfiguration{EnforcePKCE: true}, schema.OpenIDConnectClient{EnforcePKCE: true},
true, true,
false, false,
"", "",
@ -394,7 +416,7 @@ func TestNewClientPKCE(t *testing.T) {
"", "",
}, },
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest", {"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"}, schema.OpenIDConnectClient{PKCEChallengeMethod: "S256"},
true, true,
true, true,
"S256", "S256",
@ -403,7 +425,7 @@ func TestNewClientPKCE(t *testing.T) {
"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must include a code_challenge when performing the authorize code flow, but it is missing. The server is configured in a way that enforces PKCE for this client.", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must include a code_challenge when performing the authorize code flow, but it is missing. The server is configured in a way that enforces PKCE for this client.",
}, },
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod", {"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"}, schema.OpenIDConnectClient{PKCEChallengeMethod: "S256"},
true, true,
true, true,
"S256", "S256",
@ -412,7 +434,7 @@ func TestNewClientPKCE(t *testing.T) {
"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Client must use code_challenge_method=S256, is not allowed. The server is configured in a way that enforces PKCE S256 as challenge method for this client.", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Client must use code_challenge_method=S256, is not allowed. The server is configured in a way that enforces PKCE S256 as challenge method for this client.",
}, },
{"ShouldEnforcePKCEFromChallengeMethodAndNotErrorOnValidRequest", {"ShouldEnforcePKCEFromChallengeMethodAndNotErrorOnValidRequest",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"}, schema.OpenIDConnectClient{PKCEChallengeMethod: "S256"},
true, true,
true, true,
"S256", "S256",
@ -448,7 +470,7 @@ func TestNewClientPKCE(t *testing.T) {
func TestNewClientPAR(t *testing.T) { func TestNewClientPAR(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
have schema.OpenIDConnectClientConfiguration have schema.OpenIDConnectClient
expected bool expected bool
r *fosite.Request r *fosite.Request
err string err string
@ -456,7 +478,7 @@ func TestNewClientPAR(t *testing.T) {
}{ }{
{ {
"ShouldNotEnforcEPARAndNotErrorOnNonPARRequest", "ShouldNotEnforcEPARAndNotErrorOnNonPARRequest",
schema.OpenIDConnectClientConfiguration{}, schema.OpenIDConnectClient{},
false, false,
&fosite.Request{}, &fosite.Request{},
"", "",
@ -464,7 +486,7 @@ func TestNewClientPAR(t *testing.T) {
}, },
{ {
"ShouldEnforcePARAndErrorOnNonPARRequest", "ShouldEnforcePARAndErrorOnNonPARRequest",
schema.OpenIDConnectClientConfiguration{EnforcePAR: true}, schema.OpenIDConnectClient{EnforcePAR: true},
true, true,
&fosite.Request{}, &fosite.Request{},
"invalid_request", "invalid_request",
@ -472,14 +494,14 @@ func TestNewClientPAR(t *testing.T) {
}, },
{ {
"ShouldEnforcePARAndErrorOnNonPARRequest", "ShouldEnforcePARAndErrorOnNonPARRequest",
schema.OpenIDConnectClientConfiguration{EnforcePAR: true}, schema.OpenIDConnectClient{EnforcePAR: true},
true, true,
&fosite.Request{Form: map[string][]string{oidc.FormParameterRequestURI: {"https://example.com"}}}, &fosite.Request{Form: map[string][]string{oidc.FormParameterRequestURI: {"https://example.com"}}},
"invalid_request", "invalid_request",
"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Pushed Authorization Requests are enforced for this client but no such request was sent. The request_uri parameter 'https://example.com' is malformed."}, "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Pushed Authorization Requests are enforced for this client but no such request was sent. The request_uri parameter 'https://example.com' is malformed."},
{ {
"ShouldEnforcePARAndNotErrorOnPARRequest", "ShouldEnforcePARAndNotErrorOnPARRequest",
schema.OpenIDConnectClientConfiguration{EnforcePAR: true}, schema.OpenIDConnectClient{EnforcePAR: true},
true, true,
&fosite.Request{Form: map[string][]string{oidc.FormParameterRequestURI: {fmt.Sprintf("%sabc", oidc.RedirectURIPrefixPushedAuthorizationRequestURN)}}}, &fosite.Request{Form: map[string][]string{oidc.FormParameterRequestURI: {fmt.Sprintf("%sabc", oidc.RedirectURIPrefixPushedAuthorizationRequestURN)}}},
"", "",
@ -511,7 +533,7 @@ func TestNewClientPAR(t *testing.T) {
func TestNewClientResponseModes(t *testing.T) { func TestNewClientResponseModes(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
have schema.OpenIDConnectClientConfiguration have schema.OpenIDConnectClient
expected []fosite.ResponseModeType expected []fosite.ResponseModeType
r *fosite.AuthorizeRequest r *fosite.AuthorizeRequest
err string err string
@ -519,7 +541,7 @@ func TestNewClientResponseModes(t *testing.T) {
}{ }{
{ {
"ShouldEnforceResponseModePolicyAndAllowDefaultModeQuery", "ShouldEnforceResponseModePolicyAndAllowDefaultModeQuery",
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{oidc.ResponseModeQuery}}, schema.OpenIDConnectClient{ResponseModes: []string{oidc.ResponseModeQuery}},
[]fosite.ResponseModeType{fosite.ResponseModeQuery}, []fosite.ResponseModeType{fosite.ResponseModeQuery},
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: nil}}}, &fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: nil}}},
"", "",
@ -527,7 +549,7 @@ func TestNewClientResponseModes(t *testing.T) {
}, },
{ {
"ShouldEnforceResponseModePolicyAndFailOnDefaultMode", "ShouldEnforceResponseModePolicyAndFailOnDefaultMode",
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{oidc.ResponseModeFormPost}}, schema.OpenIDConnectClient{ResponseModes: []string{oidc.ResponseModeFormPost}},
[]fosite.ResponseModeType{fosite.ResponseModeFormPost}, []fosite.ResponseModeType{fosite.ResponseModeFormPost},
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: nil}}}, &fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: nil}}},
"unsupported_response_mode", "unsupported_response_mode",
@ -535,7 +557,7 @@ func TestNewClientResponseModes(t *testing.T) {
}, },
{ {
"ShouldNotEnforceConfiguredResponseMode", "ShouldNotEnforceConfiguredResponseMode",
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{oidc.ResponseModeFormPost}}, schema.OpenIDConnectClient{ResponseModes: []string{oidc.ResponseModeFormPost}},
[]fosite.ResponseModeType{fosite.ResponseModeFormPost}, []fosite.ResponseModeType{fosite.ResponseModeFormPost},
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeQuery, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: {oidc.ResponseModeQuery}}}}, &fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeQuery, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: {oidc.ResponseModeQuery}}}},
"", "",
@ -543,7 +565,7 @@ func TestNewClientResponseModes(t *testing.T) {
}, },
{ {
"ShouldNotEnforceUnconfiguredResponseMode", "ShouldNotEnforceUnconfiguredResponseMode",
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{}}, schema.OpenIDConnectClient{ResponseModes: []string{}},
[]fosite.ResponseModeType{}, []fosite.ResponseModeType{},
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: {oidc.ResponseModeQuery}}}}, &fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: {oidc.ResponseModeQuery}}}},
"", "",
@ -588,7 +610,7 @@ func TestNewClient_JSONWebKeySetURI(t *testing.T) {
ok bool ok bool
) )
client = oidc.NewClient(schema.OpenIDConnectClientConfiguration{ client = oidc.NewClient(schema.OpenIDConnectClient{
TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost,
PublicKeys: schema.OpenIDConnectClientPublicKeys{ PublicKeys: schema.OpenIDConnectClientPublicKeys{
URI: MustParseRequestURI("https://google.com"), URI: MustParseRequestURI("https://google.com"),
@ -603,7 +625,7 @@ func TestNewClient_JSONWebKeySetURI(t *testing.T) {
assert.Equal(t, "https://google.com", clientf.GetJSONWebKeysURI()) assert.Equal(t, "https://google.com", clientf.GetJSONWebKeysURI())
client = oidc.NewClient(schema.OpenIDConnectClientConfiguration{ client = oidc.NewClient(schema.OpenIDConnectClient{
TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost,
PublicKeys: schema.OpenIDConnectClientPublicKeys{ PublicKeys: schema.OpenIDConnectClientPublicKeys{
URI: nil, URI: nil,

View File

@ -23,7 +23,7 @@ import (
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
func NewConfig(config *schema.OpenIDConnectConfiguration, templates *templates.Provider) (c *Config) { func NewConfig(config *schema.OpenIDConnect, templates *templates.Provider) (c *Config) {
c = &Config{ c = &Config{
GlobalSecret: []byte(utils.HashSHA256FromString(config.HMACSecret)), GlobalSecret: []byte(utils.HashSHA256FromString(config.HMACSecret)),
SendDebugMessagesToClients: config.EnableClientDebugMessages, SendDebugMessagesToClients: config.EnableClientDebugMessages,

View File

@ -118,6 +118,14 @@ const (
SigningAlgHMACUsingSHA512 = "HS512" SigningAlgHMACUsingSHA512 = "HS512"
) )
// JWS Algorithm Prefixes.
const (
SigningAlgPrefixRSA = "RS"
SigningAlgPrefixHMAC = "HS"
SigningAlgPrefixRSAPSS = "PS"
SigningAlgPrefixECDSA = "ES"
)
const ( const (
KeyUseSignature = "sig" KeyUseSignature = "sig"
) )

View File

@ -8,7 +8,7 @@ import (
) )
// NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration. // NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration.
func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration) (config OpenIDConnectWellKnownConfiguration) { func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnect) (config OpenIDConnectWellKnownConfiguration) {
config = OpenIDConnectWellKnownConfiguration{ config = OpenIDConnectWellKnownConfiguration{
OAuth2WellKnownConfiguration: OAuth2WellKnownConfiguration{ OAuth2WellKnownConfiguration: OAuth2WellKnownConfiguration{
CommonDiscoveryOptions: CommonDiscoveryOptions{ CommonDiscoveryOptions: CommonDiscoveryOptions{
@ -73,6 +73,15 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration
SigningAlgHMACUsingSHA256, SigningAlgHMACUsingSHA256,
SigningAlgHMACUsingSHA384, SigningAlgHMACUsingSHA384,
SigningAlgHMACUsingSHA512, SigningAlgHMACUsingSHA512,
SigningAlgRSAUsingSHA256,
SigningAlgRSAUsingSHA384,
SigningAlgRSAUsingSHA512,
SigningAlgECDSAUsingP256AndSHA256,
SigningAlgECDSAUsingP384AndSHA384,
SigningAlgECDSAUsingP521AndSHA512,
SigningAlgRSAPSSUsingSHA256,
SigningAlgRSAPSSUsingSHA384,
SigningAlgRSAPSSUsingSHA512,
}, },
}, },
OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{ OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{
@ -90,6 +99,15 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration
SigningAlgHMACUsingSHA256, SigningAlgHMACUsingSHA256,
SigningAlgHMACUsingSHA384, SigningAlgHMACUsingSHA384,
SigningAlgHMACUsingSHA512, SigningAlgHMACUsingSHA512,
SigningAlgRSAUsingSHA256,
SigningAlgRSAUsingSHA384,
SigningAlgRSAUsingSHA512,
SigningAlgECDSAUsingP256AndSHA256,
SigningAlgECDSAUsingP384AndSHA384,
SigningAlgECDSAUsingP521AndSHA512,
SigningAlgRSAPSSUsingSHA256,
SigningAlgRSAPSSUsingSHA384,
SigningAlgRSAPSSUsingSHA512,
}, },
IntrospectionEndpointAuthMethodsSupported: []string{ IntrospectionEndpointAuthMethodsSupported: []string{
ClientAuthMethodClientSecretBasic, ClientAuthMethodClientSecretBasic,
@ -104,6 +122,7 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration
OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{ OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{
IDTokenSigningAlgValuesSupported: []string{ IDTokenSigningAlgValuesSupported: []string{
SigningAlgRSAUsingSHA256, SigningAlgRSAUsingSHA256,
SigningAlgNone,
}, },
UserinfoSigningAlgValuesSupported: []string{ UserinfoSigningAlgValuesSupported: []string{
SigningAlgRSAUsingSHA256, SigningAlgRSAUsingSHA256,
@ -111,11 +130,17 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration
}, },
RequestObjectSigningAlgValuesSupported: []string{ RequestObjectSigningAlgValuesSupported: []string{
SigningAlgRSAUsingSHA256, SigningAlgRSAUsingSHA256,
SigningAlgRSAUsingSHA384,
SigningAlgRSAUsingSHA512,
SigningAlgECDSAUsingP256AndSHA256,
SigningAlgECDSAUsingP384AndSHA384,
SigningAlgECDSAUsingP521AndSHA512,
SigningAlgRSAPSSUsingSHA256,
SigningAlgRSAPSSUsingSHA384,
SigningAlgRSAPSSUsingSHA512,
SigningAlgNone, SigningAlgNone,
}, },
}, },
OpenIDConnectFrontChannelLogoutDiscoveryOptions: &OpenIDConnectFrontChannelLogoutDiscoveryOptions{},
OpenIDConnectBackChannelLogoutDiscoveryOptions: &OpenIDConnectBackChannelLogoutDiscoveryOptions{},
OpenIDConnectPromptCreateDiscoveryOptions: &OpenIDConnectPromptCreateDiscoveryOptions{ OpenIDConnectPromptCreateDiscoveryOptions: &OpenIDConnectPromptCreateDiscoveryOptions{
PromptValuesSupported: []string{ PromptValuesSupported: []string{
PromptNone, PromptNone,
@ -134,25 +159,8 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration
} }
} }
for _, alg := range c.Discovery.RequestObjectSigningAlgs {
if !utils.IsStringInSlice(alg, config.RequestObjectSigningAlgValuesSupported) {
config.RequestObjectSigningAlgValuesSupported = append(config.RequestObjectSigningAlgValuesSupported, alg)
}
if !utils.IsStringInSlice(alg, config.RevocationEndpointAuthSigningAlgValuesSupported) {
config.RevocationEndpointAuthSigningAlgValuesSupported = append(config.RevocationEndpointAuthSigningAlgValuesSupported, alg)
}
if !utils.IsStringInSlice(alg, config.TokenEndpointAuthSigningAlgValuesSupported) {
config.TokenEndpointAuthSigningAlgValuesSupported = append(config.TokenEndpointAuthSigningAlgValuesSupported, alg)
}
}
sort.Sort(SortedSigningAlgs(config.IDTokenSigningAlgValuesSupported)) sort.Sort(SortedSigningAlgs(config.IDTokenSigningAlgValuesSupported))
sort.Sort(SortedSigningAlgs(config.UserinfoSigningAlgValuesSupported)) sort.Sort(SortedSigningAlgs(config.UserinfoSigningAlgValuesSupported))
sort.Sort(SortedSigningAlgs(config.RequestObjectSigningAlgValuesSupported))
sort.Sort(SortedSigningAlgs(config.RevocationEndpointAuthSigningAlgValuesSupported))
sort.Sort(SortedSigningAlgs(config.TokenEndpointAuthSigningAlgValuesSupported))
if c.EnablePKCEPlainChallenge { if c.EnablePKCEPlainChallenge {
config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain) config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain)

View File

@ -25,118 +25,51 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) {
expectedRequestObjectSigAlgsSupported, expectedRevocationSigAlgsSupported, expectedTokenAuthSigAlgsSupported []string expectedRequestObjectSigAlgsSupported, expectedRevocationSigAlgsSupported, expectedTokenAuthSigAlgsSupported []string
}{ }{
{ {
desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublic", desc: "ShouldHaveStandardCodeChallengeMethods",
pkcePlainChallenge: false, pkcePlainChallenge: false,
clients: map[string]oidc.Client{"a": &oidc.BaseClient{}}, clients: map[string]oidc.Client{"a": &oidc.BaseClient{}},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256}, expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise}, expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256}, expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512}, expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512}, expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512},
}, },
{ {
desc: "ShouldIncludeDiscoveryInfo", desc: "ShouldHaveAllCodeChallengeMethods",
pkcePlainChallenge: true,
clients: map[string]oidc.Client{"a": &oidc.BaseClient{}},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256, oidc.PKCEChallengeMethodPlain},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512},
},
{
desc: "ShouldIncludeDiscoveredResponseObjectSigningAlgs",
pkcePlainChallenge: false, pkcePlainChallenge: false,
clients: map[string]oidc.Client{"a": &oidc.BaseClient{}}, clients: map[string]oidc.Client{"a": &oidc.BaseClient{}},
discovery: schema.OpenIDConnectDiscovery{ discovery: schema.OpenIDConnectDiscovery{
ResponseObjectSigningAlgs: []string{oidc.SigningAlgECDSAUsingP521AndSHA512}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgECDSAUsingP521AndSHA512},
RequestObjectSigningAlgs: []string{oidc.SigningAlgECDSAUsingP256AndSHA256},
}, },
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256}, expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise}, expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP521AndSHA512}, expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgNone},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgNone}, expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgNone}, expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256}, expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256}, expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512},
},
{
desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublic",
pkcePlainChallenge: true,
clients: map[string]oidc.Client{"a": &oidc.BaseClient{}},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256, oidc.PKCEChallengeMethodPlain},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
},
{
desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublicPairwise",
pkcePlainChallenge: false,
clients: map[string]oidc.Client{"a": &oidc.BaseClient{SectorIdentifier: "yes"}},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
},
{
desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublicPairwise",
pkcePlainChallenge: true,
clients: map[string]oidc.Client{"a": &oidc.BaseClient{SectorIdentifier: "yes"}},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256, oidc.PKCEChallengeMethodPlain},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
},
{
desc: "ShouldHaveTokenAuthMethodsNone",
pkcePlainChallenge: true,
clients: map[string]oidc.Client{"a": &oidc.BaseClient{SectorIdentifier: "yes"}},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256, oidc.PKCEChallengeMethodPlain},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
},
{
desc: "ShouldHaveTokenAuthMethodsNone",
pkcePlainChallenge: true,
clients: map[string]oidc.Client{
"a": &oidc.BaseClient{SectorIdentifier: "yes"},
"b": &oidc.BaseClient{SectorIdentifier: "yes"},
},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256, oidc.PKCEChallengeMethodPlain},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
},
{
desc: "ShouldHaveTokenAuthMethodsNone",
pkcePlainChallenge: true,
clients: map[string]oidc.Client{
"a": &oidc.BaseClient{SectorIdentifier: "yes"},
"b": &oidc.BaseClient{SectorIdentifier: "yes"},
},
expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256, oidc.PKCEChallengeMethodPlain},
expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise},
expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256},
expectedUserInfoSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone},
expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512},
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
c := schema.OpenIDConnectConfiguration{ c := schema.OpenIDConnect{
EnablePKCEPlainChallenge: tc.pkcePlainChallenge, EnablePKCEPlainChallenge: tc.pkcePlainChallenge,
PAR: schema.OpenIDConnectPARConfiguration{ PAR: schema.OpenIDConnectPAR{
Enforce: tc.enforcePAR, Enforce: tc.enforcePAR,
}, },
Discovery: tc.discovery, Discovery: tc.discovery,
@ -169,12 +102,12 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) {
} }
func TestNewOpenIDConnectProviderDiscovery(t *testing.T) { func TestNewOpenIDConnectProviderDiscovery(t *testing.T) {
provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
HMACSecret: "asbdhaaskmdlkamdklasmdlkams", HMACSecret: "asbdhaaskmdlkamdklasmdlkams",
EnablePKCEPlainChallenge: true, EnablePKCEPlainChallenge: true,
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: "a-client", ID: "a-client",
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,
@ -210,11 +143,11 @@ func TestNewOpenIDConnectProviderDiscovery(t *testing.T) {
} }
func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *testing.T) { func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *testing.T) {
provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
HMACSecret: "asbdhaaskmdlkamdklasmdlkams", HMACSecret: "asbdhaaskmdlkamdklasmdlkams",
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: "a-client", ID: "a-client",
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,
@ -281,35 +214,13 @@ func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *test
assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodPrivateKeyJWT) assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodPrivateKeyJWT)
assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone) assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone)
assert.Len(t, disco.IntrospectionEndpointAuthMethodsSupported, 2) assert.Equal(t, []string{oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodNone}, disco.IntrospectionEndpointAuthMethodsSupported)
assert.Contains(t, disco.IntrospectionEndpointAuthMethodsSupported, oidc.ClientAuthMethodClientSecretBasic) assert.Equal(t, []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken}, disco.GrantTypesSupported)
assert.Contains(t, disco.IntrospectionEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone) assert.Equal(t, []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512}, disco.RevocationEndpointAuthSigningAlgValuesSupported)
assert.Equal(t, []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512}, disco.TokenEndpointAuthSigningAlgValuesSupported)
assert.Len(t, disco.GrantTypesSupported, 3) assert.Equal(t, []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, disco.IDTokenSigningAlgValuesSupported)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeAuthorizationCode) assert.Equal(t, []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, disco.UserinfoSigningAlgValuesSupported)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeRefreshToken) assert.Equal(t, []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512, oidc.SigningAlgNone}, disco.RequestObjectSigningAlgValuesSupported)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeImplicit)
assert.Len(t, disco.RevocationEndpointAuthSigningAlgValuesSupported, 3)
assert.Equal(t, disco.RevocationEndpointAuthSigningAlgValuesSupported[0], oidc.SigningAlgHMACUsingSHA256)
assert.Equal(t, disco.RevocationEndpointAuthSigningAlgValuesSupported[1], oidc.SigningAlgHMACUsingSHA384)
assert.Equal(t, disco.RevocationEndpointAuthSigningAlgValuesSupported[2], oidc.SigningAlgHMACUsingSHA512)
assert.Len(t, disco.TokenEndpointAuthSigningAlgValuesSupported, 3)
assert.Equal(t, disco.TokenEndpointAuthSigningAlgValuesSupported[0], oidc.SigningAlgHMACUsingSHA256)
assert.Equal(t, disco.TokenEndpointAuthSigningAlgValuesSupported[1], oidc.SigningAlgHMACUsingSHA384)
assert.Equal(t, disco.TokenEndpointAuthSigningAlgValuesSupported[2], oidc.SigningAlgHMACUsingSHA512)
assert.Len(t, disco.IDTokenSigningAlgValuesSupported, 1)
assert.Contains(t, disco.IDTokenSigningAlgValuesSupported, oidc.SigningAlgRSAUsingSHA256)
assert.Len(t, disco.UserinfoSigningAlgValuesSupported, 2)
assert.Equal(t, disco.UserinfoSigningAlgValuesSupported[0], oidc.SigningAlgRSAUsingSHA256)
assert.Equal(t, disco.UserinfoSigningAlgValuesSupported[1], oidc.SigningAlgNone)
require.Len(t, disco.RequestObjectSigningAlgValuesSupported, 2)
assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, disco.RequestObjectSigningAlgValuesSupported[0])
assert.Equal(t, oidc.SigningAlgNone, disco.RequestObjectSigningAlgValuesSupported[1])
assert.Len(t, disco.ClaimsSupported, 18) assert.Len(t, disco.ClaimsSupported, 18)
assert.Contains(t, disco.ClaimsSupported, oidc.ClaimAuthenticationMethodsReference) assert.Contains(t, disco.ClaimsSupported, oidc.ClaimAuthenticationMethodsReference)
@ -337,11 +248,11 @@ func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *test
} }
func TestNewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) { func TestNewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) {
provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
HMACSecret: "asbdhaaskmdlkamdklasmdlkams", HMACSecret: "asbdhaaskmdlkamdklasmdlkams",
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: "a-client", ID: "a-client",
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,
@ -427,12 +338,12 @@ func TestNewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T)
} }
func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfigurationWithPlainPKCE(t *testing.T) { func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfigurationWithPlainPKCE(t *testing.T) {
provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
HMACSecret: "asbdhaaskmdlkamdklasmdlkams", HMACSecret: "asbdhaaskmdlkamdklasmdlkams",
EnablePKCEPlainChallenge: true, EnablePKCEPlainChallenge: true,
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: "a-client", ID: "a-client",
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,

View File

@ -13,17 +13,17 @@ import (
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
fjwt "github.com/ory/fosite/token/jwt" fjwt "github.com/ory/fosite/token/jwt"
"github.com/ory/x/errorsx" "github.com/ory/x/errorsx"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
// NewKeyManager news up a KeyManager. // NewKeyManager news up a KeyManager.
func NewKeyManager(config *schema.OpenIDConnectConfiguration) (manager *KeyManager) { func NewKeyManager(config *schema.OpenIDConnect) (manager *KeyManager) {
manager = &KeyManager{ manager = &KeyManager{
kids: map[string]*JWK{}, alg2kid: config.Discovery.DefaultKeyIDs,
algs: map[string]*JWK{}, kids: map[string]*JWK{},
algs: map[string]*JWK{},
} }
for _, sjwk := range config.IssuerPrivateKeys { for _, sjwk := range config.IssuerPrivateKeys {
@ -31,10 +31,6 @@ func NewKeyManager(config *schema.OpenIDConnectConfiguration) (manager *KeyManag
manager.kids[sjwk.KeyID] = jwk manager.kids[sjwk.KeyID] = jwk
manager.algs[jwk.alg.Alg()] = jwk manager.algs[jwk.alg.Alg()] = jwk
if jwk.kid == config.Discovery.DefaultKeyID {
manager.kid = jwk.kid
}
} }
return manager return manager
@ -42,14 +38,29 @@ func NewKeyManager(config *schema.OpenIDConnectConfiguration) (manager *KeyManag
// The KeyManager type handles JWKs and signing operations. // The KeyManager type handles JWKs and signing operations.
type KeyManager struct { type KeyManager struct {
kid string alg2kid map[string]string
kids map[string]*JWK kids map[string]*JWK
algs map[string]*JWK algs map[string]*JWK
} }
// GetKeyID returns the default key id. // GetDefaultKeyID returns the default key id.
func (m *KeyManager) GetKeyID(ctx context.Context) string { func (m *KeyManager) GetDefaultKeyID(ctx context.Context) string {
return m.kid return m.alg2kid[SigningAlgRSAUsingSHA256]
}
// GetKeyID returns the JWK Key ID given an kid/alg or the default if it doesn't exist.
func (m *KeyManager) GetKeyID(ctx context.Context, kid, alg string) string {
if kid != "" {
if jwk, ok := m.kids[kid]; ok {
return jwk.KeyID()
}
}
if jwk, ok := m.algs[alg]; ok {
return jwk.KeyID()
}
return m.alg2kid[SigningAlgRSAUsingSHA256]
} }
// GetKeyIDFromAlgStrict returns the key id given an alg or an error if it doesn't exist. // GetKeyIDFromAlgStrict returns the key id given an alg or an error if it doesn't exist.
@ -67,7 +78,20 @@ func (m *KeyManager) GetKeyIDFromAlg(ctx context.Context, alg string) string {
return jwks.kid return jwks.kid
} }
return m.kid return m.alg2kid[SigningAlgRSAUsingSHA256]
}
// Get returns the JWK given an kid/alg or nil if it doesn't exist.
func (m *KeyManager) Get(ctx context.Context, kid, alg string) *JWK {
if kid != "" {
return m.kids[kid]
}
if jwk, ok := m.algs[alg]; ok {
return jwk
}
return nil
} }
// GetByAlg returns the JWK given an alg or nil if it doesn't exist. // GetByAlg returns the JWK given an alg or nil if it doesn't exist.
@ -82,7 +106,7 @@ func (m *KeyManager) GetByAlg(ctx context.Context, alg string) *JWK {
// GetByKID returns the JWK given an key id or nil if it doesn't exist. If given a blank string it returns the default. // GetByKID returns the JWK given an key id or nil if it doesn't exist. If given a blank string it returns the default.
func (m *KeyManager) GetByKID(ctx context.Context, kid string) *JWK { func (m *KeyManager) GetByKID(ctx context.Context, kid string) *JWK {
if kid == "" { if kid == "" {
return m.kids[m.kid] return m.kids[m.alg2kid[SigningAlgRSAUsingSHA256]]
} }
if jwk, ok := m.kids[kid]; ok { if jwk, ok := m.kids[kid]; ok {

View File

@ -17,10 +17,7 @@ import (
) )
func TestKeyManager(t *testing.T) { func TestKeyManager(t *testing.T) {
config := &schema.OpenIDConnectConfiguration{ config := &schema.OpenIDConnect{
Discovery: schema.OpenIDConnectDiscovery{
DefaultKeyID: "kid-RS256-sig",
},
IssuerPrivateKeys: []schema.JWK{ IssuerPrivateKeys: []schema.JWK{
{ {
Use: oidc.KeyUseSignature, Use: oidc.KeyUseSignature,
@ -79,8 +76,16 @@ func TestKeyManager(t *testing.T) {
}, },
} }
config.Discovery.DefaultKeyIDs = map[string]string{}
for i, key := range config.IssuerPrivateKeys { for i, key := range config.IssuerPrivateKeys {
config.IssuerPrivateKeys[i].KeyID = fmt.Sprintf("kid-%s-%s", key.Algorithm, key.Use) kid := fmt.Sprintf("kid-%s-%s", key.Algorithm, key.Use)
config.IssuerPrivateKeys[i].KeyID = kid
if _, ok := config.Discovery.DefaultKeyIDs[key.Algorithm]; !ok {
config.Discovery.DefaultKeyIDs[key.Algorithm] = kid
}
} }
manager := oidc.NewKeyManager(config) manager := oidc.NewKeyManager(config)
@ -89,7 +94,18 @@ func TestKeyManager(t *testing.T) {
ctx := context.Background() ctx := context.Background()
assert.Equal(t, "kid-RS256-sig", manager.GetKeyID(ctx)) assert.Equal(t, "kid-RS256-sig", manager.GetDefaultKeyID(ctx))
require.NotNil(t, manager.Get(ctx, "kid-RS256-sig", oidc.SigningAlgRSAUsingSHA256))
assert.Equal(t, "kid-RS256-sig", manager.Get(ctx, "kid-RS256-sig", oidc.SigningAlgRSAUsingSHA256).KeyID())
assert.Equal(t, "kid-RS256-sig", manager.Get(ctx, "", oidc.SigningAlgRSAUsingSHA256).KeyID())
assert.Nil(t, manager.Get(ctx, "", "NOKEY"))
assert.Equal(t, "kid-RS256-sig", manager.GetKeyID(ctx, "", oidc.SigningAlgRSAUsingSHA256))
assert.Equal(t, "kid-RS256-sig", manager.GetKeyID(ctx, "kid-RS256-sig", oidc.SigningAlgRSAPSSUsingSHA256))
assert.Equal(t, "kid-RS256-sig", manager.GetKeyID(ctx, "", ""))
assert.Equal(t, "kid-PS256-sig", manager.GetKeyID(ctx, "kid-PS256-sig", oidc.SigningAlgRSAPSSUsingSHA256))
assert.Equal(t, "kid-PS256-sig", manager.GetKeyID(ctx, "", oidc.SigningAlgRSAPSSUsingSHA256))
var ( var (
jwk *oidc.JWK jwk *oidc.JWK
@ -107,7 +123,7 @@ func TestKeyManager(t *testing.T) {
jwk = manager.GetByKID(ctx, "") jwk = manager.GetByKID(ctx, "")
assert.NotNil(t, jwk) assert.NotNil(t, jwk)
assert.Equal(t, config.Discovery.DefaultKeyID, jwk.KeyID()) assert.Equal(t, config.Discovery.DefaultKeyIDs[oidc.SigningAlgRSAUsingSHA256], jwk.KeyID())
jwk, err = manager.GetByHeader(ctx, &fjwt.Headers{Extra: map[string]any{oidc.JWTHeaderKeyIdentifier: "notalg"}}) jwk, err = manager.GetByHeader(ctx, &fjwt.Headers{Extra: map[string]any{oidc.JWTHeaderKeyIdentifier: "notalg"}})
assert.EqualError(t, err, "jwt header 'kid' with value 'notalg' does not match a managed jwk") assert.EqualError(t, err, "jwt header 'kid' with value 'notalg' does not match a managed jwk")
@ -126,7 +142,7 @@ func TestKeyManager(t *testing.T) {
assert.Equal(t, "", kid) assert.Equal(t, "", kid)
kid = manager.GetKeyIDFromAlg(ctx, "notalg") kid = manager.GetKeyIDFromAlg(ctx, "notalg")
assert.Equal(t, config.Discovery.DefaultKeyID, kid) assert.Equal(t, config.Discovery.DefaultKeyIDs[oidc.SigningAlgRSAUsingSHA256], kid)
set := manager.Set(ctx) set := manager.Set(ctx)

View File

@ -13,7 +13,7 @@ import (
) )
// NewOpenIDConnectProvider new-ups a OpenIDConnectProvider. // NewOpenIDConnectProvider new-ups a OpenIDConnectProvider.
func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store storage.Provider, templates *templates.Provider) (provider *OpenIDConnectProvider) { func NewOpenIDConnectProvider(config *schema.OpenIDConnect, store storage.Provider, templates *templates.Provider) (provider *OpenIDConnectProvider) {
if config == nil { if config == nil {
return nil return nil
} }

View File

@ -18,12 +18,12 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing
} }
func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) { func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) {
provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
EnablePKCEPlainChallenge: true, EnablePKCEPlainChallenge: true,
HMACSecret: badhmac, HMACSecret: badhmac,
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: myclient, ID: myclient,
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,
@ -50,11 +50,11 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing
} }
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) { func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) {
provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
HMACSecret: badhmac, HMACSecret: badhmac,
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: "a-client", ID: "a-client",
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,

View File

@ -18,8 +18,8 @@ import (
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
) )
// NewStore returns a Store when provided with a schema.OpenIDConnectConfiguration and storage.Provider. // NewStore returns a Store when provided with a schema.OpenIDConnect and storage.Provider.
func NewStore(config *schema.OpenIDConnectConfiguration, provider storage.Provider) (store *Store) { func NewStore(config *schema.OpenIDConnect, provider storage.Provider) (store *Store) {
logger := logging.Logger() logger := logging.Logger()
store = &Store{ store = &Store{

View File

@ -24,10 +24,10 @@ import (
) )
func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ s := oidc.NewStore(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: myclient, ID: myclient,
Description: myclientdesc, Description: myclientdesc,
@ -56,10 +56,10 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
} }
func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ s := oidc.NewStore(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: myclient, ID: myclient,
Description: myclientdesc, Description: myclientdesc,
@ -83,7 +83,7 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
id := myclient id := myclient
c1 := schema.OpenIDConnectClientConfiguration{ c1 := schema.OpenIDConnectClient{
ID: id, ID: id,
Description: myclientdesc, Description: myclientdesc,
Policy: onefactor, Policy: onefactor,
@ -91,10 +91,10 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,
} }
s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ s := oidc.NewStore(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{c1}, Clients: []schema.OpenIDConnectClient{c1},
}, nil) }, nil)
client, err := s.GetFullClient(id) client, err := s.GetFullClient(id)
@ -111,7 +111,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
} }
func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
c1 := schema.OpenIDConnectClientConfiguration{ c1 := schema.OpenIDConnectClient{
ID: myclient, ID: myclient,
Description: myclientdesc, Description: myclientdesc,
Policy: onefactor, Policy: onefactor,
@ -119,10 +119,10 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
Secret: tOpenIDConnectPlainTextClientSecret, Secret: tOpenIDConnectPlainTextClientSecret,
} }
s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ s := oidc.NewStore(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{c1}, Clients: []schema.OpenIDConnectClient{c1},
}, nil) }, nil)
client, err := s.GetFullClient("another-client") client, err := s.GetFullClient("another-client")
@ -131,10 +131,10 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
} }
func TestOpenIDConnectStore_IsValidClientID(t *testing.T) { func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ s := oidc.NewStore(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048, IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: myclient, ID: myclient,
Description: myclientdesc, Description: myclientdesc,
@ -169,8 +169,8 @@ func (s *StoreSuite) SetupTest() {
s.ctx = context.Background() s.ctx = context.Background()
s.ctrl = gomock.NewController(s.T()) s.ctrl = gomock.NewController(s.T())
s.mock = mocks.NewMockStorage(s.ctrl) s.mock = mocks.NewMockStorage(s.ctrl)
s.store = oidc.NewStore(&schema.OpenIDConnectConfiguration{ s.store = oidc.NewStore(&schema.OpenIDConnect{
Clients: []schema.OpenIDConnectClientConfiguration{ Clients: []schema.OpenIDConnectClient{
{ {
ID: "hs256", ID: "hs256",
Secret: tOpenIDConnectPBKDF2ClientSecret, Secret: tOpenIDConnectPBKDF2ClientSecret,

View File

@ -122,8 +122,10 @@ type BaseClient struct {
ResponseTypes []string ResponseTypes []string
ResponseModes []fosite.ResponseModeType ResponseModes []fosite.ResponseModeType
IDTokenSigningAlg string IDTokenSigningAlg string
UserinfoSigningAlg string IDTokenSigningKeyID string
UserinfoSigningAlg string
UserinfoSigningKeyID string
Policy authorization.Level Policy authorization.Level
@ -152,8 +154,11 @@ type Client interface {
GetSectorIdentifier() string GetSectorIdentifier() string
GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody
GetUserinfoSigningAlg() string
GetIDTokenSigningAlg() string GetIDTokenSigningAlg() string
GetIDTokenSigningKeyID() string
GetUserinfoSigningAlg() string
GetUserinfoSigningKeyID() string
GetPAREnforcement() bool GetPAREnforcement() bool
GetPKCEEnforcement() bool GetPKCEEnforcement() bool

View File

@ -95,10 +95,3 @@ func isSigningAlgLess(i, j string) bool {
} }
} }
} }
const (
SigningAlgPrefixRSA = "RS"
SigningAlgPrefixHMAC = "HS"
SigningAlgPrefixRSAPSS = "PS"
SigningAlgPrefixECDSA = "ES"
)

View File

@ -0,0 +1,61 @@
package oidc
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
jose "gopkg.in/square/go-jose.v2"
)
func TestIsSigningAlgLess(t *testing.T) {
assert.False(t, isSigningAlgLess(SigningAlgRSAUsingSHA256, SigningAlgRSAUsingSHA256))
assert.False(t, isSigningAlgLess(SigningAlgRSAUsingSHA256, SigningAlgHMACUsingSHA256))
assert.True(t, isSigningAlgLess(SigningAlgHMACUsingSHA256, SigningAlgNone))
assert.True(t, isSigningAlgLess(SigningAlgHMACUsingSHA256, SigningAlgRSAUsingSHA512))
assert.True(t, isSigningAlgLess(SigningAlgHMACUsingSHA256, SigningAlgRSAPSSUsingSHA256))
assert.True(t, isSigningAlgLess(SigningAlgHMACUsingSHA256, SigningAlgECDSAUsingP521AndSHA512))
assert.True(t, isSigningAlgLess(SigningAlgRSAUsingSHA256, SigningAlgECDSAUsingP521AndSHA512))
assert.True(t, isSigningAlgLess(SigningAlgECDSAUsingP521AndSHA512, "JS121"))
assert.False(t, isSigningAlgLess("JS121", SigningAlgECDSAUsingP521AndSHA512))
assert.False(t, isSigningAlgLess("JS121", "TS512"))
}
func TestSortedJSONWebKey(t *testing.T) {
testCases := []struct {
name string
have []jose.JSONWebKey
expected []jose.JSONWebKey
}{
{
"ShouldOrderByKID",
[]jose.JSONWebKey{
{KeyID: "abc"},
{KeyID: "123"},
},
[]jose.JSONWebKey{
{KeyID: "123"},
{KeyID: "abc"},
},
},
{
"ShouldOrderByAlg",
[]jose.JSONWebKey{
{Algorithm: "RS256"},
{Algorithm: "HS256"},
},
[]jose.JSONWebKey{
{Algorithm: "HS256"},
{Algorithm: "RS256"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
sort.Sort(SortedJSONWebKey(tc.have))
assert.Equal(t, tc.expected, tc.have)
})
}
}

View File

@ -0,0 +1,75 @@
package random
import (
"math/big"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCryptographical(t *testing.T) {
p := &Cryptographical{}
data := make([]byte, 10)
n, err := p.Read(data)
assert.Equal(t, 10, n)
assert.NoError(t, err)
data2, err := p.BytesErr()
assert.NoError(t, err)
assert.Len(t, data2, 72)
data2 = p.Bytes()
assert.Len(t, data2, 72)
data2 = p.BytesCustom(74, []byte(CharSetAlphabetic))
assert.Len(t, data2, 74)
data2, err = p.BytesCustomErr(76, []byte(CharSetAlphabetic))
assert.NoError(t, err)
assert.Len(t, data2, 76)
data2, err = p.BytesCustomErr(-5, []byte(CharSetAlphabetic))
assert.NoError(t, err)
assert.Len(t, data2, 72)
strdata := p.StringCustom(10, CharSetAlphabetic)
assert.Len(t, strdata, 10)
strdata, err = p.StringCustomErr(11, CharSetAlphabetic)
assert.NoError(t, err)
assert.Len(t, strdata, 11)
i := p.Intn(999)
assert.Greater(t, i, 0)
assert.Less(t, i, 999)
i, err = p.IntnErr(999)
assert.NoError(t, err)
assert.Greater(t, i, 0)
assert.Less(t, i, 999)
i, err = p.IntnErr(-4)
assert.EqualError(t, err, "n must be more than 0")
assert.Equal(t, 0, i)
bi := p.Int(big.NewInt(999))
assert.Greater(t, bi.Int64(), int64(0))
assert.Less(t, bi.Int64(), int64(999))
bi = p.Int(nil)
assert.Equal(t, int64(-1), bi.Int64())
bi, err = p.IntErr(nil)
assert.Nil(t, bi)
assert.EqualError(t, err, "max is required")
bi, err = p.IntErr(big.NewInt(-1))
assert.Nil(t, bi)
assert.EqualError(t, err, "max must be 1 or more")
prime, err := p.Prime(64)
assert.NoError(t, err)
assert.NotNil(t, prime)
}

View File

@ -112,6 +112,10 @@ func (r *Mathematical) Intn(n int) int {
// IntnErr returns a random int error combination with a maximum of n. // IntnErr returns a random int error combination with a maximum of n.
func (r *Mathematical) IntnErr(n int) (output int, err error) { func (r *Mathematical) IntnErr(n int) (output int, err error) {
if n <= 0 {
return 0, fmt.Errorf("n must be more than 0")
}
return r.Intn(n), nil return r.Intn(n), nil
} }
@ -132,15 +136,11 @@ func (r *Mathematical) IntErr(max *big.Int) (value *big.Int, err error) {
return nil, fmt.Errorf("max is required") return nil, fmt.Errorf("max is required")
} }
if max.Sign() <= 0 { if max.Int64() <= 0 {
return nil, fmt.Errorf("max must be 1 or more") return nil, fmt.Errorf("max must be 1 or more")
} }
r.lock.Lock() return big.NewInt(int64(r.Intn(int(max.Int64())))), nil
defer r.lock.Unlock()
return big.NewInt(int64(r.Intn(max.Sign()))), nil
} }
// Prime returns a number of the given bit length that is prime with high probability. Prime will return error for any // Prime returns a number of the given bit length that is prime with high probability. Prime will return error for any

View File

@ -0,0 +1,75 @@
package random
import (
"math/big"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMathematical(t *testing.T) {
p := NewMathematical()
data := make([]byte, 10)
n, err := p.Read(data)
assert.Equal(t, 10, n)
assert.NoError(t, err)
data2, err := p.BytesErr()
assert.NoError(t, err)
assert.Len(t, data2, 72)
data2 = p.Bytes()
assert.Len(t, data2, 72)
data2 = p.BytesCustom(74, []byte(CharSetAlphabetic))
assert.Len(t, data2, 74)
data2, err = p.BytesCustomErr(76, []byte(CharSetAlphabetic))
assert.NoError(t, err)
assert.Len(t, data2, 76)
data2, err = p.BytesCustomErr(-5, []byte(CharSetAlphabetic))
assert.NoError(t, err)
assert.Len(t, data2, 72)
strdata := p.StringCustom(10, CharSetAlphabetic)
assert.Len(t, strdata, 10)
strdata, err = p.StringCustomErr(11, CharSetAlphabetic)
assert.NoError(t, err)
assert.Len(t, strdata, 11)
i := p.Intn(999)
assert.Greater(t, i, 0)
assert.Less(t, i, 999)
i, err = p.IntnErr(999)
assert.NoError(t, err)
assert.Greater(t, i, 0)
assert.Less(t, i, 999)
i, err = p.IntnErr(-4)
assert.EqualError(t, err, "n must be more than 0")
assert.Equal(t, 0, i)
bi := p.Int(big.NewInt(999))
assert.Greater(t, bi.Int64(), int64(0))
assert.Less(t, bi.Int64(), int64(999))
bi = p.Int(nil)
assert.Equal(t, int64(-1), bi.Int64())
bi, err = p.IntErr(nil)
assert.Nil(t, bi)
assert.EqualError(t, err, "max is required")
bi, err = p.IntErr(big.NewInt(-1))
assert.Nil(t, bi)
assert.EqualError(t, err, "max must be 1 or more")
prime, err := p.Prime(64)
assert.NoError(t, err)
assert.NotNil(t, prime)
}

View File

@ -12,12 +12,12 @@ import (
) )
// NewRegulator create a regulator instance. // NewRegulator create a regulator instance.
func NewRegulator(config schema.RegulationConfiguration, provider storage.RegulatorProvider, clock utils.Clock) *Regulator { func NewRegulator(config schema.RegulationConfiguration, store storage.RegulatorProvider, clock utils.Clock) *Regulator {
return &Regulator{ return &Regulator{
enabled: config.MaxRetries > 0, enabled: config.MaxRetries > 0,
storageProvider: provider, store: store,
clock: clock, clock: clock,
config: config, config: config,
} }
} }
@ -26,7 +26,7 @@ func NewRegulator(config schema.RegulationConfiguration, provider storage.Regula
func (r *Regulator) Mark(ctx Context, successful, banned bool, username, requestURI, requestMethod, authType string) error { func (r *Regulator) Mark(ctx Context, successful, banned bool, username, requestURI, requestMethod, authType string) error {
ctx.RecordAuthn(successful, banned, strings.ToLower(authType)) ctx.RecordAuthn(successful, banned, strings.ToLower(authType))
return r.storageProvider.AppendAuthenticationLog(ctx, model.AuthenticationAttempt{ return r.store.AppendAuthenticationLog(ctx, model.AuthenticationAttempt{
Time: r.clock.Now(), Time: r.clock.Now(),
Successful: successful, Successful: successful,
Banned: banned, Banned: banned,
@ -46,7 +46,7 @@ func (r *Regulator) Regulate(ctx context.Context, username string) (time.Time, e
return time.Time{}, nil return time.Time{}, nil
} }
attempts, err := r.storageProvider.LoadAuthenticationLogs(ctx, username, r.clock.Now().Add(-r.config.BanTime), 10, 0) attempts, err := r.store.LoadAuthenticationLogs(ctx, username, r.clock.Now().Add(-r.config.BanTime), 10, 0)
if err != nil { if err != nil {
return time.Time{}, nil return time.Time{}, nil
} }

View File

@ -1,46 +1,69 @@
package regulation_test package regulation_test
import ( import (
"context" "fmt"
"net"
"testing" "testing"
"time" "time"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/utils"
) )
type RegulatorSuite struct { type RegulatorSuite struct {
suite.Suite suite.Suite
ctx context.Context mock *mocks.MockAutheliaCtx
ctrl *gomock.Controller
storageMock *mocks.MockStorage
config schema.RegulationConfiguration
clock utils.TestingClock
} }
func (s *RegulatorSuite) SetupTest() { func (s *RegulatorSuite) SetupTest() {
s.ctrl = gomock.NewController(s.T()) s.mock = mocks.NewMockAutheliaCtx(s.T())
s.storageMock = mocks.NewMockStorage(s.ctrl) s.mock.Ctx.Configuration.Regulation = schema.RegulationConfiguration{
s.ctx = context.Background()
s.config = schema.RegulationConfiguration{
MaxRetries: 3, MaxRetries: 3,
BanTime: time.Second * 180, BanTime: time.Second * 180,
FindTime: time.Second * 30, FindTime: time.Second * 30,
} }
s.clock.Set(time.Now())
s.mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedFor, "127.0.0.1")
} }
func (s *RegulatorSuite) TearDownTest() { func (s *RegulatorSuite) TearDownTest() {
s.ctrl.Finish() s.mock.Ctrl.Finish()
}
func (s *RegulatorSuite) TestShouldMark() {
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
s.mock.StorageMock.EXPECT().AppendAuthenticationLog(s.mock.Ctx, model.AuthenticationAttempt{
Time: s.mock.Clock.Now(),
Successful: true,
Banned: false,
Username: "john",
Type: "1fa",
RemoteIP: model.NewNullIP(net.ParseIP("127.0.0.1")),
RequestURI: "https://google.com",
RequestMethod: fasthttp.MethodGet,
})
s.NoError(regulator.Mark(s.mock.Ctx, true, false, "john", "https://google.com", fasthttp.MethodGet, "1fa"))
}
func (s *RegulatorSuite) TestShouldHandleRegulateError() {
regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
s.mock.StorageMock.EXPECT().LoadAuthenticationLogs(s.mock.Ctx, "john", s.mock.Clock.Now().Add(-s.mock.Ctx.Configuration.Regulation.BanTime), 10, 0).Return(nil, fmt.Errorf("failed"))
until, err := regulator.Regulate(s.mock.Ctx, "john")
s.NoError(err)
s.Equal(time.Time{}, until)
} }
func (s *RegulatorSuite) TestShouldNotThrowWhenUserIsLegitimate() { func (s *RegulatorSuite) TestShouldNotThrowWhenUserIsLegitimate() {
@ -48,17 +71,17 @@ func (s *RegulatorSuite) TestShouldNotThrowWhenUserIsLegitimate() {
{ {
Username: "john", Username: "john",
Successful: true, Successful: true,
Time: s.clock.Now().Add(-4 * time.Minute), Time: s.mock.Clock.Now().Add(-4 * time.Minute),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.NoError(s.T(), err) assert.NoError(s.T(), err)
} }
@ -69,27 +92,27 @@ func (s *RegulatorSuite) TestShouldNotThrowWhenFailedAuthenticationNotInFindTime
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-1 * time.Second), Time: s.mock.Clock.Now().Add(-1 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-90 * time.Second), Time: s.mock.Clock.Now().Add(-90 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-180 * time.Second), Time: s.mock.Clock.Now().Add(-180 * time.Second),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.NoError(s.T(), err) assert.NoError(s.T(), err)
} }
@ -100,32 +123,32 @@ func (s *RegulatorSuite) TestShouldBanUserIfLatestAttemptsAreWithinFinTime() {
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-1 * time.Second), Time: s.mock.Clock.Now().Add(-1 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-4 * time.Second), Time: s.mock.Clock.Now().Add(-4 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-6 * time.Second), Time: s.mock.Clock.Now().Add(-6 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-180 * time.Second), Time: s.mock.Clock.Now().Add(-180 * time.Second),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.Equal(s.T(), regulation.ErrUserIsBanned, err) assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
} }
@ -138,27 +161,27 @@ func (s *RegulatorSuite) TestShouldCheckUserIsStillBanned() {
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-31 * time.Second), Time: s.mock.Clock.Now().Add(-31 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-34 * time.Second), Time: s.mock.Clock.Now().Add(-34 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-36 * time.Second), Time: s.mock.Clock.Now().Add(-36 * time.Second),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.Equal(s.T(), regulation.ErrUserIsBanned, err) assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
} }
@ -167,22 +190,22 @@ func (s *RegulatorSuite) TestShouldCheckUserIsNotYetBanned() {
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-34 * time.Second), Time: s.mock.Clock.Now().Add(-34 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-36 * time.Second), Time: s.mock.Clock.Now().Add(-36 * time.Second),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.NoError(s.T(), err) assert.NoError(s.T(), err)
} }
@ -191,7 +214,7 @@ func (s *RegulatorSuite) TestShouldCheckUserWasAboutToBeBanned() {
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-14 * time.Second), Time: s.mock.Clock.Now().Add(-14 * time.Second),
}, },
// more than 30 seconds elapsed between this auth and the preceding one. // more than 30 seconds elapsed between this auth and the preceding one.
// In that case we don't need to regulate the user even though the number // In that case we don't need to regulate the user even though the number
@ -199,22 +222,22 @@ func (s *RegulatorSuite) TestShouldCheckUserWasAboutToBeBanned() {
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-94 * time.Second), Time: s.mock.Clock.Now().Add(-94 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-96 * time.Second), Time: s.mock.Clock.Now().Add(-96 * time.Second),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.NoError(s.T(), err) assert.NoError(s.T(), err)
} }
@ -223,34 +246,34 @@ func (s *RegulatorSuite) TestShouldCheckRegulationHasBeenResetOnSuccessfulAttemp
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-90 * time.Second), Time: s.mock.Clock.Now().Add(-90 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: true, Successful: true,
Time: s.clock.Now().Add(-93 * time.Second), Time: s.mock.Clock.Now().Add(-93 * time.Second),
}, },
// The user was almost banned but he did a successful attempt. Therefore, even if the next // The user was almost banned but he did a successful attempt. Therefore, even if the next
// failure happens within FindTime, he should not be banned. // failure happens within FindTime, he should not be banned.
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-94 * time.Second), Time: s.mock.Clock.Now().Add(-94 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-96 * time.Second), Time: s.mock.Clock.Now().Add(-96 * time.Second),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(s.mock.Ctx.Configuration.Regulation, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.NoError(s.T(), err) assert.NoError(s.T(), err)
} }
@ -265,22 +288,22 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() {
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-31 * time.Second), Time: s.mock.Clock.Now().Add(-31 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-34 * time.Second), Time: s.mock.Clock.Now().Add(-34 * time.Second),
}, },
{ {
Username: "john", Username: "john",
Successful: false, Successful: false,
Time: s.clock.Now().Add(-36 * time.Second), Time: s.mock.Clock.Now().Add(-36 * time.Second),
}, },
} }
s.storageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)).
Return(attemptsInDB, nil) Return(attemptsInDB, nil)
// Check Disabled Functionality. // Check Disabled Functionality.
@ -290,8 +313,8 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() {
BanTime: time.Second * 180, BanTime: time.Second * 180,
} }
regulator := regulation.NewRegulator(config, s.storageMock, &s.clock) regulator := regulation.NewRegulator(config, s.mock.StorageMock, &s.mock.Clock)
_, err := regulator.Regulate(s.ctx, "john") _, err := regulator.Regulate(s.mock.Ctx, "john")
assert.NoError(s.T(), err) assert.NoError(s.T(), err)
// Check Enabled Functionality. // Check Enabled Functionality.
@ -301,7 +324,7 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() {
BanTime: time.Second * 180, BanTime: time.Second * 180,
} }
regulator = regulation.NewRegulator(config, s.storageMock, &s.clock) regulator = regulation.NewRegulator(config, s.mock.StorageMock, &s.mock.Clock)
_, err = regulator.Regulate(s.ctx, "john") _, err = regulator.Regulate(s.mock.Ctx, "john")
assert.Equal(s.T(), regulation.ErrUserIsBanned, err) assert.Equal(s.T(), regulation.ErrUserIsBanned, err)
} }

View File

@ -16,7 +16,7 @@ type Regulator struct {
config schema.RegulationConfiguration config schema.RegulationConfiguration
storageProvider storage.RegulatorProvider store storage.RegulatorProvider
clock utils.Clock clock utils.Clock
} }

View File

@ -29,59 +29,6 @@ const (
tableEncryption = "encryption" tableEncryption = "encryption"
) )
// OAuth2SessionType represents the potential OAuth 2.0 session types.
type OAuth2SessionType int
// Representation of specific OAuth 2.0 session types.
const (
OAuth2SessionTypeAccessToken OAuth2SessionType = iota
OAuth2SessionTypeAuthorizeCode
OAuth2SessionTypeOpenIDConnect
OAuth2SessionTypePAR
OAuth2SessionTypePKCEChallenge
OAuth2SessionTypeRefreshToken
)
// String returns a string representation of this OAuth2SessionType.
func (s OAuth2SessionType) String() string {
switch s {
case OAuth2SessionTypeAccessToken:
return "access token"
case OAuth2SessionTypeAuthorizeCode:
return "authorization code"
case OAuth2SessionTypeOpenIDConnect:
return "openid connect"
case OAuth2SessionTypePAR:
return "pushed authorization request context"
case OAuth2SessionTypePKCEChallenge:
return "pkce challenge"
case OAuth2SessionTypeRefreshToken:
return "refresh token"
default:
return "invalid"
}
}
// Table returns the table name for this session type.
func (s OAuth2SessionType) Table() string {
switch s {
case OAuth2SessionTypeAccessToken:
return tableOAuth2AccessTokenSession
case OAuth2SessionTypeAuthorizeCode:
return tableOAuth2AuthorizeCodeSession
case OAuth2SessionTypeOpenIDConnect:
return tableOAuth2OpenIDConnectSession
case OAuth2SessionTypePAR:
return tableOAuth2PARContext
case OAuth2SessionTypePKCEChallenge:
return tableOAuth2PKCERequestSession
case OAuth2SessionTypeRefreshToken:
return tableOAuth2RefreshTokenSession
default:
return ""
}
}
const ( const (
encryptionNameCheck = "check" encryptionNameCheck = "check"
) )

View File

@ -93,3 +93,56 @@ func (r EncryptionValidationTableResult) ResultDescriptor() string {
return "SUCCESS" return "SUCCESS"
} }
// OAuth2SessionType represents the potential OAuth 2.0 session types.
type OAuth2SessionType int
// Representation of specific OAuth 2.0 session types.
const (
OAuth2SessionTypeAccessToken OAuth2SessionType = iota
OAuth2SessionTypeAuthorizeCode
OAuth2SessionTypeOpenIDConnect
OAuth2SessionTypePAR
OAuth2SessionTypePKCEChallenge
OAuth2SessionTypeRefreshToken
)
// String returns a string representation of this OAuth2SessionType.
func (s OAuth2SessionType) String() string {
switch s {
case OAuth2SessionTypeAccessToken:
return "access token"
case OAuth2SessionTypeAuthorizeCode:
return "authorization code"
case OAuth2SessionTypeOpenIDConnect:
return "openid connect"
case OAuth2SessionTypePAR:
return "pushed authorization request context"
case OAuth2SessionTypePKCEChallenge:
return "pkce challenge"
case OAuth2SessionTypeRefreshToken:
return "refresh token"
default:
return "invalid"
}
}
// Table returns the table name for this session type.
func (s OAuth2SessionType) Table() string {
switch s {
case OAuth2SessionTypeAccessToken:
return tableOAuth2AccessTokenSession
case OAuth2SessionTypeAuthorizeCode:
return tableOAuth2AuthorizeCodeSession
case OAuth2SessionTypeOpenIDConnect:
return tableOAuth2OpenIDConnectSession
case OAuth2SessionTypePAR:
return tableOAuth2PARContext
case OAuth2SessionTypePKCEChallenge:
return tableOAuth2PKCERequestSession
case OAuth2SessionTypeRefreshToken:
return tableOAuth2RefreshTokenSession
default:
return ""
}
}

View File

@ -0,0 +1,87 @@
package storage
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncryptionValidationResult(t *testing.T) {
result := &EncryptionValidationResult{
InvalidCheckValue: false,
}
assert.True(t, result.Success())
assert.True(t, result.Checked())
result = &EncryptionValidationResult{
InvalidCheckValue: true,
}
assert.False(t, result.Success())
assert.True(t, result.Checked())
result = &EncryptionValidationResult{
InvalidCheckValue: false,
Tables: map[string]EncryptionValidationTableResult{
tableWebAuthnDevices: {
Invalid: 10,
Total: 20,
},
},
}
assert.Equal(t, "FAILURE", result.Tables[tableWebAuthnDevices].ResultDescriptor())
assert.False(t, result.Success())
assert.True(t, result.Checked())
result = &EncryptionValidationResult{
InvalidCheckValue: false,
Tables: map[string]EncryptionValidationTableResult{
tableWebAuthnDevices: {
Error: fmt.Errorf("failed to check table"),
},
},
}
assert.False(t, result.Success())
assert.False(t, result.Checked())
assert.Equal(t, "N/A", result.Tables[tableWebAuthnDevices].ResultDescriptor())
result = &EncryptionValidationResult{
InvalidCheckValue: false,
Tables: map[string]EncryptionValidationTableResult{
tableWebAuthnDevices: {
Total: 20,
},
},
}
assert.True(t, result.Success())
assert.True(t, result.Checked())
assert.Equal(t, "SUCCESS", result.Tables[tableWebAuthnDevices].ResultDescriptor())
}
func TestOAuth2SessionType(t *testing.T) {
assert.Equal(t, "access token", OAuth2SessionTypeAccessToken.String())
assert.Equal(t, tableOAuth2AccessTokenSession, OAuth2SessionTypeAccessToken.Table())
assert.Equal(t, "authorization code", OAuth2SessionTypeAuthorizeCode.String())
assert.Equal(t, tableOAuth2AuthorizeCodeSession, OAuth2SessionTypeAuthorizeCode.Table())
assert.Equal(t, "openid connect", OAuth2SessionTypeOpenIDConnect.String())
assert.Equal(t, tableOAuth2OpenIDConnectSession, OAuth2SessionTypeOpenIDConnect.Table())
assert.Equal(t, "pushed authorization request context", OAuth2SessionTypePAR.String())
assert.Equal(t, tableOAuth2PARContext, OAuth2SessionTypePAR.Table())
assert.Equal(t, "pkce challenge", OAuth2SessionTypePKCEChallenge.String())
assert.Equal(t, tableOAuth2PKCERequestSession, OAuth2SessionTypePKCEChallenge.Table())
assert.Equal(t, "refresh token", OAuth2SessionTypeRefreshToken.String())
assert.Equal(t, tableOAuth2RefreshTokenSession, OAuth2SessionTypeRefreshToken.Table())
assert.Equal(t, "invalid", OAuth2SessionType(-1).String())
assert.Equal(t, "", OAuth2SessionType(-1).Table())
}

View File

@ -8,7 +8,7 @@ import (
) )
func TestShouldEncryptAndDecriptUsingAES(t *testing.T) { func TestShouldEncryptAndDecriptUsingAES(t *testing.T) {
var key [32]byte = sha256.Sum256([]byte("the key")) var key = sha256.Sum256([]byte("the key"))
var secret = "the secret" var secret = "the secret"
@ -22,7 +22,7 @@ func TestShouldEncryptAndDecriptUsingAES(t *testing.T) {
} }
func TestShouldFailDecryptOnInvalidKey(t *testing.T) { func TestShouldFailDecryptOnInvalidKey(t *testing.T) {
var key [32]byte = sha256.Sum256([]byte("the key")) var key = sha256.Sum256([]byte("the key"))
var secret = "the secret" var secret = "the secret"
@ -37,7 +37,7 @@ func TestShouldFailDecryptOnInvalidKey(t *testing.T) {
} }
func TestShouldFailDecryptOnInvalidCypherText(t *testing.T) { func TestShouldFailDecryptOnInvalidCypherText(t *testing.T) {
var key [32]byte = sha256.Sum256([]byte("the key")) var key = sha256.Sum256([]byte("the key"))
encryptedSecret := []byte("abc123") encryptedSecret := []byte("abc123")

View File

@ -1,34 +0,0 @@
package utils
import (
"errors"
"io"
)
// NewWriteCloser creates a new io.WriteCloser from an io.Writer.
func NewWriteCloser(wr io.Writer) io.WriteCloser {
return &WriteCloser{wr: wr}
}
// WriteCloser is a io.Writer with an io.Closer.
type WriteCloser struct {
wr io.Writer
closed bool
}
// Write to the io.Writer.
func (w *WriteCloser) Write(p []byte) (n int, err error) {
if w.closed {
return -1, errors.New("already closed")
}
return w.wr.Write(p)
}
// Close the io.Closer.
func (w *WriteCloser) Close() error {
w.closed = true
return nil
}

View File

@ -8,6 +8,97 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestIsStringAbsURL(t *testing.T) {
testCases := []struct {
name string
have string
err string
}{
{
"ShouldBeAbs",
"https://google.com",
"",
},
{
"ShouldNotBeAbs",
"google.com",
"could not parse 'google.com' as a URL",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
theError := IsStringAbsURL(tc.have)
if tc.err == "" {
assert.NoError(t, theError)
} else {
assert.EqualError(t, theError, tc.err)
}
})
}
}
func TestIsStringInSliceF(t *testing.T) {
testCases := []struct {
name string
needle string
haystack []string
isEqual func(needle, item string) bool
expected bool
}{
{
"ShouldBePresent",
"good",
[]string{"good"},
func(needle, item string) bool {
return needle == item
},
true,
},
{
"ShouldNotBePresent",
"bad",
[]string{"good"},
func(needle, item string) bool {
return needle == item
},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, IsStringInSliceF(tc.needle, tc.haystack, tc.isEqual))
})
}
}
func TestStringHTMLEscape(t *testing.T) {
testCases := []struct {
name string
have string
expected string
}{
{
"ShouldNotAlterAlphaNum",
"abc123",
"abc123",
},
{
"ShouldEscapeSpecial",
"abc123><@#@",
"abc123&gt;&lt;@#@",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, StringHTMLEscape(tc.have))
})
}
}
func TestStringSplitDelimitedEscaped(t *testing.T) { func TestStringSplitDelimitedEscaped(t *testing.T) {
testCases := []struct { testCases := []struct {
desc, have string desc, have string

View File

@ -209,11 +209,51 @@ func TestParseTimeString(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
index, actual, err := matchParseTimeStringWithLayouts(tc.have, StandardTimeLayouts) index, actualA, errA := matchParseTimeStringWithLayouts(tc.have, StandardTimeLayouts)
actualB, errB := ParseTimeStringWithLayouts(tc.have, StandardTimeLayouts)
actualC, errC := ParseTimeString(tc.have)
if tc.err == "" {
assert.NoError(t, errA)
assert.NoError(t, errB)
assert.NoError(t, errC)
assert.Equal(t, tc.index, index)
assert.Equal(t, tc.expected.UnixNano(), actualA.UnixNano())
assert.Equal(t, tc.expected.UnixNano(), actualB.UnixNano())
assert.Equal(t, tc.expected.UnixNano(), actualC.UnixNano())
} else {
assert.EqualError(t, errA, tc.err)
assert.EqualError(t, errB, tc.err)
assert.EqualError(t, errC, tc.err)
}
})
}
}
func TestParseTimeStringWithLayouts(t *testing.T) {
testCases := []struct {
name string
have string
index int
expected time.Time
err string
}{
{"ShouldParseIntegerAsUnix", "1675899060", -1, time.Unix(1675899060, 0), ""},
{"ShouldParseIntegerAsUnixMilli", "1675899060000", -2, time.Unix(1675899060, 0), ""},
{"ShouldParseIntegerAsUnixMicro", "1675899060000000", -3, time.Unix(1675899060, 0), ""},
{"ShouldNotParseSuperLargeInteger", "9999999999999999999999999999999999999999", -999, time.Unix(0, 0), "time value was detected as an integer but the integer could not be parsed: strconv.ParseInt: parsing \"9999999999999999999999999999999999999999\": value out of range"},
{"ShouldParseSimpleTime", "Jan 2 15:04:05 2006", 0, time.Unix(1136214245, 0), ""},
{"ShouldNotParseInvalidTime", "abc", -998, time.Unix(0, 0), "failed to find a suitable time layout for time 'abc'"},
{"ShouldMatchDate", "2020-05-01", 6, time.Unix(1588291200, 0), ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual, err := ParseTimeStringWithLayouts(tc.have, StandardTimeLayouts)
if tc.err == "" { if tc.err == "" {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tc.index, index)
assert.Equal(t, tc.expected.UnixNano(), actual.UnixNano()) assert.Equal(t, tc.expected.UnixNano(), actual.UnixNano())
} else { } else {
assert.EqualError(t, err, tc.err) assert.EqualError(t, err, tc.err)

View File

@ -59,3 +59,8 @@ func TestIsRedirectionSafe_ShouldReturnFalseOnBadDomain(t *testing.T) {
assert.False(t, isURLSafe("https://secure.example.comc", "example.com")) assert.False(t, isURLSafe("https://secure.example.comc", "example.com"))
assert.False(t, isURLSafe("https://secure.example.co", "example.com")) assert.False(t, isURLSafe("https://secure.example.co", "example.com"))
} }
func TestHasDomainSuffix(t *testing.T) {
assert.False(t, HasDomainSuffix("abc", ""))
assert.False(t, HasDomainSuffix("", ""))
}

View File

@ -23,8 +23,8 @@
"@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@mui/icons-material": "5.11.16", "@mui/icons-material": "5.11.16",
"@mui/material": "5.13.1", "@mui/material": "5.13.2",
"@mui/styles": "5.13.1", "@mui/styles": "5.13.2",
"@simplewebauthn/browser": "7.2.0", "@simplewebauthn/browser": "7.2.0",
"@simplewebauthn/typescript-types": "7.0.0", "@simplewebauthn/typescript-types": "7.0.0",
"axios": "1.4.0", "axios": "1.4.0",
@ -77,17 +77,17 @@
"@limegrass/eslint-plugin-import-alias": "1.0.6", "@limegrass/eslint-plugin-import-alias": "1.0.6",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0", "@testing-library/react": "14.0.0",
"@types/node": "20.2.1", "@types/node": "20.2.5",
"@types/react": "18.2.6", "@types/react": "18.2.7",
"@types/react-dom": "18.2.4", "@types/react-dom": "18.2.4",
"@types/testing-library__jest-dom": "5.14.5", "@types/testing-library__jest-dom": "5.14.6",
"@types/zxcvbn": "4.4.1", "@types/zxcvbn": "4.4.1",
"@typescript-eslint/eslint-plugin": "5.59.6", "@typescript-eslint/eslint-plugin": "5.59.7",
"@typescript-eslint/parser": "5.59.6", "@typescript-eslint/parser": "5.59.7",
"@vitejs/plugin-react": "4.0.0", "@vitejs/plugin-react": "4.0.0",
"@vitest/coverage-istanbul": "0.31.1", "@vitest/coverage-istanbul": "0.31.1",
"esbuild": "0.17.19", "esbuild": "0.17.19",
"eslint": "8.40.0", "eslint": "8.41.0",
"eslint-config-prettier": "8.8.0", "eslint-config-prettier": "8.8.0",
"eslint-config-react-app": "7.0.1", "eslint-config-react-app": "7.0.1",
"eslint-formatter-rdjson": "1.0.5", "eslint-formatter-rdjson": "1.0.5",
@ -97,14 +97,14 @@
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2", "eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"happy-dom": "9.19.2", "happy-dom": "9.20.3",
"husky": "8.0.3", "husky": "8.0.3",
"prettier": "2.8.8", "prettier": "2.8.8",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"typescript": "5.0.4", "typescript": "5.0.4",
"vite": "4.3.8", "vite": "4.3.9",
"vite-plugin-eslint": "1.8.1", "vite-plugin-eslint": "1.8.1",
"vite-plugin-istanbul": "4.0.1", "vite-plugin-istanbul": "4.1.0",
"vite-plugin-svgr": "3.2.0", "vite-plugin-svgr": "3.2.0",
"vite-tsconfig-paths": "4.2.0", "vite-tsconfig-paths": "4.2.0",
"vitest": "0.31.1", "vitest": "0.31.1",

File diff suppressed because it is too large Load Diff