diff --git a/docs/content/en/configuration/identity-providers/openid-connect/_index.md b/docs/content/en/configuration/identity-providers/openid-connect/_index.md index 575323138..6c7892f58 100644 --- a/docs/content/en/configuration/identity-providers/openid-connect/_index.md +++ b/docs/content/en/configuration/identity-providers/openid-connect/_index.md @@ -2,7 +2,7 @@ title: "OpenID Connect 1.0" description: "" 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 draft: false images: [] diff --git a/docs/content/en/configuration/identity-providers/openid-connect/clients.md b/docs/content/en/configuration/identity-providers/openid-connect/clients.md index 97f9345aa..0f620d12b 100644 --- a/docs/content/en/configuration/identity-providers/openid-connect/clients.md +++ b/docs/content/en/configuration/identity-providers/openid-connect/clients.md @@ -2,7 +2,7 @@ title: "OpenID Connect 1.0 Clients" 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." -date: 2023-05-08T13:38:08+10:00 +date: 2023-05-15T10:32:10+10:00 draft: false images: [] menu: @@ -28,39 +28,41 @@ intended for production use it's used to provide context and an indentation exam identity_providers: oidc: clients: - - id: myapp - description: My Application + - id: 'myapp' + description: 'My Application' secret: '$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng' # The digest of 'insecure_secret'. sector_identifier: '' public: false redirect_uris: - - https://oidc.example.com:8080/oauth2/callback + - 'https://oidc.example.com:8080/oauth2/callback' audience: [] scopes: - - openid - - groups - - email - - profile + - 'openid' + - 'groups' + - 'email' + - 'profile' grant_types: - - refresh_token - - authorization_code + - 'refresh_token' + - 'authorization_code' response_types: - - code + - 'code' response_modes: - - form_post - - query - - fragment - authorization_policy: two_factor - consent_mode: explicit - pre_configured_consent_duration: 1w + - 'form_post' + - 'query' + - 'fragment' + authorization_policy: 'two_factor' + consent_mode: 'explicit' + pre_configured_consent_duration: '1 week' enforce_par: 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_signing_alg: RS256 - id_token_signing_alg: RS256 - request_object_signing_alg: RS256 - userinfo_signing_alg: none ``` ## 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 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 {{< 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) | `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 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. +##### 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 {{< 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 client application or the administrator of the client application. @@ -388,9 +438,15 @@ The key *__MUST__*: * A P-384 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. +##### 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 diff --git a/docs/content/en/configuration/identity-providers/openid-connect/provider.md b/docs/content/en/configuration/identity-providers/openid-connect/provider.md index df773bcdb..e91ba7863 100644 --- a/docs/content/en/configuration/identity-providers/openid-connect/provider.md +++ b/docs/content/en/configuration/identity-providers/openid-connect/provider.md @@ -2,7 +2,7 @@ title: "OpenID Connect 1.0 Provider" 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." -date: 2023-05-08T13:38:08+10:00 +date: 2023-05-15T10:32:10+10:00 draft: false images: [] menu: @@ -135,41 +135,31 @@ with 64 or more characters. ### 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" >}} 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. +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 {{< confkey type="string" default="" required="no" >}} 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 - -{{< 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. +The default if this value is omitted is the first 7 characters of the public key SHA256 thumbprint encoded into +hexadecimal. #### 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. +#### 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 {{< confkey type="string" required="yes" >}} 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 -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. The private key *__MUST__*: @@ -217,15 +221,13 @@ it if present. {{< confkey type="string" required="yes" >}} -*__Important Note:__ This can also be defined using a [secret](../../methods/secrets.md) which is __strongly recommended__ -especially for containerized deployments.* - -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 +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. 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__*: @@ -240,7 +242,7 @@ key data for the first certificate in the chain. {{< 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] JSON key's in the JWKs [Discoverable Endpoint](../../../integration/openid-connect/introduction.md#discoverable-endpoints) 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 changing this value. +This restriction can also be disabled entirely when set to `-1`. + ### enforce_pkce {{< 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 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 [OpenID Connect 1.0]: https://openid.net/connect/ diff --git a/docs/content/en/configuration/security/access-control.md b/docs/content/en/configuration/security/access-control.md index b4d43a15d..e315ebda9 100644 --- a/docs/content/en/configuration/security/access-control.md +++ b/docs/content/en/configuration/security/access-control.md @@ -15,6 +15,11 @@ aliases: - /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 {{< config-alert-example >}} diff --git a/docs/content/en/configuration/storage/postgres.md b/docs/content/en/configuration/storage/postgres.md index 993f3c978..e3d92daad 100644 --- a/docs/content/en/configuration/storage/postgres.md +++ b/docs/content/en/configuration/storage/postgres.md @@ -32,6 +32,7 @@ storage: schema: 'public' username: 'authelia' password: 'mypassword' + timeout: '5s' tls: server_name: 'postgres.example.com' skip_verify: false diff --git a/docs/content/en/integration/openid-connect/frequently-asked-questions.md b/docs/content/en/integration/openid-connect/frequently-asked-questions.md index f99227cba..d2308b938 100644 --- a/docs/content/en/integration/openid-connect/frequently-asked-questions.md +++ b/docs/content/en/integration/openid-connect/frequently-asked-questions.md @@ -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 [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 The following section details solutions for multiple of the questions above. diff --git a/docs/content/en/integration/prologue/get-started.md b/docs/content/en/integration/prologue/get-started.md index 3b5fe67b3..b75c9c93c 100644 --- a/docs/content/en/integration/prologue/get-started.md +++ b/docs/content/en/integration/prologue/get-started.md @@ -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 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 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 [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 We consider it important to do several things in moving to a production environment. diff --git a/go.mod b/go.mod index c8828ef3b..33ae1241a 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,9 @@ require ( github.com/fasthttp/session/v2 v2.5.0 github.com/fsnotify/fsnotify v1.6.0 github.com/go-asn1-ber/asn1-ber v1.5.4 - github.com/go-crypt/crypt v0.2.7 - github.com/go-ldap/ldap/v3 v3.4.5-0.20230506142018-039466e6b835 - github.com/go-rod/rod v0.113.0 + github.com/go-crypt/crypt v0.2.9 + github.com/go-ldap/ldap/v3 v3.4.5-0.20230521105649-cdb0754f6668 + github.com/go-rod/rod v0.113.1 github.com/go-sql-driver/mysql v1.7.1 github.com/go-webauthn/webauthn v0.8.2 github.com/golang-jwt/jwt/v4 v4.5.0 @@ -70,7 +70,7 @@ require ( github.com/ecordell/optgen v0.0.6 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // 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-webauthn/revoke v0.1.9 // indirect github.com/golang/glog v1.0.0 // indirect diff --git a/go.sum b/go.sum index 920f796e4..5b56188b7 100644 --- a/go.sum +++ b/go.sum @@ -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/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-crypt/crypt v0.2.7 h1:Ir6E59c1wrskJhpJXMqaynHA2xAxpGN7nQXlLkbpzR0= -github.com/go-crypt/crypt v0.2.7/go.mod h1:ulieouNs4qwFCq4wF61oyTQYXAXSoOv995EU4hcHwMU= -github.com/go-crypt/x v0.2.0 h1:rHMiKRAu6kFc+xAnQywDb3iHGpvrFbIGXnP3IfCZ+2U= -github.com/go-crypt/x v0.2.0/go.mod h1:uLo5o+Cc8nvahDASQpntR1g3ZMUoq2LM/859PkhykC4= +github.com/go-crypt/crypt v0.2.9 h1:5gWWTId2Qyqs9ROIsegt5pnqo9wUSRLbhpkR6JSftjg= +github.com/go-crypt/crypt v0.2.9/go.mod h1:JjzdTYE2mArb6nBoIvvpF7o46/rK/1pfmlArCRMTFUk= +github.com/go-crypt/x v0.2.1 h1:OGw78Bswme9lffCOX6tyuC280ouU5391glsvThMtM5U= +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/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-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.20230506142018-039466e6b835/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs= +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.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.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/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-rod/rod v0.113.0 h1:E7+GLjYVZnScewIB2u8+66joQLaDGbOLzSOT4orNHms= -github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= +github.com/go-rod/rod v0.113.1 h1:+Qb4K/vkR7BOhW6FhfhtLzUD3l11+0XlF4do+27sOQk= +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.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= diff --git a/internal/authentication/const.go b/internal/authentication/const.go index f93d0ea5b..51dc84db5 100644 --- a/internal/authentication/const.go +++ b/internal/authentication/const.go @@ -2,20 +2,8 @@ package authentication import ( "errors" -) -// 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 + "golang.org/x/text/encoding/unicode" ) const ( @@ -109,3 +97,7 @@ const fileAuthenticationMode = 0600 // OWASP recommends to escape some special characters. // https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.md const specialLDAPRunes = ",#+<>;\"=" + +var ( + encodingUTF16LittleEndian = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) +) diff --git a/internal/authentication/file_user_provider.go b/internal/authentication/file_user_provider.go index 9da2c4178..bf63fd3fd 100644 --- a/internal/authentication/file_user_provider.go +++ b/internal/authentication/file_user_provider.go @@ -23,7 +23,7 @@ import ( type FileUserProvider struct { config *schema.FileAuthenticationBackend hash algorithm.Hash - database *FileUserDatabase + database FileUserDatabase mutex *sync.Mutex timeoutReload time.Time } @@ -34,6 +34,7 @@ func NewFileUserProvider(config *schema.FileAuthenticationBackend) (provider *Fi config: config, mutex: &sync.Mutex{}, 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 } - 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 { 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) } - if err = hash.Validate(); err != nil { - return nil, fmt.Errorf("failed to validate hash settings: %w", err) - } - return hash, nil } diff --git a/internal/authentication/file_user_provider_database.go b/internal/authentication/file_user_provider_database.go index 509f025e0..f5c526096 100644 --- a/internal/authentication/file_user_provider_database.go +++ b/internal/authentication/file_user_provider_database.go @@ -12,9 +12,16 @@ import ( "gopkg.in/yaml.v3" ) -// NewFileUserDatabase creates a new FileUserDatabase. -func NewFileUserDatabase(filePath string, searchEmail, searchCI bool) (database *FileUserDatabase) { - return &FileUserDatabase{ +type FileUserDatabase interface { + Save() (err error) + 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{}, Path: filePath, 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. -type FileUserDatabase struct { +// YAMLUserDatabase is a user details database that is concurrency safe database and can be reloaded. +type YAMLUserDatabase struct { *sync.RWMutex Path string @@ -39,7 +46,7 @@ type FileUserDatabase struct { } // Save the database to disk. -func (m *FileUserDatabase) Save() (err error) { +func (m *YAMLUserDatabase) Save() (err error) { m.RLock() defer m.RUnlock() @@ -52,7 +59,7 @@ func (m *FileUserDatabase) Save() (err error) { } // Load the database from disk. -func (m *FileUserDatabase) Load() (err error) { +func (m *YAMLUserDatabase) Load() (err error) { yml := &DatabaseModel{Users: map[string]UserDetailsModel{}} 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. -func (m *FileUserDatabase) LoadAliases() (err error) { +func (m *YAMLUserDatabase) LoadAliases() (err error) { if m.SearchEmail || m.SearchCI { for k, user := range m.Users { if m.SearchEmail && user.Email != "" { @@ -91,7 +98,7 @@ func (m *FileUserDatabase) LoadAliases() (err error) { return nil } -func (m *FileUserDatabase) loadAlias(k string) (err error) { +func (m *YAMLUserDatabase) loadAlias(k string) (err error) { u := strings.ToLower(k) if u != k { @@ -113,7 +120,7 @@ func (m *FileUserDatabase) loadAlias(k string) (err error) { 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) 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 // username. -func (m *FileUserDatabase) GetUserDetails(username string) (user DatabaseUserDetails, err error) { +func (m *YAMLUserDatabase) GetUserDetails(username string) (user DatabaseUserDetails, err error) { m.RLock() defer m.RUnlock() @@ -172,7 +179,7 @@ func (m *FileUserDatabase) GetUserDetails(username string) (user DatabaseUserDet } // 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 { return } @@ -184,8 +191,8 @@ func (m *FileUserDatabase) SetUserDetails(username string, details *DatabaseUser m.Unlock() } -// ToDatabaseModel converts the FileUserDatabase into the DatabaseModel for saving. -func (m *FileUserDatabase) ToDatabaseModel() (model *DatabaseModel) { +// ToDatabaseModel converts the YAMLUserDatabase into the DatabaseModel for saving. +func (m *YAMLUserDatabase) ToDatabaseModel() (model *DatabaseModel) { model = &DatabaseModel{ Users: map[string]UserDetailsModel{}, } @@ -236,8 +243,8 @@ type DatabaseModel struct { Users map[string]UserDetailsModel `yaml:"users" valid:"required"` } -// ReadToFileUserDatabase reads the DatabaseModel into a FileUserDatabase. -func (m *DatabaseModel) ReadToFileUserDatabase(db *FileUserDatabase) (err error) { +// ReadToFileUserDatabase reads the DatabaseModel into a YAMLUserDatabase. +func (m *DatabaseModel) ReadToFileUserDatabase(db *YAMLUserDatabase) (err error) { users := map[string]DatabaseUserDetails{} var udm *DatabaseUserDetails diff --git a/internal/authentication/file_user_provider_database_mock_test.go b/internal/authentication/file_user_provider_database_mock_test.go new file mode 100644 index 000000000..26e1e3a68 --- /dev/null +++ b/internal/authentication/file_user_provider_database_mock_test.go @@ -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) +} diff --git a/internal/authentication/file_user_provider_database_test.go b/internal/authentication/file_user_provider_database_test.go new file mode 100644 index 000000000..f8bb21d78 --- /dev/null +++ b/internal/authentication/file_user_provider_database_test.go @@ -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") +} diff --git a/internal/authentication/file_user_provider_hash_mock_test.go b/internal/authentication/file_user_provider_hash_mock_test.go new file mode 100644 index 000000000..fb78a32c4 --- /dev/null +++ b/internal/authentication/file_user_provider_hash_mock_test.go @@ -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)) +} diff --git a/internal/authentication/file_user_provider_test.go b/internal/authentication/file_user_provider_test.go index feb26700b..e1078acc0 100644 --- a/internal/authentication/file_user_provider_test.go +++ b/internal/authentication/file_user_provider_test.go @@ -1,59 +1,167 @@ package authentication import ( - "log" + "fmt" "os" + "path/filepath" "regexp" "runtime" "strings" + "sync" "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/require" "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) { if runtime.GOOS == "windows" { t.Skip("skipping test due to being on windows") } - _ = os.Mkdir("/tmp/noperms/", 0000) - err := checkDatabase("/tmp/noperms/users_database.yml") + dir := t.TempDir() - 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) { - err := checkDatabase("./nonexistent.yml") - _ = os.Remove("./nonexistent.yml") + dir := t.TempDir() - 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) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path provider := NewFileUserProvider(&config) @@ -68,7 +176,7 @@ func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) { } func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -84,7 +192,7 @@ func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) { } func TestShouldCheckUserPasswordIsWrong(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -100,7 +208,7 @@ func TestShouldCheckUserPasswordIsWrong(t *testing.T) { } func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -115,7 +223,7 @@ func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) { } func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -131,7 +239,7 @@ func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) { } func TestShouldRetrieveUserDetails(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration 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) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration 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. func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -180,7 +307,10 @@ func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) { 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") assert.NoError(t, err) @@ -189,15 +319,15 @@ func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) { assert.NoError(t, provider.StartupCheck()) - ok, err := provider.CheckUserPassword("harry", "newpassword") + ok, err = provider.CheckUserPassword("harry", "newpassword") assert.NoError(t, err) 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) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path config.Password.Algorithm = "sha2crypt" @@ -207,7 +337,10 @@ func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) { 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") assert.NoError(t, err) @@ -216,15 +349,29 @@ func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) { assert.NoError(t, provider.StartupCheck()) - ok, err := provider.CheckUserPassword("john", "newpassword") + ok, err = provider.CheckUserPassword("john", "newpassword") assert.NoError(t, err) 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) { - WithDatabase(MalformedUserDatabaseContent, func(path string) { + WithDatabase(t, MalformedUserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -235,7 +382,7 @@ func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) { } func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) { - WithDatabase(BadSchemaUserDatabaseContent, func(path string) { + WithDatabase(t, BadSchemaUserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -246,7 +393,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) { } func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *testing.T) { - WithDatabase(BadSHA512HashContent, func(path string) { + WithDatabase(t, BadSHA512HashContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -257,7 +404,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *tes } func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTime(t *testing.T) { - WithDatabase(BadArgon2idHashSettingsContent, func(path string) { + WithDatabase(t, BadArgon2idHashSettingsContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -268,7 +415,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTim } func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *testing.T) { - WithDatabase(BadArgon2idHashKeyContent, func(path string) { + WithDatabase(t, BadArgon2idHashKeyContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -279,7 +426,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t * } func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t *testing.T) { - WithDatabase(BadArgon2idHashSaltContent, func(path string) { + WithDatabase(t, BadArgon2idHashSaltContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -290,7 +437,7 @@ func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t } func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) { - WithDatabase(UserDatabaseWithoutCryptContent, func(path string) { + WithDatabase(t, UserDatabaseWithoutCryptContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -306,7 +453,7 @@ func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) { } func TestShouldNotAllowLoginOfDisabledUsers(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path @@ -322,7 +469,7 @@ func TestShouldNotAllowLoginOfDisabledUsers(t *testing.T) { } func TestShouldErrorOnInvalidCaseSensitiveFile(t *testing.T) { - WithDatabase(UserDatabaseContentInvalidSearchCaseInsenstive, func(path string) { + WithDatabase(t, UserDatabaseContentInvalidSearchCaseInsenstive, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path config.Search.Email = false @@ -335,7 +482,7 @@ func TestShouldErrorOnInvalidCaseSensitiveFile(t *testing.T) { } func TestShouldErrorOnDuplicateEmail(t *testing.T) { - WithDatabase(UserDatabaseContentInvalidSearchEmail, func(path string) { + WithDatabase(t, UserDatabaseContentInvalidSearchEmail, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path config.Search.Email = true @@ -349,7 +496,7 @@ func TestShouldErrorOnDuplicateEmail(t *testing.T) { } func TestShouldNotErrorOnEmailAsUsername(t *testing.T) { - WithDatabase(UserDatabaseContentSearchEmailAsUsername, func(path string) { + WithDatabase(t, UserDatabaseContentSearchEmailAsUsername, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path config.Search.Email = true @@ -362,7 +509,7 @@ func TestShouldNotErrorOnEmailAsUsername(t *testing.T) { } func TestShouldErrorOnEmailAsUsernameWithDuplicateEmail(t *testing.T) { - WithDatabase(UserDatabaseContentInvalidSearchEmailAsUsername, func(path string) { + WithDatabase(t, UserDatabaseContentInvalidSearchEmailAsUsername, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path config.Search.Email = true @@ -375,7 +522,7 @@ func TestShouldErrorOnEmailAsUsernameWithDuplicateEmail(t *testing.T) { } func TestShouldErrorOnEmailAsUsernameWithDuplicateEmailCase(t *testing.T) { - WithDatabase(UserDatabaseContentInvalidSearchEmailAsUsernameCase, func(path string) { + WithDatabase(t, UserDatabaseContentInvalidSearchEmailAsUsernameCase, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path config.Search.Email = false @@ -388,7 +535,7 @@ func TestShouldErrorOnEmailAsUsernameWithDuplicateEmailCase(t *testing.T) { } func TestShouldAllowLookupByEmail(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path config.Search.Email = true @@ -415,7 +562,7 @@ func TestShouldAllowLookupByEmail(t *testing.T) { } func TestShouldAllowLookupCI(t *testing.T) { - WithDatabase(UserDatabaseContent, func(path string) { + WithDatabase(t, UserDatabaseContent, func(path string) { config := DefaultFileAuthenticationBackendConfiguration config.Path = path 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 ( DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackend{ Path: "", @@ -657,3 +937,17 @@ users: - admins - 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()) +} diff --git a/internal/authentication/gen.go b/internal/authentication/gen.go index 18547d606..8fe2c052d 100644 --- a/internal/authentication/gen.go +++ b/internal/authentication/gen.go @@ -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_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 diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go index d2bb2603f..5596872c0 100644 --- a/internal/authentication/ldap_user_provider.go +++ b/internal/authentication/ldap_user_provider.go @@ -215,7 +215,7 @@ func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error) modifyRequest := ldap.NewModifyRequest(profile.DN, controls) // The password needs to be enclosed in quotes // 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}) err = p.modify(client, modifyRequest) diff --git a/internal/authentication/ldap_user_provider_test.go b/internal/authentication/ldap_user_provider_test.go index 97c9dc749..7dc7c7e20 100644 --- a/internal/authentication/ldap_user_provider_test.go +++ b/internal/authentication/ldap_user_provider_test.go @@ -1604,7 +1604,7 @@ func TestShouldUpdateUserPasswordMSAD(t *testing.T) { []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}) dialURLOIDs := mockFactory.EXPECT(). @@ -1715,7 +1715,7 @@ func TestShouldUpdateUserPasswordMSADWithReferrals(t *testing.T) { []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}) dialURLOIDs := mockFactory.EXPECT(). @@ -1843,7 +1843,7 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralConnectErr(t *test []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}) dialURLOIDs := mockFactory.EXPECT(). @@ -1962,7 +1962,7 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralModifyErr(t *testi []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}) dialURLOIDs := mockFactory.EXPECT(). @@ -2094,7 +2094,7 @@ func TestShouldUpdateUserPasswordMSADWithoutReferrals(t *testing.T) { []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}) dialURLOIDs := mockFactory.EXPECT(). diff --git a/internal/authentication/types.go b/internal/authentication/types.go index df96dc3ad..e210bb10f 100644 --- a/internal/authentication/types.go +++ b/internal/authentication/types.go @@ -6,7 +6,6 @@ import ( "time" "github.com/go-ldap/ldap/v3" - "golang.org/x/text/encoding/unicode" ) // LDAPClientFactory an interface of factory of LDAP clients. @@ -103,4 +102,30 @@ type LDAPSupportedControlTypes struct { 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" + } +} diff --git a/internal/authentication/types_test.go b/internal/authentication/types_test.go new file mode 100644 index 000000000..99a683843 --- /dev/null +++ b/internal/authentication/types_test.go @@ -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()) +} diff --git a/internal/authentication/util.go b/internal/authentication/util.go deleted file mode 100644 index 075e45fab..000000000 --- a/internal/authentication/util.go +++ /dev/null @@ -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" - } -} diff --git a/internal/authorization/access_control_domain.go b/internal/authorization/access_control_domain.go index 463e3293d..21fe96b80 100644 --- a/internal/authorization/access_control_domain.go +++ b/internal/authorization/access_control_domain.go @@ -66,13 +66,13 @@ func (m AccessControlDomainMatcher) IsMatch(domain string, subject Subject) (mat return strings.HasSuffix(domain, m.Name) case m.UserWildcard: 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) case m.GroupWildcard: if subject.IsAnonymous() && strings.HasSuffix(domain, m.Name) { - return true + return len(domain) > len(m.Name) } i := strings.Index(domain, ".") diff --git a/internal/authorization/access_control_domain_test.go b/internal/authorization/access_control_domain_test.go new file mode 100644 index 000000000..19c4a908c --- /dev/null +++ b/internal/authorization/access_control_domain_test.go @@ -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)) + }) + } +} diff --git a/internal/authorization/access_control_query_test.go b/internal/authorization/access_control_query_test.go new file mode 100644 index 000000000..12f40620f --- /dev/null +++ b/internal/authorization/access_control_query_test.go @@ -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)) + } + } + }) + } +} diff --git a/internal/authorization/access_control_rule_test.go b/internal/authorization/access_control_rule_test.go new file mode 100644 index 000000000..68e7b54e7 --- /dev/null +++ b/internal/authorization/access_control_rule_test.go @@ -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)) + }) + } +} diff --git a/internal/authorization/authorizer_test.go b/internal/authorization/authorizer_test.go index 8d470a4ef..fab59496f 100644 --- a/internal/authorization/authorizer_test.go +++ b/internal/authorization/authorizer_test.go @@ -1069,9 +1069,9 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithOIDC(t *testing.T) { }, }, }, - IdentityProviders: schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ - Clients: []schema.OpenIDConnectClientConfiguration{ + IdentityProviders: schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ + Clients: []schema.OpenIDConnectClient{ { Policy: oneFactor, }, diff --git a/internal/authorization/regexp.go b/internal/authorization/regexp.go index f26c5fcbf..9c3c0d73b 100644 --- a/internal/authorization/regexp.go +++ b/internal/authorization/regexp.go @@ -16,7 +16,8 @@ type RegexpGroupStringSubjectMatcher struct { // IsMatch returns true if the underlying pattern matches the input given the subject. 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 } @@ -24,16 +25,11 @@ func (r RegexpGroupStringSubjectMatcher) IsMatch(input string, subject Subject) return true } - matches := r.Pattern.FindAllStringSubmatch(input, -1) - if matches == nil { + if r.SubexpNameUser != -1 && !strings.EqualFold(subject.Username, matches[r.SubexpNameUser]) { return false } - if r.SubexpNameUser != -1 && !strings.EqualFold(subject.Username, matches[0][r.SubexpNameUser]) { - return false - } - - if r.SubexpNameGroup != -1 && !utils.IsStringInSliceFold(matches[0][r.SubexpNameGroup], subject.Groups) { + if r.SubexpNameGroup != -1 && !utils.IsStringInSliceFold(matches[r.SubexpNameGroup], subject.Groups) { return false } diff --git a/internal/authorization/regexp_test.go b/internal/authorization/regexp_test.go new file mode 100644 index 000000000..a47567b0e --- /dev/null +++ b/internal/authorization/regexp_test.go @@ -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[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 +} diff --git a/internal/authorization/types_test.go b/internal/authorization/types_test.go index 00ced8aef..1c990cf63 100644 --- a/internal/authorization/types_test.go +++ b/internal/authorization/types_test.go @@ -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()) + }) + } +} diff --git a/internal/authorization/util_test.go b/internal/authorization/util_test.go index ede91d086..4313d0ba6 100644 --- a/internal/authorization/util_test.go +++ b/internal/authorization/util_test.go @@ -2,6 +2,7 @@ package authorization import ( "net" + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -217,3 +218,76 @@ func TestIsAuthLevelSufficient(t *testing.T) { assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, 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 +} diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go index 74f3e4faf..20dbce83c 100644 --- a/internal/configuration/schema/configuration.go +++ b/internal/configuration/schema/configuration.go @@ -8,20 +8,20 @@ type Configuration struct { DefaultRedirectionURL string `koanf:"default_redirection_url"` Default2FAMethod string `koanf:"default_2fa_method"` - Log LogConfiguration `koanf:"log"` - IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"` - AuthenticationBackend AuthenticationBackend `koanf:"authentication_backend"` - Session SessionConfiguration `koanf:"session"` - TOTP TOTPConfiguration `koanf:"totp"` - DuoAPI DuoAPIConfiguration `koanf:"duo_api"` - AccessControl AccessControlConfiguration `koanf:"access_control"` - NTP NTPConfiguration `koanf:"ntp"` - Regulation RegulationConfiguration `koanf:"regulation"` - Storage StorageConfiguration `koanf:"storage"` - Notifier NotifierConfiguration `koanf:"notifier"` - Server ServerConfiguration `koanf:"server"` - Telemetry TelemetryConfig `koanf:"telemetry"` - WebAuthn WebAuthnConfiguration `koanf:"webauthn"` - PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"` - PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"` + Log LogConfiguration `koanf:"log"` + IdentityProviders IdentityProviders `koanf:"identity_providers"` + AuthenticationBackend AuthenticationBackend `koanf:"authentication_backend"` + Session SessionConfiguration `koanf:"session"` + TOTP TOTPConfiguration `koanf:"totp"` + DuoAPI DuoAPIConfiguration `koanf:"duo_api"` + AccessControl AccessControlConfiguration `koanf:"access_control"` + NTP NTPConfiguration `koanf:"ntp"` + Regulation RegulationConfiguration `koanf:"regulation"` + Storage StorageConfiguration `koanf:"storage"` + Notifier NotifierConfiguration `koanf:"notifier"` + Server ServerConfiguration `koanf:"server"` + Telemetry TelemetryConfig `koanf:"telemetry"` + WebAuthn WebAuthnConfiguration `koanf:"webauthn"` + PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"` + PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"` } diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index b3e3fa29f..9b8955077 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -6,13 +6,13 @@ import ( "time" ) -// IdentityProvidersConfiguration represents the IdentityProviders 2.0 configuration for Authelia. -type IdentityProvidersConfiguration struct { - OIDC *OpenIDConnectConfiguration `koanf:"oidc"` +// IdentityProviders represents the Identity Providers configuration for Authelia. +type IdentityProviders struct { + OIDC *OpenIDConnect `koanf:"oidc"` } -// OpenIDConnectConfiguration configuration for OpenID Connect. -type OpenIDConnectConfiguration struct { +// OpenIDConnect configuration for OpenID Connect 1.0. +type OpenIDConnect struct { HMACSecret string `koanf:"hmac_secret"` IssuerPrivateKeys []JWK `koanf:"issuer_private_keys"` @@ -30,36 +30,39 @@ type OpenIDConnectConfiguration struct { EnforcePKCE string `koanf:"enforce_pkce"` EnablePKCEPlainChallenge bool `koanf:"enable_pkce_plain_challenge"` - PAR OpenIDConnectPARConfiguration `koanf:"pushed_authorizations"` - CORS OpenIDConnectCORSConfiguration `koanf:"cors"` + PAR OpenIDConnectPAR `koanf:"pushed_authorizations"` + CORS OpenIDConnectCORS `koanf:"cors"` - Clients []OpenIDConnectClientConfiguration `koanf:"clients"` + Clients []OpenIDConnectClient `koanf:"clients"` Discovery OpenIDConnectDiscovery // MetaData value. Not configurable by users. } +// OpenIDConnectDiscovery is information discovered during validation reused for the discovery handlers. type OpenIDConnectDiscovery struct { - DefaultKeyID string - ResponseObjectSigningAlgs []string - RequestObjectSigningAlgs []string + DefaultKeyIDs map[string]string + DefaultKeyID string + ResponseObjectSigningKeyIDs []string + ResponseObjectSigningAlgs []string + RequestObjectSigningAlgs []string } -// OpenIDConnectPARConfiguration represents an OpenID Connect PAR config. -type OpenIDConnectPARConfiguration struct { +// OpenIDConnectPAR represents an OpenID Connect 1.0 PAR config. +type OpenIDConnectPAR struct { Enforce bool `koanf:"enforce"` ContextLifespan time.Duration `koanf:"context_lifespan"` } -// OpenIDConnectCORSConfiguration represents an OpenID Connect CORS config. -type OpenIDConnectCORSConfiguration struct { +// OpenIDConnectCORS represents an OpenID Connect 1.0 CORS config. +type OpenIDConnectCORS struct { Endpoints []string `koanf:"endpoints"` AllowedOrigins []url.URL `koanf:"allowed_origins"` AllowedOriginsFromClientRedirectURIs bool `koanf:"allowed_origins_from_client_redirect_uris"` } -// OpenIDConnectClientConfiguration configuration for an OpenID Connect client. -type OpenIDConnectClientConfiguration struct { +// OpenIDConnectClient represents a configuration for an OpenID Connect 1.0 client. +type OpenIDConnectClient struct { ID string `koanf:"id"` Description string `koanf:"description"` Secret *PasswordDigest `koanf:"secret"` @@ -84,25 +87,28 @@ type OpenIDConnectClientConfiguration struct { 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"` + IDTokenSigningKeyID string `koanf:"id_token_signing_key_id"` 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"` Discovery OpenIDConnectDiscovery } +// OpenIDConnectClientPublicKeys represents the Client Public Keys configuration for an OpenID Connect 1.0 client. type OpenIDConnectClientPublicKeys struct { URI *url.URL `koanf:"uri"` Values []JWK `koanf:"values"` } // DefaultOpenIDConnectConfiguration contains defaults for OIDC. -var DefaultOpenIDConnectConfiguration = OpenIDConnectConfiguration{ +var DefaultOpenIDConnectConfiguration = OpenIDConnect{ AccessTokenLifespan: time.Hour, AuthorizeCodeLifespan: time.Minute, IDTokenLifespan: time.Hour, @@ -113,12 +119,11 @@ var DefaultOpenIDConnectConfiguration = OpenIDConnectConfiguration{ var defaultOIDCClientConsentPreConfiguredDuration = time.Hour * 24 * 7 // DefaultOpenIDConnectClientConfiguration contains defaults for OIDC Clients. -var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{ - Policy: "two_factor", - Scopes: []string{"openid", "groups", "profile", "email"}, - ResponseTypes: []string{"code"}, - ResponseModes: []string{"form_post"}, - +var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClient{ + Policy: "two_factor", + Scopes: []string{"openid", "groups", "profile", "email"}, + ResponseTypes: []string{"code"}, + ResponseModes: []string{"form_post"}, IDTokenSigningAlg: "RS256", UserinfoSigningAlg: "none", ConsentMode: "auto", diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index 9a1ffde97..e2ee63916 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -20,7 +20,7 @@ var Keys = []string{ "identity_providers.oidc.hmac_secret", "identity_providers.oidc.issuer_private_keys", "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[].key", "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_pkce", "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_key_id", "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.values", "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[].key", "identity_providers.oidc.clients[].public_keys.values[].certificate_chain", diff --git a/internal/configuration/schema/shared.go b/internal/configuration/schema/shared.go index d244b13a7..84ae6260b 100644 --- a/internal/configuration/schema/shared.go +++ b/internal/configuration/schema/shared.go @@ -37,8 +37,8 @@ type ServerBuffers struct { // JWK represents a JWK. type JWK struct { - KeyID string `koanf:"key_id"` - Use string + KeyID string `koanf:"key_id"` + Use string `koanf:"use"` Algorithm string `koanf:"algorithm"` Key CryptographicKey `koanf:"key"` CertificateChain X509CertificateChain `koanf:"certificate_chain"` diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 04697c889..a9e934f2f 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -147,13 +147,15 @@ const ( errFmtOIDCProviderNoPrivateKey = "identity_providers: oidc: option `issuer_private_keys` or 'issuer_private_key' is required" errFmtOIDCProviderEnforcePKCEInvalidValue = "identity_providers: oidc: option 'enforce_pkce' must be 'never', " + "'public_clients_only' or 'always', but it's configured as '%s'" - errFmtOIDCProviderInsecureParameterEntropy = "openid connect provider: SECURITY ISSUE - minimum parameter entropy is " + - "configured to an unsafe value, it should be above 8 but it's configured to %d" + errFmtOIDCProviderInsecureParameterEntropy = "identity_providers: oidc: option 'minimum_parameter_entropy' is " + + "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" 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" - 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" 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" @@ -436,7 +438,9 @@ const ( attrOIDCRedirectURIs = "redirect_uris" attrOIDCTokenAuthMethod = "token_endpoint_auth_method" attrOIDCUsrSigAlg = "userinfo_signing_alg" + attrOIDCUsrSigKID = "userinfo_signing_key_id" attrOIDCIDTokenSigAlg = "id_token_signing_alg" + attrOIDCIDTokenSigKID = "id_token_signing_key_id" attrOIDCPKCEChallengeMethod = "pkce_challenge_method" ) @@ -462,6 +466,7 @@ var ( reKeyReplacer = regexp.MustCompile(`\[\d+]`) 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]))?$`) + reOpenIDConnectKID = regexp.MustCompile(`^([a-zA-Z0-9](([a-zA-Z0-9._~-]*)([a-zA-Z0-9]))?)?$`) ) var replacedKeys = map[string]string{ diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index d01afa888..77a29ccc6 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -1,9 +1,7 @@ package validator import ( - "crypto" "crypto/ecdsa" - "crypto/ed25519" "crypto/rsa" "fmt" "net/url" @@ -12,7 +10,7 @@ import ( "strings" "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/oidc" @@ -20,11 +18,11 @@ import ( ) // 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) } -func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { +func validateOIDC(config *schema.OpenIDConnect, val *schema.StructValidator) { if config == nil { return } @@ -35,8 +33,13 @@ func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructV sort.Sort(oidc.SortedSigningAlgs(config.Discovery.ResponseObjectSigningAlgs)) - if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 { - val.PushWarning(fmt.Errorf(errFmtOIDCProviderInsecureParameterEntropy, config.MinimumParameterEntropy)) + switch { + 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 { @@ -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 { case config.IssuerPrivateKey != nil: 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{{ Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, @@ -77,34 +80,14 @@ func validateOIDCIssuerPrivateKey(config *schema.OpenIDConnectConfiguration) { }}, config.IssuerPrivateKeys...) } -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 validateOIDCIssuerPrivateKeys(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { +func validateOIDCIssuerPrivateKeys(config *schema.OpenIDConnect, val *schema.StructValidator) { var ( props *JWKProperties 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++ { 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 } - case n > 7: + case n > 100: 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)) } - kids[i] = config.IssuerPrivateKeys[i].KeyID + config.Discovery.ResponseObjectSigningKeyIDs[i] = config.IssuerPrivateKeys[i].KeyID - if !utils.IsStringAlphaNumeric(config.IssuerPrivateKeys[i].KeyID) { - val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDNotAlphaNumeric, i+1, config.IssuerPrivateKeys[i].KeyID)) + if !reOpenIDConnectKID.MatchString(config.IssuerPrivateKeys[i].KeyID) { + val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysKeyIDNotValid, i+1, config.IssuerPrivateKeys[i].KeyID)) } 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 { case "": config.IssuerPrivateKeys[i].Use = props.Use @@ -162,26 +145,26 @@ func validateOIDCIssuerPrivateKeysUseAlg(i int, props *JWKProperties, config *sc switch { case config.IssuerPrivateKeys[i].Algorithm == "": config.IssuerPrivateKeys[i].Algorithm = props.Algorithm + + fallthrough 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: val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysInvalidOptionOneOf, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCAlgorithm, strJoinOr(validOIDCIssuerJWKSigningAlgs), config.IssuerPrivateKeys[i].Algorithm)) } if config.IssuerPrivateKeys[i].Algorithm != "" { - if utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, config.Discovery.ResponseObjectSigningAlgs) { - val.Push(fmt.Errorf(errFmtOIDCProviderPrivateKeysAttributeNotUnique, i+1, config.IssuerPrivateKeys[i].KeyID, attrOIDCAlgorithm)) - } else { + if !utils.IsStringInSlice(config.IssuerPrivateKeys[i].Algorithm, config.Discovery.ResponseObjectSigningAlgs) { 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 ( checkEqualKey bool 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) { 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) if config.CORS.AllowedOriginsFromClientRedirectURIs { @@ -245,7 +228,7 @@ func validateOIDCOptionsCORS(config *schema.OpenIDConnectConfiguration, validato 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 { if origin.String() == "*" { 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 _, redirectURI := range client.RedirectURIs { 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 { if !utils.IsStringInSlice(endpoint, 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 ( 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].Secret != nil { 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) } -func validateOIDCClientPublicKeys(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { +func validateOIDCClientPublicKeys(c int, config *schema.OpenIDConnect, val *schema.StructValidator) { switch { case config.Clients[c].PublicKeys.URI != nil && len(config.Clients[c].PublicKeys.Values) != 0: 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 ( props *JWKProperties 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 { case "": 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 utils.IsURLHostComponent(config.Clients[c].SectorIdentifier) || utils.IsURLHostComponentWithPort(config.Clients[c].SectorIdentifier) { 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 { case utils.IsStringInSlice(config.Clients[c].ConsentMode, []string{"", auto}): 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 { 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 { 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 { 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 { 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 { switch responseType { 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 { switch grantType { 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 ( parsedRedirectURI *url.URL 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) 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 { case config.Clients[c].TokenEndpointAuthSigningAlg == "": 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 { case config.TokenEndpointAuthSigningAlg == "": 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) { - if config.Clients[c].UserinfoSigningAlg == "" { - config.Clients[c].UserinfoSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.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)) +func validateOIDDClientSigningAlgs(c int, config *schema.OpenIDConnect, val *schema.StructValidator) { + switch config.Clients[c].UserinfoSigningKeyID { + case "": + if config.Clients[c].UserinfoSigningAlg == "" { + config.Clients[c].UserinfoSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.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 == "" { - config.Clients[c].IDTokenSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.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)) + switch config.Clients[c].IDTokenSigningKeyID { + case "": + if config.Clients[c].IDTokenSigningAlg == "" { + config.Clients[c].IDTokenSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.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) + } } } diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index 9e4212df9..a282f049f 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -23,8 +23,8 @@ import ( func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "abc", }, } @@ -39,14 +39,14 @@ func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) { func TestShouldNotRaiseErrorWhenCORSEndpointsValid(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, - CORS: schema.OpenIDConnectCORSConfiguration{ + CORS: schema.OpenIDConnectCORS{ Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo}, }, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "example", Secret: tOpenIDConnectPlainTextClientSecret, @@ -62,14 +62,14 @@ func TestShouldNotRaiseErrorWhenCORSEndpointsValid(t *testing.T) { func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, - CORS: schema.OpenIDConnectCORSConfiguration{ + CORS: schema.OpenIDConnectCORS{ Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo, "invalid_endpoint"}, }, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "example", Secret: tOpenIDConnectPlainTextClientSecret, @@ -87,8 +87,8 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) { func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, EnforcePKCE: testInvalid, @@ -106,15 +106,15 @@ func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) { func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, - CORS: schema.OpenIDConnectCORSConfiguration{ + CORS: schema.OpenIDConnectCORS{ AllowedOrigins: utils.URLsFromStringSlice([]string{"https://example.com/", "https://site.example.com/subpath", "https://site.example.com?example=true", "*"}), AllowedOriginsFromClientRedirectURIs: true, }, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "myclient", Secret: tOpenIDConnectPlainTextClientSecret, @@ -141,8 +141,8 @@ func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) { func TestShouldRaiseErrorWhenOIDCServerNoClients(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, }, @@ -167,12 +167,12 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { testCases := []struct { Name string - Clients []schema.OpenIDConnectClientConfiguration + Clients []schema.OpenIDConnectClient Errors []string }{ { Name: "EmptyIDAndSecret", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "", Secret: nil, @@ -187,7 +187,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "InvalidPolicy", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-1", Secret: tOpenIDConnectPlainTextClientSecret, @@ -203,7 +203,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "ClientIDDuplicated", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-x", Secret: tOpenIDConnectPlainTextClientSecret, @@ -223,7 +223,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "RedirectURIInvalid", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-check-uri-parse", Secret: tOpenIDConnectPlainTextClientSecret, @@ -239,7 +239,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "RedirectURINotAbsolute", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-check-uri-abs", Secret: tOpenIDConnectPlainTextClientSecret, @@ -255,7 +255,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "ValidSectorIdentifier", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-valid-sector", Secret: tOpenIDConnectPlainTextClientSecret, @@ -269,7 +269,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "ValidSectorIdentifierWithPort", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-valid-sector", Secret: tOpenIDConnectPlainTextClientSecret, @@ -283,7 +283,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "InvalidSectorIdentifierInvalidURL", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-invalid-sector", Secret: tOpenIDConnectPlainTextClientSecret, @@ -305,7 +305,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "InvalidSectorIdentifierInvalidHost", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-invalid-sector", Secret: tOpenIDConnectPlainTextClientSecret, @@ -322,7 +322,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "InvalidConsentMode", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-bad-consent-mode", Secret: tOpenIDConnectPlainTextClientSecret, @@ -339,7 +339,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "InvalidPKCEChallengeMethod", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-bad-pkce-mode", Secret: tOpenIDConnectPlainTextClientSecret, @@ -356,7 +356,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { }, { Name: "InvalidPKCEChallengeMethodLowerCaseS256", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-bad-pkce-mode-s256", Secret: tOpenIDConnectPlainTextClientSecret, @@ -376,8 +376,8 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, Clients: tc.Clients, @@ -400,11 +400,11 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "good_id", Secret: tOpenIDConnectPlainTextClientSecret, @@ -426,11 +426,11 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) { func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "good_id", Secret: tOpenIDConnectPBKDF2ClientSecret, @@ -452,12 +452,12 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) func TestShouldNotErrorOnCertificateValid(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerCertificateChain: certRSA2048, IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "good_id", Secret: tOpenIDConnectPBKDF2ClientSecret, @@ -478,12 +478,12 @@ func TestShouldNotErrorOnCertificateValid(t *testing.T) { func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", IssuerCertificateChain: certRSA2048, IssuerPrivateKey: keyRSA4096, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "good_id", Secret: tOpenIDConnectPBKDF2ClientSecret, @@ -504,41 +504,106 @@ func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) { assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: issuer_private_keys: key #1 with key id 'bf1e10': option 'certificate_chain' does not appear to contain the public key for the private key provided by option 'key'") } -func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T) { - validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ - HMACSecret: "abc", - IssuerPrivateKey: keyRSA2048, - MinimumParameterEntropy: 1, - Clients: []schema.OpenIDConnectClientConfiguration{ - { - ID: "good_id", - Secret: tOpenIDConnectPBKDF2ClientSecret, - Policy: "two_factor", - RedirectURIs: []string{ - "https://google.com/callback", - }, - }, - }, +func TestValidateIdentityProvidersOpenIDConnectMinimumParameterEntropy(t *testing.T) { + testCases := []struct { + name string + have int + expected int + warnings []string + errors []string + }{ + { + "ShouldNotOverrideCustomValue", + 20, + 20, + nil, + nil, + }, + { + "ShouldSetDefault", + 0, + 8, + nil, + nil, + }, + { + "ShouldSetDefaultNegative", + -2, + 8, + nil, + nil, + }, + { + "ShouldAllowDisabledAndWarn", + -1, + -1, + []string{"identity_providers: oidc: option 'minimum_parameter_entropy' is disabled which is considered unsafe and insecure"}, + nil, + }, + { + "ShouldWarnOnTooLow", + 2, + 2, + []string{"identity_providers: oidc: option 'minimum_parameter_entropy' is configured to an unsafe and insecure value, it should at least be 8 but it's configured to 2"}, + nil, }, } - ValidateIdentityProviders(config, validator) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + validator := schema.NewStructValidator() + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ + HMACSecret: "abc", + IssuerPrivateKey: keyRSA2048, + MinimumParameterEntropy: tc.have, + Clients: []schema.OpenIDConnectClient{ + { + ID: "good_id", + Secret: tOpenIDConnectPBKDF2ClientSecret, + Policy: "two_factor", + RedirectURIs: []string{ + "https://google.com/callback", + }, + }, + }, + }, + } - assert.Len(t, validator.Errors(), 0) - require.Len(t, validator.Warnings(), 1) + ValidateIdentityProviders(config, validator) - assert.EqualError(t, validator.Warnings()[0], "openid connect provider: SECURITY ISSUE - minimum parameter entropy is configured to an unsafe value, it should be above 8 but it's configured to 1") + assert.Equal(t, tc.expected, config.OIDC.MinimumParameterEntropy) + + if n := len(tc.warnings); n == 0 { + assert.Len(t, validator.Warnings(), 0) + } else { + require.Len(t, validator.Warnings(), n) + + for i := 0; i < n; i++ { + assert.EqualError(t, validator.Warnings()[i], tc.warnings[i]) + } + } + + if n := len(tc.errors); n == 0 { + assert.Len(t, validator.Errors(), 0) + } else { + require.Len(t, validator.Errors(), n) + + for i := 0; i < n; i++ { + assert.EqualError(t, validator.Errors()[i], tc.errors[i]) + } + } + }) + } } func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "hmac1", IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-with-invalid-secret", Secret: tOpenIDConnectPlainTextClientSecret, @@ -572,11 +637,11 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidClientOptions(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "hmac1", IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "installed-app-client", Public: true, @@ -631,11 +696,11 @@ func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidClientOptions(t *te func TestValidateIdentityProvidersShouldRaiseWarningOnPlainTextClients(t *testing.T) { validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ + config := &schema.IdentityProviders{ + OIDC: &schema.OpenIDConnect{ HMACSecret: "hmac1", IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "client-with-invalid-secret_standard", Secret: tOpenIDConnectPlainTextClientSecret, @@ -658,8 +723,8 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnPlainTextClients(t *testin // All valid schemes are supported as defined in https://datatracker.ietf.org/doc/html/rfc8252#section-7.1 func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing.T) { - have := &schema.OpenIDConnectConfiguration{ - Clients: []schema.OpenIDConnectClientConfiguration{ + have := &schema.OpenIDConnect{ + Clients: []schema.OpenIDConnectClient{ { ID: "owncloud", RedirectURIs: []string{ @@ -706,8 +771,8 @@ func TestValidateOIDCClients(t *testing.T) { testCasses := []struct { name string - setup func(have *schema.OpenIDConnectConfiguration) - validate func(t *testing.T, have *schema.OpenIDConnectConfiguration) + setup func(have *schema.OpenIDConnect) + validate func(t *testing.T, have *schema.OpenIDConnect) have tcv expected tcv serrs []string // Soft errors which will be warnings before GA. @@ -1160,12 +1225,12 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldValidateCorrectRedirectURIsConfidentialClientType", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].RedirectURIs = []string{ "https://google.com", } }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, []string{"https://google.com"}, have.Clients[0].RedirectURIs) }, tcv{ @@ -1185,14 +1250,14 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldValidateCorrectRedirectURIsPublicClientType", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].Public = true have.Clients[0].Secret = nil have.Clients[0].RedirectURIs = []string{ oauth2InstalledApp, } }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, []string{oauth2InstalledApp}, have.Clients[0].RedirectURIs) }, tcv{ @@ -1212,12 +1277,12 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidRedirectURIsPublicOnly", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].RedirectURIs = []string{ "urn:ietf:wg:oauth:2.0:oob", } }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, []string{oauth2InstalledApp}, have.Clients[0].RedirectURIs) }, tcv{ @@ -1239,12 +1304,12 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidRedirectURIsMalformedURI", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].RedirectURIs = []string{ "http://abc@%two", } }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, []string{"http://abc@%two"}, have.Clients[0].RedirectURIs) }, tcv{ @@ -1266,12 +1331,12 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidRedirectURIsNotAbsolute", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].RedirectURIs = []string{ "google.com", } }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, []string{"google.com"}, have.Clients[0].RedirectURIs) }, tcv{ @@ -1293,13 +1358,13 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnDuplicateRedirectURI", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].RedirectURIs = []string{ "https://google.com", "https://google.com", } }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, []string{"https://google.com", "https://google.com"}, have.Clients[0].RedirectURIs) }, tcv{ @@ -1322,7 +1387,7 @@ func TestValidateOIDCClients(t *testing.T) { { "ShouldNotSetDefaultTokenEndpointClientAuthMethodConfidentialClientType", nil, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "", have.Clients[0].TokenEndpointAuthMethod) }, tcv{ @@ -1342,10 +1407,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldNotOverrideValidClientAuthMethod", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretPost }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.ClientAuthMethodClientSecretPost, have.Clients[0].TokenEndpointAuthMethod) }, tcv{ @@ -1365,10 +1430,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidClientAuthMethod", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = "client_credentials" }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "client_credentials", have.Clients[0].TokenEndpointAuthMethod) }, tcv{ @@ -1390,12 +1455,12 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidClientAuthMethodForPublicClientType", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretBasic have.Clients[0].Public = true have.Clients[0].Secret = nil }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.ClientAuthMethodClientSecretBasic, have.Clients[0].TokenEndpointAuthMethod) }, tcv{ @@ -1417,10 +1482,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidClientAuthMethodForConfidentialClientTypeAuthorizationCodeFlow", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodNone }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.ClientAuthMethodNone, have.Clients[0].TokenEndpointAuthMethod) }, tcv{ @@ -1442,10 +1507,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidClientAuthMethodForConfidentialClientTypeHybridFlow", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodNone }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.ClientAuthMethodNone, have.Clients[0].TokenEndpointAuthMethod) }, tcv{ @@ -1468,7 +1533,7 @@ func TestValidateOIDCClients(t *testing.T) { { "ShouldSetDefaultUserInfoAlg", nil, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.SigningAlgNone, have.Clients[0].UserinfoSigningAlg) }, tcv{ @@ -1488,10 +1553,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldNotOverrideUserInfoAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].UserinfoSigningAlg = oidc.SigningAlgRSAUsingSHA256 }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, have.Clients[0].UserinfoSigningAlg) }, tcv{ @@ -1511,10 +1576,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidUserInfoSigningAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].UserinfoSigningAlg = rs256 }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, rs256, have.Clients[0].UserinfoSigningAlg) }, tcv{ @@ -1536,10 +1601,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidIDTokenSigningAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].IDTokenSigningAlg = rs256 }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, rs256, have.Clients[0].IDTokenSigningAlg) }, tcv{ @@ -1562,7 +1627,7 @@ func TestValidateOIDCClients(t *testing.T) { { "ShouldSetDefaultConsentMode", nil, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "explicit", have.Clients[0].ConsentMode) }, tcv{ @@ -1582,10 +1647,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldSetDefaultConsentModeAuto", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].ConsentMode = auto }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "explicit", have.Clients[0].ConsentMode) }, tcv{ @@ -1605,13 +1670,13 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldSetDefaultConsentModePreConfigured", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { d := time.Minute have.Clients[0].ConsentMode = "" have.Clients[0].ConsentPreConfiguredDuration = &d }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "pre-configured", have.Clients[0].ConsentMode) }, tcv{ @@ -1631,13 +1696,13 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldSetDefaultConsentModeAutoPreConfigured", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { d := time.Minute have.Clients[0].ConsentMode = auto have.Clients[0].ConsentPreConfiguredDuration = &d }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "pre-configured", have.Clients[0].ConsentMode) }, tcv{ @@ -1657,10 +1722,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldNotOverrideConsentMode", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].ConsentMode = "implicit" }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "implicit", have.Clients[0].ConsentMode) }, tcv{ @@ -1680,10 +1745,10 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldSentConsentPreConfiguredDefaultDuration", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].ConsentMode = "pre-configured" }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, "pre-configured", have.Clients[0].ConsentMode) assert.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.ConsentPreConfiguredDuration, have.Clients[0].ConsentPreConfiguredDuration) }, @@ -1704,7 +1769,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnTokenEndpointClientAuthMethodPrivateKeyJWTMustSetAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodPrivateKeyJWT have.Clients[0].Secret = tOpenIDConnectPBKDF2ClientSecret }, @@ -1729,7 +1794,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnTokenEndpointClientAuthMethodPrivateKeyJWTMustSetAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodPrivateKeyJWT have.Clients[0].TokenEndpointAuthSigningAlg = "nope" have.Clients[0].Secret = tOpenIDConnectPBKDF2ClientSecret @@ -1755,7 +1820,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnTokenEndpointClientAuthMethodPrivateKeyJWTMustSetKnownAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodPrivateKeyJWT have.Clients[0].TokenEndpointAuthSigningAlg = oidc.SigningAlgECDSAUsingP384AndSHA384 have.Clients[0].Secret = tOpenIDConnectPBKDF2ClientSecret @@ -1787,7 +1852,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnTokenEndpointClientAuthMethodPrivateKeyJWTMustSetKnownAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodPrivateKeyJWT have.Clients[0].TokenEndpointAuthSigningAlg = oidc.SigningAlgECDSAUsingP384AndSHA384 have.Clients[0].Secret = tOpenIDConnectPBKDF2ClientSecret @@ -1812,7 +1877,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnIncorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT have.Clients[0].Secret = tOpenIDConnectPBKDF2ClientSecret }, @@ -1836,7 +1901,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldNotRaiseWarningOrErrorOnCorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret }, @@ -1858,7 +1923,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnIncorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT have.Clients[0].Secret = MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng") }, @@ -1882,7 +1947,7 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldNotRaiseWarningOrErrorOnCorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT have.Clients[0].Secret = MustDecodeSecret("$plaintext$abc123") }, @@ -1902,13 +1967,62 @@ func TestValidateOIDCClients(t *testing.T) { nil, nil, }, + { + "ShouldSetValidDefaultKeyID", + func(have *schema.OpenIDConnect) { + have.Clients[0].IDTokenSigningKeyID = "abcabc123" + have.Clients[0].UserinfoSigningKeyID = "abc123abc" + have.Discovery.ResponseObjectSigningKeyIDs = []string{"abcabc123", "abc123abc"} + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + nil, + }, + { + "ShouldRaiseErrorOnInvalidKeyID", + func(have *schema.OpenIDConnect) { + have.Clients[0].IDTokenSigningKeyID = "ab" + have.Clients[0].UserinfoSigningKeyID = "cd" + have.Discovery.ResponseObjectSigningKeyIDs = []string{"abc123xyz"} + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + []string{ + "identity_providers: oidc: clients: client 'test': option 'userinfo_signing_key_id' must be one of 'abc123xyz' but it's configured as 'cd'", + "identity_providers: oidc: clients: client 'test': option 'id_token_signing_key_id' must be one of 'abc123xyz' but it's configured as 'ab'", + }, + }, { "ShouldSetDefaultTokenEndpointAuthSigAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.SigningAlgHMACUsingSHA256, have.Clients[0].TokenEndpointAuthSigningAlg) }, tcv{ @@ -1928,13 +2042,13 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidPublicTokenAuthAlg", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT have.Clients[0].TokenEndpointAuthSigningAlg = oidc.SigningAlgHMACUsingSHA256 have.Clients[0].Secret = nil have.Clients[0].Public = true }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.SigningAlgHMACUsingSHA256, have.Clients[0].TokenEndpointAuthSigningAlg) }, tcv{ @@ -1956,12 +2070,12 @@ func TestValidateOIDCClients(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidTokenAuthAlgClientTypeConfidential", - func(have *schema.OpenIDConnectConfiguration) { + func(have *schema.OpenIDConnect) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT have.Clients[0].TokenEndpointAuthSigningAlg = oidc.EndpointToken have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + func(t *testing.T, have *schema.OpenIDConnect) { assert.Equal(t, oidc.EndpointToken, have.Clients[0].TokenEndpointAuthSigningAlg) }, tcv{ @@ -1987,11 +2101,11 @@ func TestValidateOIDCClients(t *testing.T) { for _, tc := range testCasses { t.Run(tc.name, func(t *testing.T) { - have := &schema.OpenIDConnectConfiguration{ + have := &schema.OpenIDConnect{ Discovery: schema.OpenIDConnectDiscovery{ ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "test", Secret: tOpenIDConnectPBKDF2ClientSecret, @@ -2067,8 +2181,8 @@ func TestValidateOIDCClientTokenEndpointAuthMethod(t *testing.T) { for _, tc := range testCasses { t.Run(tc.name, func(t *testing.T) { - have := &schema.OpenIDConnectConfiguration{ - Clients: []schema.OpenIDConnectClientConfiguration{ + have := &schema.OpenIDConnect{ + Clients: []schema.OpenIDConnectClient{ { ID: "test", Public: tc.public, @@ -2106,8 +2220,8 @@ func TestValidateOIDCClientJWKS(t *testing.T) { name string haveURI *url.URL haveJWKS []schema.JWK - setup func(config *schema.OpenIDConnectConfiguration) - expected func(t *testing.T, config *schema.OpenIDConnectConfiguration) + setup func(config *schema.OpenIDConnect) + expected func(t *testing.T, config *schema.OpenIDConnect) errs []string }{ { @@ -2276,7 +2390,7 @@ func TestValidateOIDCClientJWKS(t *testing.T) { {KeyID: "test", Use: "", Algorithm: "", Key: keyRSA2048PKCS8.Public()}, }, nil, - func(t *testing.T, config *schema.OpenIDConnectConfiguration) { + func(t *testing.T, config *schema.OpenIDConnect) { assert.Equal(t, oidc.KeyUseSignature, config.Clients[0].PublicKeys.Values[0].Use) assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, config.Clients[0].PublicKeys.Values[0].Algorithm) }, @@ -2289,7 +2403,7 @@ func TestValidateOIDCClientJWKS(t *testing.T) { {KeyID: "test", Use: "", Algorithm: "", Key: keyECDSAP256.Public()}, }, nil, - func(t *testing.T, config *schema.OpenIDConnectConfiguration) { + func(t *testing.T, config *schema.OpenIDConnect) { assert.Equal(t, oidc.KeyUseSignature, config.Clients[0].PublicKeys.Values[0].Use) assert.Equal(t, oidc.SigningAlgECDSAUsingP256AndSHA256, config.Clients[0].PublicKeys.Values[0].Algorithm) }, @@ -2302,7 +2416,7 @@ func TestValidateOIDCClientJWKS(t *testing.T) { {KeyID: "test", Use: "", Algorithm: "", Key: keyECDSAP384.Public()}, }, nil, - func(t *testing.T, config *schema.OpenIDConnectConfiguration) { + func(t *testing.T, config *schema.OpenIDConnect) { assert.Equal(t, oidc.KeyUseSignature, config.Clients[0].PublicKeys.Values[0].Use) assert.Equal(t, oidc.SigningAlgECDSAUsingP384AndSHA384, config.Clients[0].PublicKeys.Values[0].Algorithm) }, @@ -2315,7 +2429,7 @@ func TestValidateOIDCClientJWKS(t *testing.T) { {KeyID: "test", Use: "", Algorithm: "", Key: keyECDSAP521.Public()}, }, nil, - func(t *testing.T, config *schema.OpenIDConnectConfiguration) { + func(t *testing.T, config *schema.OpenIDConnect) { assert.Equal(t, oidc.KeyUseSignature, config.Clients[0].PublicKeys.Values[0].Use) assert.Equal(t, oidc.SigningAlgECDSAUsingP521AndSHA512, config.Clients[0].PublicKeys.Values[0].Algorithm) }, @@ -2328,7 +2442,7 @@ func TestValidateOIDCClientJWKS(t *testing.T) { {KeyID: "test", Use: "", Algorithm: "", Key: keyECDSAP521.Public()}, }, nil, - func(t *testing.T, config *schema.OpenIDConnectConfiguration) { + func(t *testing.T, config *schema.OpenIDConnect) { assert.Equal(t, oidc.KeyUseSignature, config.Clients[0].PublicKeys.Values[0].Use) assert.Equal(t, oidc.SigningAlgECDSAUsingP521AndSHA512, config.Clients[0].PublicKeys.Values[0].Algorithm) @@ -2342,10 +2456,10 @@ func TestValidateOIDCClientJWKS(t *testing.T) { []schema.JWK{ {KeyID: "test", Use: "", Algorithm: "", Key: keyECDSAP521.Public()}, }, - func(config *schema.OpenIDConnectConfiguration) { + func(config *schema.OpenIDConnect) { config.Clients[0].RequestObjectSigningAlg = oidc.SigningAlgRSAUsingSHA512 }, - func(t *testing.T, config *schema.OpenIDConnectConfiguration) { + func(t *testing.T, config *schema.OpenIDConnect) { assert.Equal(t, oidc.KeyUseSignature, config.Clients[0].PublicKeys.Values[0].Use) assert.Equal(t, oidc.SigningAlgECDSAUsingP521AndSHA512, config.Clients[0].PublicKeys.Values[0].Algorithm) @@ -2359,8 +2473,8 @@ func TestValidateOIDCClientJWKS(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - config := &schema.OpenIDConnectConfiguration{ - Clients: []schema.OpenIDConnectClientConfiguration{ + config := &schema.OpenIDConnect{ + Clients: []schema.OpenIDConnectClient{ { ID: "test", PublicKeys: schema.OpenIDConnectClientPublicKeys{ @@ -2407,22 +2521,22 @@ func TestValidateOIDCIssuer(t *testing.T) { testCases := []struct { name string - have schema.OpenIDConnectConfiguration - expected schema.OpenIDConnectConfiguration + have schema.OpenIDConnect + expected schema.OpenIDConnect errs []string }{ { "ShouldMapLegacyConfiguration", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKey: keyRSA2048, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKey: keyRSA2048, IssuerPrivateKeys: []schema.JWK{ {KeyID: "1f8bfc", Key: keyRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "1f8bfc", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "1f8bfc"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, @@ -2430,7 +2544,7 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldSetDefaultKeyValues", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA2048, CertificateChain: certRSA2048}, {Key: keyECDSAP256, CertificateChain: certECDSAP256}, @@ -2438,7 +2552,7 @@ func TestValidateOIDCIssuer(t *testing.T) { {Key: keyECDSAP521, CertificateChain: certECDSAP521}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "1f8bfc"}, {Key: keyECDSAP256, CertificateChain: certECDSAP256, Algorithm: oidc.SigningAlgECDSAUsingP256AndSHA256, Use: oidc.KeyUseSignature, KeyID: "1e7788"}, @@ -2446,47 +2560,50 @@ func TestValidateOIDCIssuer(t *testing.T) { {Key: keyECDSAP521, CertificateChain: certECDSAP521, Algorithm: oidc.SigningAlgECDSAUsingP521AndSHA512, Use: oidc.KeyUseSignature, KeyID: "7ecbac"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "1f8bfc", + DefaultKeyIDs: map[string]string{ + oidc.SigningAlgRSAUsingSHA256: "1f8bfc", + oidc.SigningAlgECDSAUsingP256AndSHA256: "1e7788", + oidc.SigningAlgECDSAUsingP384AndSHA384: "ba8508", + oidc.SigningAlgECDSAUsingP521AndSHA512: "7ecbac", + }, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512}, }, }, nil, }, { - "ShouldRaiseErrorsDuplicateRSA256Keys", - schema.OpenIDConnectConfiguration{ + "ShouldNotRaiseErrorsMultipleRSA256Keys", + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA2048, CertificateChain: certRSA2048}, {Key: keyRSA4096, CertificateChain: certRSA4096}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "1f8bfc"}, {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "bf1e10"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "1f8bfc", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "1f8bfc"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, - []string{ - "identity_providers: oidc: issuer_private_keys: key #2 with key id 'bf1e10': option 'algorithm' must be unique", - }, + nil, }, { "ShouldRaiseErrorsDuplicateRSA256Keys", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA512}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA512, Use: oidc.KeyUseSignature, KeyID: "bf1e10"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA512: "bf1e10"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA512}, }, }, @@ -2496,19 +2613,19 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnBadCurve", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096}, {Key: keyECDSAP224, CertificateChain: certECDSAP224}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "bf1e10"}, {Key: keyECDSAP224, CertificateChain: certECDSAP224}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "bf1e10", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "bf1e10"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, @@ -2518,17 +2635,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnBadRSAKey", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA1024, CertificateChain: certRSA1024}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA1024, CertificateChain: certRSA1024, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "cf375e"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "cf375e", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "cf375e"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, @@ -2538,17 +2655,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnBadAlg", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: "invalid"}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: "invalid", Use: oidc.KeyUseSignature, KeyID: "bf1e10"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "", + DefaultKeyIDs: map[string]string{}, ResponseObjectSigningAlgs: []string{"invalid"}, }, }, @@ -2559,17 +2676,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnBadUse", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Use: "invalid"}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: "invalid", KeyID: "bf1e10"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "bf1e10", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "bf1e10"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, @@ -2579,59 +2696,87 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnBadKeyIDLength", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ - {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "thisistoolong"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "thisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolong"}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ - {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "thisistoolong"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "thisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolong"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "thisistoolong", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "thisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolong"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, []string{ - "identity_providers: oidc: issuer_private_keys: key #1 with key id 'thisistoolong': option `key_id`` must be 7 characters or less", + "identity_providers: oidc: issuer_private_keys: key #1 with key id 'thisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolongthisistoolong': option `key_id` must be 100 characters or less", }, }, { "ShouldRaiseErrorOnBadKeyIDCharacters", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x@x"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "-xx"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "xx."}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x@x"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "-xx"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "xx."}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "x@x", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "x@x"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, []string{ - "identity_providers: oidc: issuer_private_keys: key #1 with key id 'x@x': option 'key_id' must only have alphanumeric characters", + "identity_providers: oidc: issuer_private_keys: key #1 with key id 'x@x': option 'key_id' must only contain RFC3986 unreserved characters and must only start and end with alphanumeric characters", + "identity_providers: oidc: issuer_private_keys: key #2 with key id '-xx': option 'key_id' must only contain RFC3986 unreserved characters and must only start and end with alphanumeric characters", + "identity_providers: oidc: issuer_private_keys: key #3 with key id 'xx.': option 'key_id' must only contain RFC3986 unreserved characters and must only start and end with alphanumeric characters", }, }, + { + "ShouldNotRaiseErrorOnGoodKeyIDCharacters", + schema.OpenIDConnect{ + IssuerPrivateKeys: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x-x"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "xx"}, + }, + }, + schema.OpenIDConnect{ + IssuerPrivateKeys: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x-x"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "xx"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "x-x"}, + ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + nil, + }, { "ShouldRaiseErrorOnBadKeyIDDuplicates", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x"}, {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAPSSUsingSHA256, KeyID: "x"}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x"}, {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAPSSUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "x", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "x", oidc.SigningAlgRSAPSSUsingSHA256: "x"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA256}, }, }, @@ -2641,17 +2786,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnEd25519Keys", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyEd2519, CertificateChain: certEd15519}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyEd2519, CertificateChain: certEd15519, KeyID: "14dfd3"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "", + DefaultKeyIDs: map[string]string{}, ResponseObjectSigningAlgs: []string(nil), }, }, @@ -2661,17 +2806,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnCertificateAsKey", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: publicRSA2048Pair}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: publicRSA2048Pair, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "9a0e71"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "9a0e71", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "9a0e71"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, @@ -2681,17 +2826,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidChain", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA2048, CertificateChain: frankenchain}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA2048, CertificateChain: frankenchain, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "1f8bfc"}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "1f8bfc", + DefaultKeyIDs: map[string]string{oidc.SigningAlgRSAUsingSHA256: "1f8bfc"}, ResponseObjectSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, }, }, @@ -2701,17 +2846,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnInvalidPrivateKeyN", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: frankenkey}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: frankenkey}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "", + DefaultKeyIDs: map[string]string{}, ResponseObjectSigningAlgs: []string(nil), }, }, @@ -2721,17 +2866,17 @@ func TestValidateOIDCIssuer(t *testing.T) { }, { "ShouldRaiseErrorOnCertForKey", - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: certRSA2048}, }, }, - schema.OpenIDConnectConfiguration{ + schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: certRSA2048}, }, Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "", + DefaultKeyIDs: map[string]string{}, ResponseObjectSigningAlgs: []string(nil), }, }, @@ -2749,7 +2894,7 @@ func TestValidateOIDCIssuer(t *testing.T) { validateOIDCIssuer(&tc.have, val) - assert.Equal(t, tc.expected.Discovery.DefaultKeyID, tc.have.Discovery.DefaultKeyID) + assert.Equal(t, tc.expected.Discovery.DefaultKeyIDs, tc.have.Discovery.DefaultKeyIDs) assert.Equal(t, tc.expected.Discovery.ResponseObjectSigningAlgs, tc.have.Discovery.ResponseObjectSigningAlgs) assert.Equal(t, tc.expected.IssuerPrivateKey, tc.have.IssuerPrivateKey) assert.Equal(t, tc.expected.IssuerCertificateChain, tc.have.IssuerCertificateChain) diff --git a/internal/configuration/validator/util.go b/internal/configuration/validator/util.go index 9ccf235c5..037e58d0e 100644 --- a/internal/configuration/validator/util.go +++ b/internal/configuration/validator/util.go @@ -1,6 +1,7 @@ package validator import ( + "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" @@ -9,6 +10,7 @@ import ( "strings" "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/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) } } + +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 +} diff --git a/internal/configuration/validator/util_test.go b/internal/configuration/validator/util_test.go index 2e3f14542..6a4a8c52c 100644 --- a/internal/configuration/validator/util_test.go +++ b/internal/configuration/validator/util_test.go @@ -79,3 +79,16 @@ func TestSchemaJWKGetPropertiesMissingTests(t *testing.T) { assert.Equal(t, nil, props.Curve) 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")) +} diff --git a/internal/handlers/handler_authz_builder_test.go b/internal/handlers/handler_authz_builder_test.go new file mode 100644 index 000000000..dc4bff1fd --- /dev/null +++ b/internal/handlers/handler_authz_builder_test.go @@ -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) +} diff --git a/internal/handlers/handler_authz_misc_test.go b/internal/handlers/handler_authz_misc_test.go new file mode 100644 index 000000000..ad57c21b2 --- /dev/null +++ b/internal/handlers/handler_authz_misc_test.go @@ -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"}}) +} diff --git a/internal/handlers/handler_oidc_authorization.go b/internal/handlers/handler_oidc_authorization.go index 2b1af3ed5..e97598385 100644 --- a/internal/handlers/handler_oidc_authorization.go +++ b/internal/handlers/handler_oidc_authorization.go @@ -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) - session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKeyIDFromAlg(ctx, client.GetIDTokenSigningAlg()), - userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester) + session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKeyID(ctx, client.GetIDTokenSigningKeyID(), client.GetIDTokenSigningAlg()), 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", requester.GetID(), session.ClientID, session.Subject, session.Username, session.Claims) diff --git a/internal/handlers/handler_oidc_userinfo.go b/internal/handlers/handler_oidc_userinfo.go index 9e98a0f4a..6ad16c2fe 100644 --- a/internal/handlers/handler_oidc_userinfo.go +++ b/internal/handlers/handler_oidc_userinfo.go @@ -105,7 +105,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, default: 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))) return diff --git a/internal/metrics/prometheus_test.go b/internal/metrics/prometheus_test.go new file mode 100644 index 000000000..fd0220fbc --- /dev/null +++ b/internal/metrics/prometheus_test.go @@ -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) +} diff --git a/internal/middlewares/authelia_context_test.go b/internal/middlewares/authelia_context_test.go index bd8d0a50e..13095d3a0 100644 --- a/internal/middlewares/authelia_context_test.go +++ b/internal/middlewares/authelia_context_test.go @@ -154,6 +154,8 @@ func TestIssuerURL(t *testing.T) { func TestShouldCallNextWithAutheliaCtx(t *testing.T) { ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := &fasthttp.RequestCtx{} configuration := schema.Configuration{} userProvider := mocks.NewMockUserProvider(ctrl) diff --git a/internal/oidc/authentication_test.go b/internal/oidc/authentication_test.go index 3c666a9db..206a9dffb 100644 --- a/internal/oidc/authentication_test.go +++ b/internal/oidc/authentication_test.go @@ -199,12 +199,12 @@ func (s *ClientAuthenticationStrategySuite) SetupTest() { secret := tOpenIDConnectPlainTextClientSecret - s.provider = oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + s.provider = oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ {Key: keyRSA2048, CertificateChain: certRSA2048, Use: oidc.KeyUseSignature, Algorithm: oidc.SigningAlgRSAUsingSHA256}, }, HMACSecret: "abc123", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "hs256", Secret: secret, diff --git a/internal/oidc/client.go b/internal/oidc/client.go index 2950e4ca6..71b5ceaa8 100644 --- a/internal/oidc/client.go +++ b/internal/oidc/client.go @@ -13,7 +13,7 @@ import ( ) // NewClient creates a new Client. -func NewClient(config schema.OpenIDConnectClientConfiguration) (client Client) { +func NewClient(config schema.OpenIDConnectClient) (client Client) { base := &BaseClient{ ID: config.ID, Description: config.Description, @@ -34,8 +34,10 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client Client) { EnforcePAR: config.EnforcePAR, - IDTokenSigningAlg: config.IDTokenSigningAlg, - UserinfoSigningAlg: config.UserinfoSigningAlg, + IDTokenSigningAlg: config.IDTokenSigningAlg, + IDTokenSigningKeyID: config.IDTokenSigningKeyID, + UserinfoSigningAlg: config.UserinfoSigningAlg, + UserinfoSigningKeyID: config.UserinfoSigningKeyID, Policy: authorization.NewLevel(config.Policy), @@ -151,6 +153,11 @@ func (c *BaseClient) GetIDTokenSigningAlg() (alg string) { return c.IDTokenSigningAlg } +// GetIDTokenSigningKeyID returns the IDTokenSigningKeyID. +func (c *BaseClient) GetIDTokenSigningKeyID() (alg string) { + return c.IDTokenSigningKeyID +} + // GetUserinfoSigningAlg returns the UserinfoSigningAlg. func (c *BaseClient) GetUserinfoSigningAlg() string { if c.UserinfoSigningAlg == "" { @@ -160,6 +167,11 @@ func (c *BaseClient) GetUserinfoSigningAlg() string { return c.UserinfoSigningAlg } +// GetUserinfoSigningKeyID returns the UserinfoSigningKeyID. +func (c *BaseClient) GetUserinfoSigningKeyID() (kid string) { + return c.UserinfoSigningKeyID +} + // GetPAREnforcement returns EnforcePAR. func (c *BaseClient) GetPAREnforcement() bool { return c.EnforcePAR diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go index b8c5d3e29..98d0ee9f8 100644 --- a/internal/oidc/client_test.go +++ b/internal/oidc/client_test.go @@ -18,7 +18,7 @@ import ( ) func TestNewClient(t *testing.T) { - config := schema.OpenIDConnectClientConfiguration{} + config := schema.OpenIDConnectClient{} client := oidc.NewClient(config) assert.Equal(t, "", client.GetID()) assert.Equal(t, "", client.GetDescription()) @@ -30,11 +30,12 @@ func TestNewClient(t *testing.T) { require.True(t, ok) assert.Equal(t, "", bclient.UserinfoSigningAlg) assert.Equal(t, oidc.SigningAlgNone, client.GetUserinfoSigningAlg()) + assert.Equal(t, "", client.GetUserinfoSigningKeyID()) _, ok = client.(*oidc.FullClient) assert.False(t, ok) - config = schema.OpenIDConnectClientConfiguration{ + config = schema.OpenIDConnectClient{ ID: myclient, Description: myclientdesc, Policy: twofactor, @@ -52,7 +53,7 @@ func TestNewClient(t *testing.T) { assert.Equal(t, fosite.ResponseModeFormPost, client.GetResponseModes()[0]) assert.Equal(t, authorization.TwoFactor, client.GetAuthorizationPolicy()) - config = schema.OpenIDConnectClientConfiguration{ + config = schema.OpenIDConnectClient{ TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, } @@ -64,12 +65,32 @@ func TestNewClient(t *testing.T) { assert.Equal(t, "", fclient.UserinfoSigningAlg) assert.Equal(t, oidc.SigningAlgNone, client.GetUserinfoSigningAlg()) + assert.Equal(t, oidc.SigningAlgNone, fclient.GetUserinfoSigningAlg()) 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, oidc.SigningAlgRSAUsingSHA256, client.GetIDTokenSigningAlg()) + assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, fclient.GetIDTokenSigningAlg()) 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.GetTokenEndpointAuthMethod()) @@ -81,6 +102,7 @@ func TestNewClient(t *testing.T) { assert.Equal(t, "", fclient.GetRequestObjectSigningAlgorithm()) fclient.RequestObjectSigningAlgorithm = oidc.SigningAlgRSAUsingSHA256 + assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, fclient.GetRequestObjectSigningAlgorithm()) assert.Equal(t, "", fclient.JSONWebKeysURI) @@ -355,7 +377,7 @@ func TestClient_GetResponseTypes(t *testing.T) { func TestNewClientPKCE(t *testing.T) { testCases := []struct { name string - have schema.OpenIDConnectClientConfiguration + have schema.OpenIDConnectClient expectedEnforcePKCE bool expectedEnforcePKCEChallengeMethod bool expected string @@ -365,7 +387,7 @@ func TestNewClientPKCE(t *testing.T) { }{ { "ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest", - schema.OpenIDConnectClientConfiguration{}, + schema.OpenIDConnectClient{}, false, false, "", @@ -375,7 +397,7 @@ func TestNewClientPKCE(t *testing.T) { }, { "ShouldEnforcePKCEAndErrorOnNonPKCERequest", - schema.OpenIDConnectClientConfiguration{EnforcePKCE: true}, + schema.OpenIDConnectClient{EnforcePKCE: true}, true, false, "", @@ -385,7 +407,7 @@ func TestNewClientPKCE(t *testing.T) { }, { "ShouldEnforcePKCEAndNotErrorOnPKCERequest", - schema.OpenIDConnectClientConfiguration{EnforcePKCE: true}, + schema.OpenIDConnectClient{EnforcePKCE: true}, true, false, "", @@ -394,7 +416,7 @@ func TestNewClientPKCE(t *testing.T) { "", }, {"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest", - schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"}, + schema.OpenIDConnectClient{PKCEChallengeMethod: "S256"}, true, true, "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.", }, {"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod", - schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"}, + schema.OpenIDConnectClient{PKCEChallengeMethod: "S256"}, true, true, "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.", }, {"ShouldEnforcePKCEFromChallengeMethodAndNotErrorOnValidRequest", - schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"}, + schema.OpenIDConnectClient{PKCEChallengeMethod: "S256"}, true, true, "S256", @@ -448,7 +470,7 @@ func TestNewClientPKCE(t *testing.T) { func TestNewClientPAR(t *testing.T) { testCases := []struct { name string - have schema.OpenIDConnectClientConfiguration + have schema.OpenIDConnectClient expected bool r *fosite.Request err string @@ -456,7 +478,7 @@ func TestNewClientPAR(t *testing.T) { }{ { "ShouldNotEnforcEPARAndNotErrorOnNonPARRequest", - schema.OpenIDConnectClientConfiguration{}, + schema.OpenIDConnectClient{}, false, &fosite.Request{}, "", @@ -464,7 +486,7 @@ func TestNewClientPAR(t *testing.T) { }, { "ShouldEnforcePARAndErrorOnNonPARRequest", - schema.OpenIDConnectClientConfiguration{EnforcePAR: true}, + schema.OpenIDConnectClient{EnforcePAR: true}, true, &fosite.Request{}, "invalid_request", @@ -472,14 +494,14 @@ func TestNewClientPAR(t *testing.T) { }, { "ShouldEnforcePARAndErrorOnNonPARRequest", - schema.OpenIDConnectClientConfiguration{EnforcePAR: true}, + schema.OpenIDConnectClient{EnforcePAR: true}, true, &fosite.Request{Form: map[string][]string{oidc.FormParameterRequestURI: {"https://example.com"}}}, "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."}, { "ShouldEnforcePARAndNotErrorOnPARRequest", - schema.OpenIDConnectClientConfiguration{EnforcePAR: true}, + schema.OpenIDConnectClient{EnforcePAR: true}, true, &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) { testCases := []struct { name string - have schema.OpenIDConnectClientConfiguration + have schema.OpenIDConnectClient expected []fosite.ResponseModeType r *fosite.AuthorizeRequest err string @@ -519,7 +541,7 @@ func TestNewClientResponseModes(t *testing.T) { }{ { "ShouldEnforceResponseModePolicyAndAllowDefaultModeQuery", - schema.OpenIDConnectClientConfiguration{ResponseModes: []string{oidc.ResponseModeQuery}}, + schema.OpenIDConnectClient{ResponseModes: []string{oidc.ResponseModeQuery}}, []fosite.ResponseModeType{fosite.ResponseModeQuery}, &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", - schema.OpenIDConnectClientConfiguration{ResponseModes: []string{oidc.ResponseModeFormPost}}, + schema.OpenIDConnectClient{ResponseModes: []string{oidc.ResponseModeFormPost}}, []fosite.ResponseModeType{fosite.ResponseModeFormPost}, &fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{oidc.FormParameterResponseMode: nil}}}, "unsupported_response_mode", @@ -535,7 +557,7 @@ func TestNewClientResponseModes(t *testing.T) { }, { "ShouldNotEnforceConfiguredResponseMode", - schema.OpenIDConnectClientConfiguration{ResponseModes: []string{oidc.ResponseModeFormPost}}, + schema.OpenIDConnectClient{ResponseModes: []string{oidc.ResponseModeFormPost}}, []fosite.ResponseModeType{fosite.ResponseModeFormPost}, &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", - schema.OpenIDConnectClientConfiguration{ResponseModes: []string{}}, + schema.OpenIDConnectClient{ResponseModes: []string{}}, []fosite.ResponseModeType{}, &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 ) - client = oidc.NewClient(schema.OpenIDConnectClientConfiguration{ + client = oidc.NewClient(schema.OpenIDConnectClient{ TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, PublicKeys: schema.OpenIDConnectClientPublicKeys{ URI: MustParseRequestURI("https://google.com"), @@ -603,7 +625,7 @@ func TestNewClient_JSONWebKeySetURI(t *testing.T) { assert.Equal(t, "https://google.com", clientf.GetJSONWebKeysURI()) - client = oidc.NewClient(schema.OpenIDConnectClientConfiguration{ + client = oidc.NewClient(schema.OpenIDConnectClient{ TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, PublicKeys: schema.OpenIDConnectClientPublicKeys{ URI: nil, diff --git a/internal/oidc/config.go b/internal/oidc/config.go index 6dbd29e34..44fe989c0 100644 --- a/internal/oidc/config.go +++ b/internal/oidc/config.go @@ -23,7 +23,7 @@ import ( "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{ GlobalSecret: []byte(utils.HashSHA256FromString(config.HMACSecret)), SendDebugMessagesToClients: config.EnableClientDebugMessages, diff --git a/internal/oidc/const.go b/internal/oidc/const.go index 911ce4175..0db403f90 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -118,6 +118,14 @@ const ( SigningAlgHMACUsingSHA512 = "HS512" ) +// JWS Algorithm Prefixes. +const ( + SigningAlgPrefixRSA = "RS" + SigningAlgPrefixHMAC = "HS" + SigningAlgPrefixRSAPSS = "PS" + SigningAlgPrefixECDSA = "ES" +) + const ( KeyUseSignature = "sig" ) diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index 919a5955a..b3c147f89 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -8,7 +8,7 @@ import ( ) // NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration. -func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration) (config OpenIDConnectWellKnownConfiguration) { +func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnect) (config OpenIDConnectWellKnownConfiguration) { config = OpenIDConnectWellKnownConfiguration{ OAuth2WellKnownConfiguration: OAuth2WellKnownConfiguration{ CommonDiscoveryOptions: CommonDiscoveryOptions{ @@ -73,6 +73,15 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration SigningAlgHMACUsingSHA256, SigningAlgHMACUsingSHA384, SigningAlgHMACUsingSHA512, + SigningAlgRSAUsingSHA256, + SigningAlgRSAUsingSHA384, + SigningAlgRSAUsingSHA512, + SigningAlgECDSAUsingP256AndSHA256, + SigningAlgECDSAUsingP384AndSHA384, + SigningAlgECDSAUsingP521AndSHA512, + SigningAlgRSAPSSUsingSHA256, + SigningAlgRSAPSSUsingSHA384, + SigningAlgRSAPSSUsingSHA512, }, }, OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{ @@ -90,6 +99,15 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration SigningAlgHMACUsingSHA256, SigningAlgHMACUsingSHA384, SigningAlgHMACUsingSHA512, + SigningAlgRSAUsingSHA256, + SigningAlgRSAUsingSHA384, + SigningAlgRSAUsingSHA512, + SigningAlgECDSAUsingP256AndSHA256, + SigningAlgECDSAUsingP384AndSHA384, + SigningAlgECDSAUsingP521AndSHA512, + SigningAlgRSAPSSUsingSHA256, + SigningAlgRSAPSSUsingSHA384, + SigningAlgRSAPSSUsingSHA512, }, IntrospectionEndpointAuthMethodsSupported: []string{ ClientAuthMethodClientSecretBasic, @@ -104,6 +122,7 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{ IDTokenSigningAlgValuesSupported: []string{ SigningAlgRSAUsingSHA256, + SigningAlgNone, }, UserinfoSigningAlgValuesSupported: []string{ SigningAlgRSAUsingSHA256, @@ -111,11 +130,17 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration }, RequestObjectSigningAlgValuesSupported: []string{ SigningAlgRSAUsingSHA256, + SigningAlgRSAUsingSHA384, + SigningAlgRSAUsingSHA512, + SigningAlgECDSAUsingP256AndSHA256, + SigningAlgECDSAUsingP384AndSHA384, + SigningAlgECDSAUsingP521AndSHA512, + SigningAlgRSAPSSUsingSHA256, + SigningAlgRSAPSSUsingSHA384, + SigningAlgRSAPSSUsingSHA512, SigningAlgNone, }, }, - OpenIDConnectFrontChannelLogoutDiscoveryOptions: &OpenIDConnectFrontChannelLogoutDiscoveryOptions{}, - OpenIDConnectBackChannelLogoutDiscoveryOptions: &OpenIDConnectBackChannelLogoutDiscoveryOptions{}, OpenIDConnectPromptCreateDiscoveryOptions: &OpenIDConnectPromptCreateDiscoveryOptions{ PromptValuesSupported: []string{ 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.UserinfoSigningAlgValuesSupported)) - sort.Sort(SortedSigningAlgs(config.RequestObjectSigningAlgValuesSupported)) - sort.Sort(SortedSigningAlgs(config.RevocationEndpointAuthSigningAlgValuesSupported)) - sort.Sort(SortedSigningAlgs(config.TokenEndpointAuthSigningAlgValuesSupported)) if c.EnablePKCEPlainChallenge { config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain) diff --git a/internal/oidc/discovery_test.go b/internal/oidc/discovery_test.go index e971f5ff6..24e033f02 100644 --- a/internal/oidc/discovery_test.go +++ b/internal/oidc/discovery_test.go @@ -25,118 +25,51 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { expectedRequestObjectSigAlgsSupported, expectedRevocationSigAlgsSupported, expectedTokenAuthSigAlgsSupported []string }{ { - desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublic", + desc: "ShouldHaveStandardCodeChallengeMethods", pkcePlainChallenge: false, clients: map[string]oidc.Client{"a": &oidc.BaseClient{}}, expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256}, expectSubjectTypesSupported: []string{oidc.SubjectTypePublic, oidc.SubjectTypePairwise}, - expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256}, + expectedIDTokenSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, 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}, + 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: "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, clients: map[string]oidc.Client{"a": &oidc.BaseClient{}}, discovery: schema.OpenIDConnectDiscovery{ ResponseObjectSigningAlgs: []string{oidc.SigningAlgECDSAUsingP521AndSHA512}, - RequestObjectSigningAlgs: []string{oidc.SigningAlgECDSAUsingP256AndSHA256}, }, expectCodeChallengeMethodsSupported: []string{oidc.PKCEChallengeMethodSHA256}, 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}, - expectedRequestObjectSigAlgsSupported: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgNone}, - expectedRevocationSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256}, - expectedTokenAuthSigAlgsSupported: []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256}, - }, - { - 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}, + 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}, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - c := schema.OpenIDConnectConfiguration{ + c := schema.OpenIDConnect{ EnablePKCEPlainChallenge: tc.pkcePlainChallenge, - PAR: schema.OpenIDConnectPARConfiguration{ + PAR: schema.OpenIDConnectPAR{ Enforce: tc.enforcePAR, }, Discovery: tc.discovery, @@ -169,12 +102,12 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { } func TestNewOpenIDConnectProviderDiscovery(t *testing.T) { - provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", EnablePKCEPlainChallenge: true, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "a-client", Secret: tOpenIDConnectPlainTextClientSecret, @@ -210,11 +143,11 @@ func TestNewOpenIDConnectProviderDiscovery(t *testing.T) { } func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *testing.T) { - provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "a-client", Secret: tOpenIDConnectPlainTextClientSecret, @@ -281,35 +214,13 @@ func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *test assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodPrivateKeyJWT) assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone) - assert.Len(t, disco.IntrospectionEndpointAuthMethodsSupported, 2) - assert.Contains(t, disco.IntrospectionEndpointAuthMethodsSupported, oidc.ClientAuthMethodClientSecretBasic) - assert.Contains(t, disco.IntrospectionEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone) - - assert.Len(t, disco.GrantTypesSupported, 3) - assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeAuthorizationCode) - assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeRefreshToken) - 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.Equal(t, []string{oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodNone}, disco.IntrospectionEndpointAuthMethodsSupported) + assert.Equal(t, []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken}, disco.GrantTypesSupported) + 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.Equal(t, []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, disco.IDTokenSigningAlgValuesSupported) + assert.Equal(t, []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, disco.UserinfoSigningAlgValuesSupported) + 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.Len(t, disco.ClaimsSupported, 18) assert.Contains(t, disco.ClaimsSupported, oidc.ClaimAuthenticationMethodsReference) @@ -337,11 +248,11 @@ func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *test } func TestNewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) { - provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "a-client", Secret: tOpenIDConnectPlainTextClientSecret, @@ -427,12 +338,12 @@ func TestNewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) } func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfigurationWithPlainPKCE(t *testing.T) { - provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", EnablePKCEPlainChallenge: true, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "a-client", Secret: tOpenIDConnectPlainTextClientSecret, diff --git a/internal/oidc/keys.go b/internal/oidc/keys.go index 51fe0dcdc..3503a555f 100644 --- a/internal/oidc/keys.go +++ b/internal/oidc/keys.go @@ -13,17 +13,17 @@ import ( "github.com/golang-jwt/jwt/v4" fjwt "github.com/ory/fosite/token/jwt" "github.com/ory/x/errorsx" - "gopkg.in/square/go-jose.v2" "github.com/authelia/authelia/v4/internal/configuration/schema" ) // NewKeyManager news up a KeyManager. -func NewKeyManager(config *schema.OpenIDConnectConfiguration) (manager *KeyManager) { +func NewKeyManager(config *schema.OpenIDConnect) (manager *KeyManager) { manager = &KeyManager{ - kids: map[string]*JWK{}, - algs: map[string]*JWK{}, + alg2kid: config.Discovery.DefaultKeyIDs, + kids: map[string]*JWK{}, + algs: map[string]*JWK{}, } for _, sjwk := range config.IssuerPrivateKeys { @@ -31,10 +31,6 @@ func NewKeyManager(config *schema.OpenIDConnectConfiguration) (manager *KeyManag manager.kids[sjwk.KeyID] = jwk manager.algs[jwk.alg.Alg()] = jwk - - if jwk.kid == config.Discovery.DefaultKeyID { - manager.kid = jwk.kid - } } return manager @@ -42,14 +38,29 @@ func NewKeyManager(config *schema.OpenIDConnectConfiguration) (manager *KeyManag // The KeyManager type handles JWKs and signing operations. type KeyManager struct { - kid string - kids map[string]*JWK - algs map[string]*JWK + alg2kid map[string]string + kids map[string]*JWK + algs map[string]*JWK } -// GetKeyID returns the default key id. -func (m *KeyManager) GetKeyID(ctx context.Context) string { - return m.kid +// GetDefaultKeyID returns the default key id. +func (m *KeyManager) GetDefaultKeyID(ctx context.Context) string { + 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. @@ -67,7 +78,20 @@ func (m *KeyManager) GetKeyIDFromAlg(ctx context.Context, alg string) string { 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. @@ -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. func (m *KeyManager) GetByKID(ctx context.Context, kid string) *JWK { if kid == "" { - return m.kids[m.kid] + return m.kids[m.alg2kid[SigningAlgRSAUsingSHA256]] } if jwk, ok := m.kids[kid]; ok { diff --git a/internal/oidc/keys_blackbox_test.go b/internal/oidc/keys_blackbox_test.go index 5bbe43336..8b25729b0 100644 --- a/internal/oidc/keys_blackbox_test.go +++ b/internal/oidc/keys_blackbox_test.go @@ -17,10 +17,7 @@ import ( ) func TestKeyManager(t *testing.T) { - config := &schema.OpenIDConnectConfiguration{ - Discovery: schema.OpenIDConnectDiscovery{ - DefaultKeyID: "kid-RS256-sig", - }, + config := &schema.OpenIDConnect{ IssuerPrivateKeys: []schema.JWK{ { Use: oidc.KeyUseSignature, @@ -79,8 +76,16 @@ func TestKeyManager(t *testing.T) { }, } + config.Discovery.DefaultKeyIDs = map[string]string{} + 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) @@ -89,7 +94,18 @@ func TestKeyManager(t *testing.T) { 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 ( jwk *oidc.JWK @@ -107,7 +123,7 @@ func TestKeyManager(t *testing.T) { jwk = manager.GetByKID(ctx, "") 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"}}) 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) 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) diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index 1f7837989..915951920 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -13,7 +13,7 @@ import ( ) // 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 { return nil } diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go index 8e17af71e..5d5fc33f9 100644 --- a/internal/oidc/provider_test.go +++ b/internal/oidc/provider_test.go @@ -18,12 +18,12 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing } func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) { - provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, EnablePKCEPlainChallenge: true, HMACSecret: badhmac, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: myclient, Secret: tOpenIDConnectPlainTextClientSecret, @@ -50,11 +50,11 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing } func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) { - provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := oidc.NewOpenIDConnectProvider(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, HMACSecret: badhmac, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: "a-client", Secret: tOpenIDConnectPlainTextClientSecret, diff --git a/internal/oidc/store.go b/internal/oidc/store.go index 788de40ab..c39b52530 100644 --- a/internal/oidc/store.go +++ b/internal/oidc/store.go @@ -18,8 +18,8 @@ import ( "github.com/authelia/authelia/v4/internal/storage" ) -// NewStore returns a Store when provided with a schema.OpenIDConnectConfiguration and storage.Provider. -func NewStore(config *schema.OpenIDConnectConfiguration, provider storage.Provider) (store *Store) { +// NewStore returns a Store when provided with a schema.OpenIDConnect and storage.Provider. +func NewStore(config *schema.OpenIDConnect, provider storage.Provider) (store *Store) { logger := logging.Logger() store = &Store{ diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go index f6eb95818..2cf670e9c 100644 --- a/internal/oidc/store_test.go +++ b/internal/oidc/store_test.go @@ -24,10 +24,10 @@ import ( ) func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { - s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ + s := oidc.NewStore(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: myclient, Description: myclientdesc, @@ -56,10 +56,10 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { } func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { - s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ + s := oidc.NewStore(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: myclient, Description: myclientdesc, @@ -83,7 +83,7 @@ func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { id := myclient - c1 := schema.OpenIDConnectClientConfiguration{ + c1 := schema.OpenIDConnectClient{ ID: id, Description: myclientdesc, Policy: onefactor, @@ -91,10 +91,10 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { Secret: tOpenIDConnectPlainTextClientSecret, } - s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ + s := oidc.NewStore(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{c1}, + Clients: []schema.OpenIDConnectClient{c1}, }, nil) client, err := s.GetFullClient(id) @@ -111,7 +111,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { } func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { - c1 := schema.OpenIDConnectClientConfiguration{ + c1 := schema.OpenIDConnectClient{ ID: myclient, Description: myclientdesc, Policy: onefactor, @@ -119,10 +119,10 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { Secret: tOpenIDConnectPlainTextClientSecret, } - s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ + s := oidc.NewStore(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{c1}, + Clients: []schema.OpenIDConnectClient{c1}, }, nil) client, err := s.GetFullClient("another-client") @@ -131,10 +131,10 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { } func TestOpenIDConnectStore_IsValidClientID(t *testing.T) { - s := oidc.NewStore(&schema.OpenIDConnectConfiguration{ + s := oidc.NewStore(&schema.OpenIDConnect{ IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: keyRSA2048, - Clients: []schema.OpenIDConnectClientConfiguration{ + Clients: []schema.OpenIDConnectClient{ { ID: myclient, Description: myclientdesc, @@ -169,8 +169,8 @@ func (s *StoreSuite) SetupTest() { s.ctx = context.Background() s.ctrl = gomock.NewController(s.T()) s.mock = mocks.NewMockStorage(s.ctrl) - s.store = oidc.NewStore(&schema.OpenIDConnectConfiguration{ - Clients: []schema.OpenIDConnectClientConfiguration{ + s.store = oidc.NewStore(&schema.OpenIDConnect{ + Clients: []schema.OpenIDConnectClient{ { ID: "hs256", Secret: tOpenIDConnectPBKDF2ClientSecret, diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 720ac4fa7..326604698 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -122,8 +122,10 @@ type BaseClient struct { ResponseTypes []string ResponseModes []fosite.ResponseModeType - IDTokenSigningAlg string - UserinfoSigningAlg string + IDTokenSigningAlg string + IDTokenSigningKeyID string + UserinfoSigningAlg string + UserinfoSigningKeyID string Policy authorization.Level @@ -152,8 +154,11 @@ type Client interface { GetSectorIdentifier() string GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody - GetUserinfoSigningAlg() string GetIDTokenSigningAlg() string + GetIDTokenSigningKeyID() string + + GetUserinfoSigningAlg() string + GetUserinfoSigningKeyID() string GetPAREnforcement() bool GetPKCEEnforcement() bool diff --git a/internal/oidc/util.go b/internal/oidc/util.go index 6fc10cab1..de7c7eef8 100644 --- a/internal/oidc/util.go +++ b/internal/oidc/util.go @@ -95,10 +95,3 @@ func isSigningAlgLess(i, j string) bool { } } } - -const ( - SigningAlgPrefixRSA = "RS" - SigningAlgPrefixHMAC = "HS" - SigningAlgPrefixRSAPSS = "PS" - SigningAlgPrefixECDSA = "ES" -) diff --git a/internal/oidc/util_test.go b/internal/oidc/util_test.go new file mode 100644 index 000000000..d96a22d1e --- /dev/null +++ b/internal/oidc/util_test.go @@ -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) + }) + } +} diff --git a/internal/random/cryptographical_test.go b/internal/random/cryptographical_test.go new file mode 100644 index 000000000..afb4ab29d --- /dev/null +++ b/internal/random/cryptographical_test.go @@ -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) +} diff --git a/internal/random/mathematical.go b/internal/random/mathematical.go index 24b8e34d3..a4498e96b 100644 --- a/internal/random/mathematical.go +++ b/internal/random/mathematical.go @@ -112,6 +112,10 @@ func (r *Mathematical) Intn(n int) int { // IntnErr returns a random int error combination with a maximum of n. 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 } @@ -132,15 +136,11 @@ func (r *Mathematical) IntErr(max *big.Int) (value *big.Int, err error) { 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") } - r.lock.Lock() - - defer r.lock.Unlock() - - return big.NewInt(int64(r.Intn(max.Sign()))), nil + return big.NewInt(int64(r.Intn(int(max.Int64())))), nil } // Prime returns a number of the given bit length that is prime with high probability. Prime will return error for any diff --git a/internal/random/mathematical_test.go b/internal/random/mathematical_test.go new file mode 100644 index 000000000..b2f296dfa --- /dev/null +++ b/internal/random/mathematical_test.go @@ -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) +} diff --git a/internal/regulation/regulator.go b/internal/regulation/regulator.go index c57cabdcd..ab1995483 100644 --- a/internal/regulation/regulator.go +++ b/internal/regulation/regulator.go @@ -12,12 +12,12 @@ import ( ) // 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{ - enabled: config.MaxRetries > 0, - storageProvider: provider, - clock: clock, - config: config, + enabled: config.MaxRetries > 0, + store: store, + clock: clock, + 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 { 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(), Successful: successful, Banned: banned, @@ -46,7 +46,7 @@ func (r *Regulator) Regulate(ctx context.Context, username string) (time.Time, e 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 { return time.Time{}, nil } diff --git a/internal/regulation/regulator_test.go b/internal/regulation/regulator_test.go index 6b61be4da..a7156dbd0 100644 --- a/internal/regulation/regulator_test.go +++ b/internal/regulation/regulator_test.go @@ -1,46 +1,69 @@ package regulation_test import ( - "context" + "fmt" + "net" "testing" "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/regulation" - "github.com/authelia/authelia/v4/internal/utils" ) type RegulatorSuite struct { suite.Suite - ctx context.Context - ctrl *gomock.Controller - storageMock *mocks.MockStorage - config schema.RegulationConfiguration - clock utils.TestingClock + mock *mocks.MockAutheliaCtx } func (s *RegulatorSuite) SetupTest() { - s.ctrl = gomock.NewController(s.T()) - s.storageMock = mocks.NewMockStorage(s.ctrl) - s.ctx = context.Background() - - s.config = schema.RegulationConfiguration{ + s.mock = mocks.NewMockAutheliaCtx(s.T()) + s.mock.Ctx.Configuration.Regulation = schema.RegulationConfiguration{ MaxRetries: 3, BanTime: time.Second * 180, 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() { - 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() { @@ -48,17 +71,17 @@ func (s *RegulatorSuite) TestShouldNotThrowWhenUserIsLegitimate() { { Username: "john", Successful: true, - Time: s.clock.Now().Add(-4 * time.Minute), + Time: s.mock.Clock.Now().Add(-4 * time.Minute), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). 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) } @@ -69,27 +92,27 @@ func (s *RegulatorSuite) TestShouldNotThrowWhenFailedAuthenticationNotInFindTime { Username: "john", Successful: false, - Time: s.clock.Now().Add(-1 * time.Second), + Time: s.mock.Clock.Now().Add(-1 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-90 * time.Second), + Time: s.mock.Clock.Now().Add(-90 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-180 * time.Second), + Time: s.mock.Clock.Now().Add(-180 * time.Second), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). 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) } @@ -100,32 +123,32 @@ func (s *RegulatorSuite) TestShouldBanUserIfLatestAttemptsAreWithinFinTime() { { Username: "john", Successful: false, - Time: s.clock.Now().Add(-1 * time.Second), + Time: s.mock.Clock.Now().Add(-1 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-4 * time.Second), + Time: s.mock.Clock.Now().Add(-4 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-6 * time.Second), + Time: s.mock.Clock.Now().Add(-6 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-180 * time.Second), + Time: s.mock.Clock.Now().Add(-180 * time.Second), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). 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) } @@ -138,27 +161,27 @@ func (s *RegulatorSuite) TestShouldCheckUserIsStillBanned() { { Username: "john", Successful: false, - Time: s.clock.Now().Add(-31 * time.Second), + Time: s.mock.Clock.Now().Add(-31 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-34 * time.Second), + Time: s.mock.Clock.Now().Add(-34 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-36 * time.Second), + Time: s.mock.Clock.Now().Add(-36 * time.Second), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). 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) } @@ -167,22 +190,22 @@ func (s *RegulatorSuite) TestShouldCheckUserIsNotYetBanned() { { Username: "john", Successful: false, - Time: s.clock.Now().Add(-34 * time.Second), + Time: s.mock.Clock.Now().Add(-34 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-36 * time.Second), + Time: s.mock.Clock.Now().Add(-36 * time.Second), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). 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) } @@ -191,7 +214,7 @@ func (s *RegulatorSuite) TestShouldCheckUserWasAboutToBeBanned() { { Username: "john", 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. // 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", Successful: false, - Time: s.clock.Now().Add(-94 * time.Second), + Time: s.mock.Clock.Now().Add(-94 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-96 * time.Second), + Time: s.mock.Clock.Now().Add(-96 * time.Second), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). 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) } @@ -223,34 +246,34 @@ func (s *RegulatorSuite) TestShouldCheckRegulationHasBeenResetOnSuccessfulAttemp { Username: "john", Successful: false, - Time: s.clock.Now().Add(-90 * time.Second), + Time: s.mock.Clock.Now().Add(-90 * time.Second), }, { Username: "john", 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 // failure happens within FindTime, he should not be banned. { Username: "john", Successful: false, - Time: s.clock.Now().Add(-94 * time.Second), + Time: s.mock.Clock.Now().Add(-94 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-96 * time.Second), + Time: s.mock.Clock.Now().Add(-96 * time.Second), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). 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) } @@ -265,22 +288,22 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() { { Username: "john", Successful: false, - Time: s.clock.Now().Add(-31 * time.Second), + Time: s.mock.Clock.Now().Add(-31 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-34 * time.Second), + Time: s.mock.Clock.Now().Add(-34 * time.Second), }, { Username: "john", Successful: false, - Time: s.clock.Now().Add(-36 * time.Second), + Time: s.mock.Clock.Now().Add(-36 * time.Second), }, } - s.storageMock.EXPECT(). - LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). + s.mock.StorageMock.EXPECT(). + LoadAuthenticationLogs(s.mock.Ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) // Check Disabled Functionality. @@ -290,8 +313,8 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() { BanTime: time.Second * 180, } - regulator := regulation.NewRegulator(config, s.storageMock, &s.clock) - _, err := regulator.Regulate(s.ctx, "john") + regulator := regulation.NewRegulator(config, s.mock.StorageMock, &s.mock.Clock) + _, err := regulator.Regulate(s.mock.Ctx, "john") assert.NoError(s.T(), err) // Check Enabled Functionality. @@ -301,7 +324,7 @@ func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() { BanTime: time.Second * 180, } - regulator = regulation.NewRegulator(config, s.storageMock, &s.clock) - _, err = regulator.Regulate(s.ctx, "john") + regulator = regulation.NewRegulator(config, s.mock.StorageMock, &s.mock.Clock) + _, err = regulator.Regulate(s.mock.Ctx, "john") assert.Equal(s.T(), regulation.ErrUserIsBanned, err) } diff --git a/internal/regulation/types.go b/internal/regulation/types.go index d5ad21edd..11c56fec0 100644 --- a/internal/regulation/types.go +++ b/internal/regulation/types.go @@ -16,7 +16,7 @@ type Regulator struct { config schema.RegulationConfiguration - storageProvider storage.RegulatorProvider + store storage.RegulatorProvider clock utils.Clock } diff --git a/internal/storage/const.go b/internal/storage/const.go index a65093d77..a8a967411 100644 --- a/internal/storage/const.go +++ b/internal/storage/const.go @@ -29,59 +29,6 @@ const ( 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 ( encryptionNameCheck = "check" ) diff --git a/internal/storage/types.go b/internal/storage/types.go index 7537c7695..5954131e0 100644 --- a/internal/storage/types.go +++ b/internal/storage/types.go @@ -93,3 +93,56 @@ func (r EncryptionValidationTableResult) ResultDescriptor() string { 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 "" + } +} diff --git a/internal/storage/types_test.go b/internal/storage/types_test.go new file mode 100644 index 000000000..e4903ee26 --- /dev/null +++ b/internal/storage/types_test.go @@ -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()) +} diff --git a/internal/utils/aes_test.go b/internal/utils/aes_test.go index 47482c664..b04234540 100644 --- a/internal/utils/aes_test.go +++ b/internal/utils/aes_test.go @@ -8,7 +8,7 @@ import ( ) 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" @@ -22,7 +22,7 @@ func TestShouldEncryptAndDecriptUsingAES(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" @@ -37,7 +37,7 @@ func TestShouldFailDecryptOnInvalidKey(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") diff --git a/internal/utils/io.go b/internal/utils/io.go deleted file mode 100644 index eacaf0386..000000000 --- a/internal/utils/io.go +++ /dev/null @@ -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 -} diff --git a/internal/utils/strings_test.go b/internal/utils/strings_test.go index c12978739..3a9147111 100644 --- a/internal/utils/strings_test.go +++ b/internal/utils/strings_test.go @@ -8,6 +8,97 @@ import ( "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><@#@", + }, + } + + 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) { testCases := []struct { desc, have string diff --git a/internal/utils/time_test.go b/internal/utils/time_test.go index d1e1b6681..c98ef73c8 100644 --- a/internal/utils/time_test.go +++ b/internal/utils/time_test.go @@ -209,11 +209,51 @@ func TestParseTimeString(t *testing.T) { for _, tc := range testCases { 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 == "" { assert.NoError(t, err) - assert.Equal(t, tc.index, index) assert.Equal(t, tc.expected.UnixNano(), actual.UnixNano()) } else { assert.EqualError(t, err, tc.err) diff --git a/internal/utils/url_test.go b/internal/utils/url_test.go index 03b51540f..dc8ab1508 100644 --- a/internal/utils/url_test.go +++ b/internal/utils/url_test.go @@ -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.co", "example.com")) } + +func TestHasDomainSuffix(t *testing.T) { + assert.False(t, HasDomainSuffix("abc", "")) + assert.False(t, HasDomainSuffix("", "")) +} diff --git a/web/package.json b/web/package.json index c1200aa20..895f0fcf6 100644 --- a/web/package.json +++ b/web/package.json @@ -23,8 +23,8 @@ "@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/react-fontawesome": "0.2.0", "@mui/icons-material": "5.11.16", - "@mui/material": "5.13.1", - "@mui/styles": "5.13.1", + "@mui/material": "5.13.2", + "@mui/styles": "5.13.2", "@simplewebauthn/browser": "7.2.0", "@simplewebauthn/typescript-types": "7.0.0", "axios": "1.4.0", @@ -77,17 +77,17 @@ "@limegrass/eslint-plugin-import-alias": "1.0.6", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.0.0", - "@types/node": "20.2.1", - "@types/react": "18.2.6", + "@types/node": "20.2.5", + "@types/react": "18.2.7", "@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", - "@typescript-eslint/eslint-plugin": "5.59.6", - "@typescript-eslint/parser": "5.59.6", + "@typescript-eslint/eslint-plugin": "5.59.7", + "@typescript-eslint/parser": "5.59.7", "@vitejs/plugin-react": "4.0.0", "@vitest/coverage-istanbul": "0.31.1", "esbuild": "0.17.19", - "eslint": "8.40.0", + "eslint": "8.41.0", "eslint-config-prettier": "8.8.0", "eslint-config-react-app": "7.0.1", "eslint-formatter-rdjson": "1.0.5", @@ -97,14 +97,14 @@ "eslint-plugin-prettier": "4.2.1", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", - "happy-dom": "9.19.2", + "happy-dom": "9.20.3", "husky": "8.0.3", "prettier": "2.8.8", "react-test-renderer": "18.2.0", "typescript": "5.0.4", - "vite": "4.3.8", + "vite": "4.3.9", "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-tsconfig-paths": "4.2.0", "vitest": "0.31.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 341fbb1a6..cfedfba9e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -6,10 +6,10 @@ dependencies: version: 11.11.0 '@emotion/react': specifier: 11.11.0 - version: 11.11.0(@types/react@18.2.6)(react@18.2.0) + version: 11.11.0(@types/react@18.2.7)(react@18.2.0) '@emotion/styled': specifier: 11.11.0 - version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.7)(react@18.2.0) '@fortawesome/fontawesome-svg-core': specifier: 6.4.0 version: 6.4.0 @@ -24,13 +24,13 @@ dependencies: version: 0.2.0(@fortawesome/fontawesome-svg-core@6.4.0)(react@18.2.0) '@mui/icons-material': specifier: 5.11.16 - version: 5.11.16(@mui/material@5.13.1)(@types/react@18.2.6)(react@18.2.0) + version: 5.11.16(@mui/material@5.13.2)(@types/react@18.2.7)(react@18.2.0) '@mui/material': - specifier: 5.13.1 - version: 5.13.1(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + specifier: 5.13.2 + version: 5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) '@mui/styles': - specifier: 5.13.1 - version: 5.13.1(@types/react@18.2.6)(react@18.2.0) + specifier: 5.13.2 + version: 5.13.2(@types/react@18.2.7)(react@18.2.0) '@simplewebauthn/browser': specifier: 7.2.0 version: 7.2.0 @@ -89,7 +89,7 @@ devDependencies: version: 17.6.3 '@limegrass/eslint-plugin-import-alias': specifier: 1.0.6 - version: 1.0.6(eslint@8.40.0) + version: 1.0.6(eslint@8.41.0) '@testing-library/jest-dom': specifier: 5.16.5 version: 5.16.5 @@ -97,29 +97,29 @@ devDependencies: specifier: 14.0.0 version: 14.0.0(react-dom@18.2.0)(react@18.2.0) '@types/node': - specifier: 20.2.1 - version: 20.2.1 + specifier: 20.2.5 + version: 20.2.5 '@types/react': - specifier: 18.2.6 - version: 18.2.6 + specifier: 18.2.7 + version: 18.2.7 '@types/react-dom': specifier: 18.2.4 version: 18.2.4 '@types/testing-library__jest-dom': - specifier: 5.14.5 - version: 5.14.5 + specifier: 5.14.6 + version: 5.14.6 '@types/zxcvbn': specifier: 4.4.1 version: 4.4.1 '@typescript-eslint/eslint-plugin': - specifier: 5.59.6 - version: 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.7 + version: 5.59.7(@typescript-eslint/parser@5.59.7)(eslint@8.41.0)(typescript@5.0.4) '@typescript-eslint/parser': - specifier: 5.59.6 - version: 5.59.6(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.7 + version: 5.59.7(eslint@8.41.0)(typescript@5.0.4) '@vitejs/plugin-react': specifier: 4.0.0 - version: 4.0.0(vite@4.3.8) + version: 4.0.0(vite@4.3.9) '@vitest/coverage-istanbul': specifier: 0.31.1 version: 0.31.1(vitest@0.31.1) @@ -127,38 +127,38 @@ devDependencies: specifier: 0.17.19 version: 0.17.19 eslint: - specifier: 8.40.0 - version: 8.40.0 + specifier: 8.41.0 + version: 8.41.0 eslint-config-prettier: specifier: 8.8.0 - version: 8.8.0(eslint@8.40.0) + version: 8.8.0(eslint@8.41.0) eslint-config-react-app: specifier: 7.0.1 - version: 7.0.1(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0)(typescript@5.0.4) + version: 7.0.1(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0)(typescript@5.0.4) eslint-formatter-rdjson: specifier: 1.0.5 version: 1.0.5 eslint-import-resolver-typescript: specifier: 3.5.5 - version: 3.5.5(@typescript-eslint/parser@5.59.6)(eslint-plugin-import@2.27.5)(eslint@8.40.0) + version: 3.5.5(@typescript-eslint/parser@5.59.7)(eslint-plugin-import@2.27.5)(eslint@8.41.0) eslint-plugin-import: specifier: 2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) + version: 2.27.5(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0) eslint-plugin-jsx-a11y: specifier: 6.7.1 - version: 6.7.1(eslint@8.40.0) + version: 6.7.1(eslint@8.41.0) eslint-plugin-prettier: specifier: 4.2.1 - version: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.40.0)(prettier@2.8.8) + version: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.41.0)(prettier@2.8.8) eslint-plugin-react: specifier: 7.32.2 - version: 7.32.2(eslint@8.40.0) + version: 7.32.2(eslint@8.41.0) eslint-plugin-react-hooks: specifier: 4.6.0 - version: 4.6.0(eslint@8.40.0) + version: 4.6.0(eslint@8.41.0) happy-dom: - specifier: 9.19.2 - version: 9.19.2 + specifier: 9.20.3 + version: 9.20.3 husky: specifier: 8.0.3 version: 8.0.3 @@ -172,23 +172,23 @@ devDependencies: specifier: 5.0.4 version: 5.0.4 vite: - specifier: 4.3.8 - version: 4.3.8(@types/node@20.2.1) + specifier: 4.3.9 + version: 4.3.9(@types/node@20.2.5) vite-plugin-eslint: specifier: 1.8.1 - version: 1.8.1(eslint@8.40.0)(vite@4.3.8) + version: 1.8.1(eslint@8.41.0)(vite@4.3.9) vite-plugin-istanbul: - specifier: 4.0.1 - version: 4.0.1(vite@4.3.8) + specifier: 4.1.0 + version: 4.1.0(vite@4.3.9) vite-plugin-svgr: specifier: 3.2.0 - version: 3.2.0(vite@4.3.8) + version: 3.2.0(vite@4.3.9) vite-tsconfig-paths: specifier: 4.2.0 - version: 4.2.0(typescript@5.0.4)(vite@4.3.8) + version: 4.2.0(typescript@5.0.4)(vite@4.3.9) vitest: specifier: 0.31.1 - version: 0.31.1(happy-dom@9.19.2) + version: 0.31.1(happy-dom@9.20.3) vitest-preview: specifier: 0.0.1 version: 0.0.1 @@ -241,7 +241,7 @@ packages: - supports-color dev: true - /@babel/eslint-parser@7.21.3(@babel/core@7.21.4)(eslint@8.40.0): + /@babel/eslint-parser@7.21.3(@babel/core@7.21.4)(eslint@8.41.0): resolution: {integrity: sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -250,7 +250,7 @@ packages: dependencies: '@babel/core': 7.21.4 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.40.0 + eslint: 8.41.0 eslint-visitor-keys: 2.1.0 semver: 6.3.0 dev: true @@ -1639,15 +1639,15 @@ packages: '@commitlint/execute-rule': 17.4.0 '@commitlint/resolve-extends': 17.4.4 '@commitlint/types': 17.4.4 - '@types/node': 20.2.1 + '@types/node': 20.2.5 chalk: 4.1.2 cosmiconfig: 8.1.3 - cosmiconfig-typescript-loader: 4.3.0(@types/node@20.2.1)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4) + cosmiconfig-typescript-loader: 4.3.0(@types/node@20.2.5)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@20.2.1)(typescript@5.0.4) + ts-node: 10.9.1(@types/node@20.2.5)(typescript@5.0.4) typescript: 5.0.4 transitivePeerDependencies: - '@swc/core' @@ -1768,7 +1768,7 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.11.0(@types/react@18.2.6)(react@18.2.0): + /@emotion/react@11.11.0(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-ZSK3ZJsNkwfjT3JpDAWJZlrGD81Z3ytNDsxw1LKq1o+xkmO5pnWfr6gmCC8gHEFf3nSSX/09YrG67jybNPxSUw==} peerDependencies: '@types/react': '*' @@ -1784,7 +1784,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.6 + '@types/react': 18.2.7 hoist-non-react-statics: 3.3.2 react: 18.2.0 dev: false @@ -1803,7 +1803,7 @@ packages: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0): + /@emotion/styled@11.11.0(@emotion/react@11.11.0)(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 @@ -1816,11 +1816,11 @@ packages: '@babel/runtime': 7.21.0 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.1 - '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/react': 11.11.0(@types/react@18.2.7)(react@18.2.0) '@emotion/serialize': 1.1.2 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 - '@types/react': 18.2.6 + '@types/react': 18.2.7 react: 18.2.0 dev: false @@ -2060,13 +2060,13 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.40.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.41.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.40.0 + eslint: 8.41.0 eslint-visitor-keys: 3.4.1 dev: true @@ -2092,8 +2092,8 @@ packages: - supports-color dev: true - /@eslint/js@8.40.0: - resolution: {integrity: sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==} + /@eslint/js@8.41.0: + resolution: {integrity: sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -2195,7 +2195,7 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.2.1 + '@types/node': 20.2.5 '@types/yargs': 17.0.24 chalk: 4.1.2 dev: true @@ -2246,12 +2246,12 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@limegrass/eslint-plugin-import-alias@1.0.6(eslint@8.40.0): + /@limegrass/eslint-plugin-import-alias@1.0.6(eslint@8.41.0): resolution: {integrity: sha512-BtPmdHbL4NmkVh2wMnOboyOCrdLOpBqwwtBIsB0/giTiALw/UTHD9TyH4vVnbDOuWPZQgE6kKloJ9G77FApt7w==} peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 dependencies: - eslint: 8.40.0 + eslint: 8.41.0 find-up: 5.0.0 fs-extra: 10.1.0 micromatch: 4.0.5 @@ -2259,8 +2259,8 @@ packages: tsconfig-paths: 3.14.2 dev: true - /@mui/base@5.0.0-beta.1(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-xrkDCeu3JQE+JjJUnJnOrdQJMXwKhbV4AW+FRjMIj5i9cHK3BAuatG/iqbf1M+jklVWLk0KdbgioKwK+03aYbA==} + /@mui/base@5.0.0-beta.2(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-R9R+aqrl1QhZJaO05rhvooqxOaf7SKpQ+EjW80sbP3ticTVmLmrn4YBLQS7/ML+WXdrkrPtqSmKFdSE5Ik3gBQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || 18 @@ -2272,10 +2272,10 @@ packages: dependencies: '@babel/runtime': 7.21.0 '@emotion/is-prop-valid': 1.2.1 - '@mui/types': 7.2.4(@types/react@18.2.6) + '@mui/types': 7.2.4(@types/react@18.2.7) '@mui/utils': 5.13.1(react@18.2.0) '@popperjs/core': 2.11.7 - '@types/react': 18.2.6 + '@types/react': 18.2.7 clsx: 1.2.1 prop-types: 15.8.1 react: 18.2.0 @@ -2283,11 +2283,11 @@ packages: react-is: 18.2.0 dev: false - /@mui/core-downloads-tracker@5.13.1: - resolution: {integrity: sha512-qDHtNDO72NcBQMhaWBt9EZMvNiO+OXjPg5Sdk/6LgRDw6Zr3HdEZ5n2FJ/qtYsaT/okGyCuQavQkcZCOCEVf/g==} + /@mui/core-downloads-tracker@5.13.2: + resolution: {integrity: sha512-aOLCXMCySMFL2WmUhnz+DjF84AoFVu8rn35OsL759HXOZMz8zhEwVf5w/xxkWx7DycM2KXDTgAvYW48nTfqTLA==} dev: false - /@mui/icons-material@5.11.16(@mui/material@5.13.1)(@types/react@18.2.6)(react@18.2.0): + /@mui/icons-material@5.11.16(@mui/material@5.13.2)(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-oKkx9z9Kwg40NtcIajF9uOXhxiyTZrrm9nmIJ4UjkU2IdHpd4QVLbCc/5hZN/y0C6qzi2Zlxyr9TGddQx2vx2A==} engines: {node: '>=12.0.0'} peerDependencies: @@ -2299,13 +2299,13 @@ packages: optional: true dependencies: '@babel/runtime': 7.21.0 - '@mui/material': 5.13.1(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.6 + '@mui/material': 5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.7 react: 18.2.0 dev: false - /@mui/material@5.13.1(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-qSnbJZer8lIuDYFDv19/t3s0AXYY9SxcOdhCnGvetRSfOG4gy3TkiFXNCdW5OLNveTieiMpOuv46eXUmE3ZA6A==} + /@mui/material@5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Pfke1l0GG2OJb/Nr10aVr8huoBFcBTdWKV5iFSTEHqf9c2C1ZlyYMISn7ui6X3Gix8vr+hP5kVqH1LAWwQSb6w==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -2322,14 +2322,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.21.0 - '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) - '@mui/base': 5.0.0-beta.1(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 5.13.1 - '@mui/system': 5.13.1(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) - '@mui/types': 7.2.4(@types/react@18.2.6) + '@emotion/react': 11.11.0(@types/react@18.2.7)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.7)(react@18.2.0) + '@mui/base': 5.0.0-beta.2(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.13.2 + '@mui/system': 5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.7)(react@18.2.0) + '@mui/types': 7.2.4(@types/react@18.2.7) '@mui/utils': 5.13.1(react@18.2.0) - '@types/react': 18.2.6 + '@types/react': 18.2.7 '@types/react-transition-group': 4.4.6 clsx: 1.2.1 csstype: 3.1.2 @@ -2340,7 +2340,7 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false - /@mui/private-theming@5.13.1(@types/react@18.2.6)(react@18.2.0): + /@mui/private-theming@5.13.1(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -2352,13 +2352,13 @@ packages: dependencies: '@babel/runtime': 7.21.0 '@mui/utils': 5.13.1(react@18.2.0) - '@types/react': 18.2.6 + '@types/react': 18.2.7 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@5.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-AhZtiRyT8Bjr7fufxE/mLS+QJ3LxwX1kghIcM2B2dvJzSSg9rnIuXDXM959QfUVIM3C8U4x3mgVoPFMQJvc4/g==} + /@mui/styled-engine@5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -2372,15 +2372,15 @@ packages: dependencies: '@babel/runtime': 7.21.0 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@emotion/react': 11.11.0(@types/react@18.2.7)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.7)(react@18.2.0) csstype: 3.1.2 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styles@5.13.1(@types/react@18.2.6)(react@18.2.0): - resolution: {integrity: sha512-Y4Mw5O0OQCv+y+8RexSJLaHI9h9ISf5gl3oHDMKI9m3p3Af3d0I9E406psAAfeJnEUgTvELFQ5qQL0E6i9LzRw==} + /@mui/styles@5.13.2(@types/react@18.2.7)(react@18.2.0): + resolution: {integrity: sha512-gKNkVyk6azQ8wfCamh3yU/wLv1JscYrsQX9huQBwfwtE7kUTq2rgggdfJjRADjbcmT6IPX+oCHYjGfqqFgDIQQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || 18 @@ -2391,10 +2391,10 @@ packages: dependencies: '@babel/runtime': 7.21.0 '@emotion/hash': 0.9.1 - '@mui/private-theming': 5.13.1(@types/react@18.2.6)(react@18.2.0) - '@mui/types': 7.2.4(@types/react@18.2.6) + '@mui/private-theming': 5.13.1(@types/react@18.2.7)(react@18.2.0) + '@mui/types': 7.2.4(@types/react@18.2.7) '@mui/utils': 5.13.1(react@18.2.0) - '@types/react': 18.2.6 + '@types/react': 18.2.7 clsx: 1.2.1 csstype: 3.1.2 hoist-non-react-statics: 3.3.2 @@ -2410,8 +2410,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.13.1(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0): - resolution: {integrity: sha512-BsDUjhiO6ZVAvzKhnWBHLZ5AtPJcdT+62VjnRLyA4isboqDKLg4fmYIZXq51yndg/soDK9RkY5lYZwEDku13Ow==} + /@mui/system@5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.7)(react@18.2.0): + resolution: {integrity: sha512-TPyWmRJPt0JPVxacZISI4o070xEJ7ftxpVtu6LWuYVOUOINlhoGOclam4iV8PDT3EMQEHuUrwU49po34UdWLlw==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -2427,20 +2427,20 @@ packages: optional: true dependencies: '@babel/runtime': 7.21.0 - '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) - '@mui/private-theming': 5.13.1(@types/react@18.2.6)(react@18.2.0) - '@mui/styled-engine': 5.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0) - '@mui/types': 7.2.4(@types/react@18.2.6) + '@emotion/react': 11.11.0(@types/react@18.2.7)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.7)(react@18.2.0) + '@mui/private-theming': 5.13.1(@types/react@18.2.7)(react@18.2.0) + '@mui/styled-engine': 5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.4(@types/react@18.2.7) '@mui/utils': 5.13.1(react@18.2.0) - '@types/react': 18.2.6 + '@types/react': 18.2.7 clsx: 1.2.1 csstype: 3.1.2 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/types@7.2.4(@types/react@18.2.6): + /@mui/types@7.2.4(@types/react@18.2.7): resolution: {integrity: sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==} peerDependencies: '@types/react': '*' @@ -2448,7 +2448,7 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.6 + '@types/react': 18.2.7 dev: false /@mui/utils@5.13.1(react@18.2.0): @@ -2694,7 +2694,7 @@ packages: dependencies: '@adobe/css-tools': 4.2.0 '@babel/runtime': 7.21.0 - '@types/testing-library__jest-dom': 5.14.5 + '@types/testing-library__jest-dom': 5.14.6 aria-query: 5.1.3 chalk: 3.0.0 css.escape: 1.5.1 @@ -2741,7 +2741,7 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.2.1 + '@types/node': 20.2.5 dev: true /@types/chai-subset@1.3.3: @@ -2757,7 +2757,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.2.1 + '@types/node': 20.2.5 dev: true /@types/eslint@8.37.0: @@ -2774,7 +2774,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.2.1 + '@types/node': 20.2.5 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -2831,8 +2831,8 @@ packages: resolution: {integrity: sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==} dev: true - /@types/node@20.2.1: - resolution: {integrity: sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==} + /@types/node@20.2.5: + resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} dev: true /@types/normalize-package-data@2.4.1: @@ -2856,23 +2856,23 @@ packages: /@types/react-dom@18.2.4: resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==} dependencies: - '@types/react': 18.2.6 + '@types/react': 18.2.7 dev: true /@types/react-is@18.2.0: resolution: {integrity: sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ==} dependencies: - '@types/react': 18.2.6 + '@types/react': 18.2.7 dev: false /@types/react-transition-group@4.4.6: resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} dependencies: - '@types/react': 18.2.6 + '@types/react': 18.2.7 dev: false - /@types/react@18.2.6: - resolution: {integrity: sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==} + /@types/react@18.2.7: + resolution: {integrity: sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.3 @@ -2889,15 +2889,15 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.2.1 + '@types/node': 20.2.5 dev: true /@types/stack-utils@2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true - /@types/testing-library__jest-dom@5.14.5: - resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==} + /@types/testing-library__jest-dom@5.14.6: + resolution: {integrity: sha512-FkHXCb+ikSoUP4Y4rOslzTdX5sqYwMxfefKh1GmZ8ce1GOkEHntSp6b5cGadmNfp5e4BMEWOMx+WSKd5/MqlDA==} dependencies: '@types/jest': 29.5.0 dev: true @@ -2916,8 +2916,8 @@ packages: resolution: {integrity: sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==} dev: true - /@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.40.0)(typescript@5.0.4): - resolution: {integrity: sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==} + /@typescript-eslint/eslint-plugin@5.59.7(@typescript-eslint/parser@5.59.7)(eslint@8.41.0)(typescript@5.0.4): + resolution: {integrity: sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -2928,12 +2928,12 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.0 - '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/type-utils': 5.59.6(eslint@8.40.0)(typescript@5.0.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.40.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.7(eslint@8.41.0)(typescript@5.0.4) + '@typescript-eslint/scope-manager': 5.59.7 + '@typescript-eslint/type-utils': 5.59.7(eslint@8.41.0)(typescript@5.0.4) + '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.0.4) debug: 4.3.4 - eslint: 8.40.0 + eslint: 8.41.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 @@ -2944,21 +2944,21 @@ packages: - supports-color dev: true - /@typescript-eslint/experimental-utils@5.58.0(eslint@8.40.0)(typescript@5.0.4): + /@typescript-eslint/experimental-utils@5.58.0(eslint@8.41.0)(typescript@5.0.4): resolution: {integrity: sha512-LA/sRPaynZlrlYxdefrZbMx8dqs/1Kc0yNG+XOk5CwwZx7tTv263ix3AJNioF0YBVt7hADpAUR20owl6pv4MIQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.58.0(eslint@8.40.0)(typescript@5.0.4) - eslint: 8.40.0 + '@typescript-eslint/utils': 5.58.0(eslint@8.41.0)(typescript@5.0.4) + eslint: 8.41.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/parser@5.59.6(eslint@8.40.0)(typescript@5.0.4): - resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} + /@typescript-eslint/parser@5.59.7(eslint@8.41.0)(typescript@5.0.4): + resolution: {integrity: sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2967,11 +2967,11 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.0.4) + '@typescript-eslint/scope-manager': 5.59.7 + '@typescript-eslint/types': 5.59.7 + '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.0.4) debug: 4.3.4 - eslint: 8.40.0 + eslint: 8.41.0 typescript: 5.0.4 transitivePeerDependencies: - supports-color @@ -2985,16 +2985,16 @@ packages: '@typescript-eslint/visitor-keys': 5.58.0 dev: true - /@typescript-eslint/scope-manager@5.59.6: - resolution: {integrity: sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==} + /@typescript-eslint/scope-manager@5.59.7: + resolution: {integrity: sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/visitor-keys': 5.59.6 + '@typescript-eslint/types': 5.59.7 + '@typescript-eslint/visitor-keys': 5.59.7 dev: true - /@typescript-eslint/type-utils@5.59.6(eslint@8.40.0)(typescript@5.0.4): - resolution: {integrity: sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==} + /@typescript-eslint/type-utils@5.59.7(eslint@8.41.0)(typescript@5.0.4): + resolution: {integrity: sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -3003,10 +3003,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.0.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.40.0)(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.0.4) + '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.0.4) debug: 4.3.4 - eslint: 8.40.0 + eslint: 8.41.0 tsutils: 3.21.0(typescript@5.0.4) typescript: 5.0.4 transitivePeerDependencies: @@ -3018,8 +3018,8 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types@5.59.6: - resolution: {integrity: sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==} + /@typescript-eslint/types@5.59.7: + resolution: {integrity: sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -3044,8 +3044,8 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@5.59.6(typescript@5.0.4): - resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} + /@typescript-eslint/typescript-estree@5.59.7(typescript@5.0.4): + resolution: {integrity: sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -3053,8 +3053,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/visitor-keys': 5.59.6 + '@typescript-eslint/types': 5.59.7 + '@typescript-eslint/visitor-keys': 5.59.7 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -3065,19 +3065,19 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@5.58.0(eslint@8.40.0)(typescript@5.0.4): + /@typescript-eslint/utils@5.58.0(eslint@8.41.0)(typescript@5.0.4): resolution: {integrity: sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.41.0) '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 '@typescript-eslint/scope-manager': 5.58.0 '@typescript-eslint/types': 5.58.0 '@typescript-eslint/typescript-estree': 5.58.0(typescript@5.0.4) - eslint: 8.40.0 + eslint: 8.41.0 eslint-scope: 5.1.1 semver: 7.5.0 transitivePeerDependencies: @@ -3085,19 +3085,19 @@ packages: - typescript dev: true - /@typescript-eslint/utils@5.59.6(eslint@8.40.0)(typescript@5.0.4): - resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} + /@typescript-eslint/utils@5.59.7(eslint@8.41.0)(typescript@5.0.4): + resolution: {integrity: sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.41.0) '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.0.4) - eslint: 8.40.0 + '@typescript-eslint/scope-manager': 5.59.7 + '@typescript-eslint/types': 5.59.7 + '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.0.4) + eslint: 8.41.0 eslint-scope: 5.1.1 semver: 7.5.0 transitivePeerDependencies: @@ -3113,15 +3113,15 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@typescript-eslint/visitor-keys@5.59.6: - resolution: {integrity: sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==} + /@typescript-eslint/visitor-keys@5.59.7: + resolution: {integrity: sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.59.6 + '@typescript-eslint/types': 5.59.7 eslint-visitor-keys: 3.4.1 dev: true - /@vitejs/plugin-react@4.0.0(vite@4.3.8): + /@vitejs/plugin-react@4.0.0(vite@4.3.9): resolution: {integrity: sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -3131,7 +3131,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.4) '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.4) react-refresh: 0.14.0 - vite: 4.3.8(@types/node@20.2.1) + vite: 4.3.9(@types/node@20.2.5) transitivePeerDependencies: - supports-color dev: true @@ -3153,7 +3153,7 @@ packages: istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 test-exclude: 6.0.0 - vitest: 0.31.1(happy-dom@9.19.2) + vitest: 0.31.1(happy-dom@9.20.3) transitivePeerDependencies: - supports-color dev: true @@ -3748,7 +3748,7 @@ packages: browserslist: 4.21.5 dev: true - /cosmiconfig-typescript-loader@4.3.0(@types/node@20.2.1)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4): + /cosmiconfig-typescript-loader@4.3.0(@types/node@20.2.5)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@5.0.4): resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -3757,9 +3757,9 @@ packages: ts-node: '>=10' typescript: '>=3' dependencies: - '@types/node': 20.2.1 + '@types/node': 20.2.5 cosmiconfig: 8.1.3 - ts-node: 10.9.1(@types/node@20.2.1)(typescript@5.0.4) + ts-node: 10.9.1(@types/node@20.2.5)(typescript@5.0.4) typescript: 5.0.4 dev: true @@ -4368,15 +4368,15 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - /eslint-config-prettier@8.8.0(eslint@8.40.0): + /eslint-config-prettier@8.8.0(eslint@8.41.0): resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.40.0 + eslint: 8.41.0 dev: true - /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0)(typescript@5.0.4): + /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0)(typescript@5.0.4): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4387,20 +4387,20 @@ packages: optional: true dependencies: '@babel/core': 7.21.4 - '@babel/eslint-parser': 7.21.3(@babel/core@7.21.4)(eslint@8.40.0) + '@babel/eslint-parser': 7.21.3(@babel/core@7.21.4)(eslint@8.41.0) '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.40.0)(typescript@5.0.4) - '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) + '@typescript-eslint/eslint-plugin': 5.59.7(@typescript-eslint/parser@5.59.7)(eslint@8.41.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.7(eslint@8.41.0)(typescript@5.0.4) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 - eslint: 8.40.0 - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.40.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.59.6)(eslint@8.40.0)(typescript@5.0.4) - eslint-plugin-jsx-a11y: 6.7.1(eslint@8.40.0) - eslint-plugin-react: 7.32.2(eslint@8.40.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.40.0) - eslint-plugin-testing-library: 5.10.2(eslint@8.40.0)(typescript@5.0.4) + eslint: 8.41.0 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.41.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.59.7)(eslint@8.41.0)(typescript@5.0.4) + eslint-plugin-jsx-a11y: 6.7.1(eslint@8.41.0) + eslint-plugin-react: 7.32.2(eslint@8.41.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.41.0) + eslint-plugin-testing-library: 5.10.2(eslint@8.41.0)(typescript@5.0.4) typescript: 5.0.4 transitivePeerDependencies: - '@babel/plugin-syntax-flow' @@ -4425,7 +4425,7 @@ packages: - supports-color dev: true - /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.6)(eslint-plugin-import@2.27.5)(eslint@8.40.0): + /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.7)(eslint-plugin-import@2.27.5)(eslint@8.41.0): resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -4434,9 +4434,9 @@ packages: dependencies: debug: 4.3.4 enhanced-resolve: 5.12.0 - eslint: 8.40.0 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) + eslint: 8.41.0 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0) get-tsconfig: 4.5.0 globby: 13.1.3 is-core-module: 2.12.0 @@ -4449,7 +4449,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0): + /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0): resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -4470,16 +4470,16 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.7(eslint@8.41.0)(typescript@5.0.4) debug: 3.2.7 - eslint: 8.40.0 + eslint: 8.41.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6)(eslint-plugin-import@2.27.5)(eslint@8.40.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.7)(eslint-plugin-import@2.27.5)(eslint@8.41.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.40.0): + /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.41.0): resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -4489,12 +4489,12 @@ packages: dependencies: '@babel/plugin-syntax-flow': 7.21.4(@babel/core@7.21.4) '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.21.4) - eslint: 8.40.0 + eslint: 8.41.0 lodash: 4.17.21 string-natural-compare: 3.0.1 dev: true - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0): + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: @@ -4504,15 +4504,15 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.7(eslint@8.41.0)(typescript@5.0.4) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.40.0 + eslint: 8.41.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0) has: 1.0.3 is-core-module: 2.12.0 is-glob: 4.0.3 @@ -4527,7 +4527,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.59.6)(eslint@8.40.0)(typescript@5.0.4): + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.59.7)(eslint@8.41.0)(typescript@5.0.4): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -4540,15 +4540,15 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.40.0)(typescript@5.0.4) - '@typescript-eslint/experimental-utils': 5.58.0(eslint@8.40.0)(typescript@5.0.4) - eslint: 8.40.0 + '@typescript-eslint/eslint-plugin': 5.59.7(@typescript-eslint/parser@5.59.7)(eslint@8.41.0)(typescript@5.0.4) + '@typescript-eslint/experimental-utils': 5.58.0(eslint@8.41.0)(typescript@5.0.4) + eslint: 8.41.0 transitivePeerDependencies: - supports-color - typescript dev: true - /eslint-plugin-jsx-a11y@6.7.1(eslint@8.40.0): + /eslint-plugin-jsx-a11y@6.7.1(eslint@8.41.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} peerDependencies: @@ -4563,7 +4563,7 @@ packages: axobject-query: 3.1.1 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.40.0 + eslint: 8.41.0 has: 1.0.3 jsx-ast-utils: 3.3.3 language-tags: 1.0.5 @@ -4573,7 +4573,7 @@ packages: semver: 6.3.0 dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0)(eslint@8.40.0)(prettier@2.8.8): + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0)(eslint@8.41.0)(prettier@2.8.8): resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -4584,22 +4584,22 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.40.0 - eslint-config-prettier: 8.8.0(eslint@8.40.0) + eslint: 8.41.0 + eslint-config-prettier: 8.8.0(eslint@8.41.0) prettier: 2.8.8 prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.40.0): + /eslint-plugin-react-hooks@4.6.0(eslint@8.41.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.40.0 + eslint: 8.41.0 dev: true - /eslint-plugin-react@7.32.2(eslint@8.40.0): + /eslint-plugin-react@7.32.2(eslint@8.41.0): resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} engines: {node: '>=4'} peerDependencies: @@ -4609,7 +4609,7 @@ packages: array.prototype.flatmap: 1.3.1 array.prototype.tosorted: 1.1.1 doctrine: 2.1.0 - eslint: 8.40.0 + eslint: 8.41.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.3 minimatch: 3.1.2 @@ -4623,14 +4623,14 @@ packages: string.prototype.matchall: 4.0.8 dev: true - /eslint-plugin-testing-library@5.10.2(eslint@8.40.0)(typescript@5.0.4): + /eslint-plugin-testing-library@5.10.2(eslint@8.41.0)(typescript@5.0.4): resolution: {integrity: sha512-f1DmDWcz5SDM+IpCkEX0lbFqrrTs8HRsEElzDEqN/EBI0hpRj8Cns5+IVANXswE8/LeybIJqPAOQIFu2j5Y5sw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.40.0)(typescript@5.0.4) - eslint: 8.40.0 + '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.0.4) + eslint: 8.41.0 transitivePeerDependencies: - supports-color - typescript @@ -4662,14 +4662,14 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.40.0: - resolution: {integrity: sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==} + /eslint@8.41.0: + resolution: {integrity: sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.41.0) '@eslint-community/regexpp': 4.5.0 '@eslint/eslintrc': 2.0.3 - '@eslint/js': 8.40.0 + '@eslint/js': 8.41.0 '@humanwhocodes/config-array': 0.11.8 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -4689,13 +4689,12 @@ packages: find-up: 5.0.0 glob-parent: 6.0.2 globals: 13.20.0 - grapheme-splitter: 1.0.4 + graphemer: 1.4.0 ignore: 5.2.4 import-fresh: 3.3.0 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-sdsl: 4.4.0 js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 @@ -5156,8 +5155,12 @@ packages: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true - /happy-dom@9.19.2: - resolution: {integrity: sha512-WBey64FErn5niCLddcjXxkgDk6burN/5doiJpMUQXpgEG3TUJdbygJV1bzcj1Ey+pz+0QGCZH1pwe24uPW+WnQ==} + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /happy-dom@9.20.3: + resolution: {integrity: sha512-eBsgauT435fXFvQDNcmm5QbGtYzxEzOaX35Ia+h6yP/wwa4xSWZh1CfP+mGby8Hk6Xu59mTkpyf72rUXHNxY7A==} dependencies: css.escape: 1.5.1 entities: 4.5.0 @@ -5639,17 +5642,13 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 20.2.1 + '@types/node': 20.2.5 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 picomatch: 2.3.1 dev: true - /js-sdsl@4.4.0: - resolution: {integrity: sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==} - dev: true - /js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} @@ -7161,7 +7160,7 @@ packages: engines: {node: '>=8'} dev: true - /ts-node@10.9.1(@types/node@20.2.1)(typescript@5.0.4): + /ts-node@10.9.1(@types/node@20.2.5)(typescript@5.0.4): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} peerDependencies: '@swc/core': '>=1.2.50' @@ -7179,7 +7178,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 20.2.1 + '@types/node': 20.2.5 acorn: 8.8.2 acorn-walk: 8.2.0 arg: 4.1.3 @@ -7374,7 +7373,7 @@ packages: engines: {node: '>= 0.8'} dev: true - /vite-node@0.31.1(@types/node@20.2.1): + /vite-node@0.31.1(@types/node@20.2.5): resolution: {integrity: sha512-BajE/IsNQ6JyizPzu9zRgHrBwczkAs0erQf/JRpgTIESpKvNj9/Gd0vxX905klLkb0I0SJVCKbdrl5c6FnqYKA==} engines: {node: '>=v14.18.0'} dependencies: @@ -7383,7 +7382,7 @@ packages: mlly: 1.2.0 pathe: 1.1.0 picocolors: 1.0.0 - vite: 4.3.8(@types/node@20.2.1) + vite: 4.3.9(@types/node@20.2.5) transitivePeerDependencies: - '@types/node' - less @@ -7394,7 +7393,7 @@ packages: - terser dev: true - /vite-plugin-eslint@1.8.1(eslint@8.40.0)(vite@4.3.8): + /vite-plugin-eslint@1.8.1(eslint@8.41.0)(vite@4.3.9): resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: eslint: '>=7' @@ -7402,13 +7401,13 @@ packages: dependencies: '@rollup/pluginutils': 4.2.1 '@types/eslint': 8.37.0 - eslint: 8.40.0 + eslint: 8.41.0 rollup: 2.79.1 - vite: 4.3.8(@types/node@20.2.1) + vite: 4.3.9(@types/node@20.2.5) dev: true - /vite-plugin-istanbul@4.0.1(vite@4.3.8): - resolution: {integrity: sha512-1fUCJyYvt/vkDQWR/15knwCk+nWmNbVbmZTXf/X4XD0dcdmJsYrZF5JQo7ttYxFyflGH2SVu+XRlpN06CakKPQ==} + /vite-plugin-istanbul@4.1.0(vite@4.3.9): + resolution: {integrity: sha512-d8FRxaswOUYlGqCCNv2BTbt9pyqt7J4RPgab3WmMf+T2TflLlCmC7S26zDRfL9Ve4JSHrcf5bdzt+E0n9CrPvA==} peerDependencies: vite: '>=2.9.1 <= 5' dependencies: @@ -7416,12 +7415,12 @@ packages: istanbul-lib-instrument: 5.2.1 picocolors: 1.0.0 test-exclude: 6.0.0 - vite: 4.3.8(@types/node@20.2.1) + vite: 4.3.9(@types/node@20.2.5) transitivePeerDependencies: - supports-color dev: true - /vite-plugin-svgr@3.2.0(vite@4.3.8): + /vite-plugin-svgr@3.2.0(vite@4.3.9): resolution: {integrity: sha512-Uvq6niTvhqJU6ga78qLKBFJSDvxWhOnyfQSoKpDPMAGxJPo5S3+9hyjExE5YDj6Lpa4uaLkGc1cBgxXov+LjSw==} peerDependencies: vite: ^2.6.0 || 3 || 4 @@ -7429,13 +7428,13 @@ packages: '@rollup/pluginutils': 5.0.2 '@svgr/core': 7.0.0 '@svgr/plugin-jsx': 7.0.0 - vite: 4.3.8(@types/node@20.2.1) + vite: 4.3.9(@types/node@20.2.5) transitivePeerDependencies: - rollup - supports-color dev: true - /vite-tsconfig-paths@4.2.0(typescript@5.0.4)(vite@4.3.8): + /vite-tsconfig-paths@4.2.0(typescript@5.0.4)(vite@4.3.9): resolution: {integrity: sha512-jGpus0eUy5qbbMVGiTxCL1iB9ZGN6Bd37VGLJU39kTDD6ZfULTTb1bcc5IeTWqWJKiWV5YihCaibeASPiGi8kw==} peerDependencies: vite: '*' @@ -7446,7 +7445,7 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 2.1.1(typescript@5.0.4) - vite: 4.3.8(@types/node@20.2.1) + vite: 4.3.9(@types/node@20.2.5) transitivePeerDependencies: - supports-color - typescript @@ -7485,8 +7484,8 @@ packages: fsevents: 2.3.2 dev: true - /vite@4.3.8(@types/node@20.2.1): - resolution: {integrity: sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==} + /vite@4.3.9(@types/node@20.2.5): + resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/node': '>= 14' @@ -7509,7 +7508,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.2.1 + '@types/node': 20.2.5 esbuild: 0.17.19 postcss: 8.4.23 rollup: 3.21.0 @@ -7534,7 +7533,7 @@ packages: - terser dev: true - /vitest@0.31.1(happy-dom@9.19.2): + /vitest@0.31.1(happy-dom@9.20.3): resolution: {integrity: sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==} engines: {node: '>=v14.18.0'} peerDependencies: @@ -7566,7 +7565,7 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.2.1 + '@types/node': 20.2.5 '@vitest/expect': 0.31.1 '@vitest/runner': 0.31.1 '@vitest/snapshot': 0.31.1 @@ -7578,7 +7577,7 @@ packages: chai: 4.3.7 concordance: 5.0.4 debug: 4.3.4 - happy-dom: 9.19.2 + happy-dom: 9.20.3 local-pkg: 0.4.3 magic-string: 0.30.0 pathe: 1.1.0 @@ -7587,8 +7586,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.5.0 - vite: 4.3.8(@types/node@20.2.1) - vite-node: 0.31.1(@types/node@20.2.1) + vite: 4.3.9(@types/node@20.2.5) + vite-node: 0.31.1(@types/node@20.2.5) why-is-node-running: 2.2.2 transitivePeerDependencies: - less