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

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

View File

@ -2,7 +2,7 @@
title: "OpenID Connect 1.0"
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: []

View File

@ -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

View File

@ -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="<thumbprint of public key>" 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/

View File

@ -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 >}}

View File

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

View File

@ -88,6 +88,26 @@ If you've configured Authelia alongside a proxy and are making a request directl
request via the proxy. If you're avoiding the proxy due to a DNS limitation see
[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.

View File

@ -36,7 +36,7 @@ In addition to the `https` scheme requirement for Authelia itself:
1. Due to the fact a cookie is used, it's an intentional design decision that *__ALL__* applications/domains protected via
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.

8
go.mod
View File

@ -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

16
go.sum
View File

@ -130,22 +130,22 @@ github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrt
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/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=

View File

@ -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)
)

View File

@ -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
}

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -1,59 +1,167 @@
package authentication
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())
}

View File

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

View File

@ -215,7 +215,7 @@ func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error)
modifyRequest := ldap.NewModifyRequest(profile.DN, controls)
// 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)

View File

@ -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().

View File

@ -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"
}
}

View File

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

View File

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

View File

@ -66,13 +66,13 @@ func (m AccessControlDomainMatcher) IsMatch(domain string, subject Subject) (mat
return strings.HasSuffix(domain, m.Name)
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, ".")

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package authorization
import (
"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
}

View File

@ -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"`
}

View File

@ -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",

View File

@ -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",

View File

@ -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"`

View File

@ -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{

View File

@ -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)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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"))
}

View File

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

View File

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

View File

@ -127,8 +127,7 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID)
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)

View File

@ -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

View File

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

View File

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

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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)

View File

@ -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,

View File

@ -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 {

View File

@ -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)

View File

@ -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
}

View File

@ -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,

View File

@ -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{

View File

@ -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,

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -112,6 +112,10 @@ func (r *Mathematical) Intn(n int) int {
// IntnErr returns a random int error combination with a maximum of n.
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

View File

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

View File

@ -12,12 +12,12 @@ import (
)
// NewRegulator create a regulator instance.
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
}

View File

@ -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)
}

View File

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

View File

@ -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"
)

View File

@ -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 ""
}
}

View File

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

View File

@ -8,7 +8,7 @@ import (
)
func TestShouldEncryptAndDecriptUsingAES(t *testing.T) {
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")

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -23,8 +23,8 @@
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/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",

File diff suppressed because it is too large Load Diff