Merge branch 'master' into fix-pp-layout
commit
92f43a4061
|
@ -7,5 +7,5 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
const (
|
const (
|
||||||
versionSwaggerUI = "4.18.1"
|
versionSwaggerUI = "4.18.2"
|
||||||
)
|
)
|
||||||
|
|
|
@ -49,7 +49,7 @@ authelia configuration, and authelia database prior to attempting to do so._
|
||||||
Notable Missing Features from this build:
|
Notable Missing Features from this build:
|
||||||
|
|
||||||
- OpenID Connect 1.0 PAR
|
- OpenID Connect 1.0 PAR
|
||||||
- Multi-Device Webauthn
|
- Multi-Device WebAuthn
|
||||||
- Device Registration OTP
|
- Device Registration OTP
|
||||||
|
|
||||||
- Container Images:
|
- Container Images:
|
||||||
|
@ -144,7 +144,7 @@ Please see the [roadmap](../../roadmap/active/openid-connect.md) for more inform
|
||||||
|
|
||||||
##### Initial Implementation
|
##### Initial Implementation
|
||||||
|
|
||||||
_**Important Note:** This feature at the time of this writing, will not work well with Webauthn. Steps are being taken
|
_**Important Note:** This feature at the time of this writing, will not work well with WebAuthn. Steps are being taken
|
||||||
to address this however it will not specifically delay the release of this feature._
|
to address this however it will not specifically delay the release of this feature._
|
||||||
|
|
||||||
This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a
|
This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a
|
||||||
|
@ -160,14 +160,14 @@ NGINX/NGINX Proxy Manager/SWAG/HAProxy with the use of the new
|
||||||
[Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only
|
[Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only
|
||||||
need to configure a single middleware or helper to perform automatic redirection.
|
need to configure a single middleware or helper to perform automatic redirection.
|
||||||
|
|
||||||
## Webauthn
|
## WebAuthn
|
||||||
|
|
||||||
As part of our ongoing effort for comprehensive support for Webauthn we'll be introducing several important
|
As part of our ongoing effort for comprehensive support for WebAuthn we'll be introducing several important
|
||||||
features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information.
|
features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information.
|
||||||
|
|
||||||
##### Multiple Webauthn Credentials Per-User
|
##### Multiple WebAuthn Credentials Per-User
|
||||||
|
|
||||||
In this release we see full support for multiple Webauthn credentials. This is a fairly basic feature but getting the
|
In this release we see full support for multiple WebAuthn credentials. This is a fairly basic feature but getting the
|
||||||
frontend experience right is important to us. This is going to be supported via the
|
frontend experience right is important to us. This is going to be supported via the
|
||||||
[User Control Panel](#user-dashboard--control-panel).
|
[User Control Panel](#user-dashboard--control-panel).
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ authelia --config configuration.yml,config-acl.yml,config-other.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Authelia's configuration files use the YAML format. A template with all possible options can be found at the root of the
|
Authelia's configuration files use the YAML format. A template with all possible options can be found at the root of the
|
||||||
repository [here](https://github.com/authelia/authelia/blob/master/config.template.yml).
|
repository {{< github-link name="here" path="config.template.yml" >}}.
|
||||||
|
|
||||||
*__Important Note:__ You should not have configuration sections such as Access Control Rules or OpenID Connect clients
|
*__Important Note:__ You should not have configuration sections such as Access Control Rules or OpenID Connect clients
|
||||||
configured in multiple files. If you wish to split these into their own files that is fine, but if you have two files that
|
configured in multiple files. If you wish to split these into their own files that is fine, but if you have two files that
|
||||||
|
|
|
@ -16,9 +16,8 @@ toc: true
|
||||||
|
|
||||||
We document the configuration in two ways:
|
We document the configuration in two ways:
|
||||||
|
|
||||||
1. The [YAML] configuration template
|
1. The [YAML] configuration template {{< github-link path="config.template.yml" >}} has comments with very limited
|
||||||
[config.template.yml](https://github.com/authelia/authelia/blob/master/config.template.yml) has comments with very
|
documentation on the effective use of a particular option. All documentation lines start with `##`. Lines
|
||||||
limited documentation on the effective use of a particular option. All documentation lines start with `##`. Lines
|
|
||||||
starting with a single `#` are [YAML] configuration options which are commented to disable them or as examples.
|
starting with a single `#` are [YAML] configuration options which are commented to disable them or as examples.
|
||||||
2. This documentation site. Generally each section of the configuration is in its own section of the documentation
|
2. This documentation site. Generally each section of the configuration is in its own section of the documentation
|
||||||
site. Each configuration option is listed in its relevant section as a heading, under that heading generally are two
|
site. Each configuration option is listed in its relevant section as a heading, under that heading generally are two
|
||||||
|
|
|
@ -37,3 +37,4 @@ this instance if you wanted to downgrade to pre1 you would need to use an Authel
|
||||||
| 6 | 4.37.0 | Adjusted the OpenID Connect tables to allow pre-configured consent improvements |
|
| 6 | 4.37.0 | Adjusted the OpenID Connect tables to allow pre-configured consent improvements |
|
||||||
| 7 | 4.37.3 | Fixed some schema inconsistencies most notably the MySQL/MariaDB Engine and Collation |
|
| 7 | 4.37.3 | Fixed some schema inconsistencies most notably the MySQL/MariaDB Engine and Collation |
|
||||||
| 8 | 4.38.0 | OpenID Connect 1.0 Pushed Authorization Requests |
|
| 8 | 4.38.0 | OpenID Connect 1.0 Pushed Authorization Requests |
|
||||||
|
| 9 | 4.38.0 | Fix a PostgreSQL NOT NULL constraint issue on the `aaguid` column of the `webauthn_devices` table |
|
||||||
|
|
|
@ -38,6 +38,23 @@ The additional tools are recommended:
|
||||||
* [yamllint]
|
* [yamllint]
|
||||||
* [VSCodium] or [GoLand]
|
* [VSCodium] or [GoLand]
|
||||||
|
|
||||||
|
## Certificate
|
||||||
|
|
||||||
|
Authelia utilizes a self-signed Root CA certificate for the development environment. This allows us to sign elements of
|
||||||
|
the CI process uniformly and only trust a single additional Root CA Certificate. The private key for this certificate is
|
||||||
|
maintained by the [Core Team] so if you need an additional certificate signed for this purpose please reach out to them.
|
||||||
|
|
||||||
|
While developing for Authelia you may also want to trust this Root CA. It is critical that you are aware of what this
|
||||||
|
means if you decide to do so.
|
||||||
|
|
||||||
|
1. It will allow us to generate trusted certificates for machines this is installed on.
|
||||||
|
2. If compromised there is no formal revocation process at this time as we are not a certified CA.
|
||||||
|
3. Trusting Root CA's is not necessary for the development process it only makes it smoother.
|
||||||
|
4. Trusting additional Root CA's for prolonged periods is not generally a good idea.
|
||||||
|
|
||||||
|
If you'd still like to trust the Root CA Certificate it's located (encoded as a PEM) in the main git repository at
|
||||||
|
[/internal/suites/common/pki/ca/ca.public.crt](https://github.com/authelia/authelia/blob/master/internal/suites/common/pki/ca/ca.public.crt).
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
There is a scripting context provided with __Authelia__ which can easily be configured. It allows running integration
|
There is a scripting context provided with __Authelia__ which can easily be configured. It allows running integration
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Testing"
|
title: "Testing"
|
||||||
description: "Authelia Development Testing Guidelines"
|
description: "Authelia Development Testing Guidelines"
|
||||||
lead: "This section covers the testing guidelines."
|
lead: "This section covers the testing guidelines."
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-20T15:03:52+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Amir Zarrinkafsh"
|
title: "Amir Zarrinkafsh"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Clément Michaud"
|
title: "Clément Michaud"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Manuel Nuñez"
|
title: "Manuel Nuñez"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
title: "About"
|
title: "About"
|
||||||
description: "About Authelia and the Authelia Team"
|
description: "About Authelia and the Authelia Team"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
aliases:
|
aliases:
|
||||||
|
|
|
@ -25,8 +25,8 @@ bootstrapping *Authelia*.
|
||||||
|
|
||||||
We publish two example [systemd] unit files:
|
We publish two example [systemd] unit files:
|
||||||
|
|
||||||
* [authelia.service](https://github.com/authelia/authelia/blob/master/authelia.service)
|
* {{< github-link path="authelia.service" >}}
|
||||||
* [authelia@.service](https://github.com/authelia/authelia/blob/master/authelia%40.service)
|
* {{< github-link path="authelia@.service" >}}
|
||||||
|
|
||||||
## Arch Linux
|
## Arch Linux
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Firezone"
|
title: "Firezone"
|
||||||
description: "Integrating Firezone with the Authelia OpenID Connect Provider."
|
description: "Integrating Firezone with the Authelia OpenID Connect Provider."
|
||||||
lead: ""
|
lead: ""
|
||||||
date: 2023-03-25T13:07:02+10:00
|
date: 2023-03-28T20:29:13+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "MinIO"
|
title: "MinIO"
|
||||||
description: "Integrating MinIO with the Authelia OpenID Connect Provider."
|
description: "Integrating MinIO with the Authelia OpenID Connect Provider."
|
||||||
lead: ""
|
lead: ""
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-21T11:21:23+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Misago"
|
title: "Misago"
|
||||||
description: "Integrating Misago with the Authelia OpenID Connect Provider."
|
description: "Integrating Misago with the Authelia OpenID Connect Provider."
|
||||||
lead: ""
|
lead: ""
|
||||||
date: 2023-03-04T13:20:00+00:00
|
date: 2023-03-14T08:51:13+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -23,24 +23,31 @@ common scenarios however those using more advanced architectures are likely goin
|
||||||
help with answering less specific questions about this and it may be possible if provided adequate information more
|
help with answering less specific questions about this and it may be possible if provided adequate information more
|
||||||
specific questions may be answered.
|
specific questions may be answered.
|
||||||
|
|
||||||
|
1. Authelia *__MUST__* be served via the `https` scheme. This is not optional even for testing. This is a deliberate
|
||||||
|
design decision to improve security directly (by using encrypted communication) and indirectly by reducing complexity.
|
||||||
|
|
||||||
### Forwarded Authentication
|
### Forwarded Authentication
|
||||||
|
|
||||||
Forwarded Authentication is a simple per-request authorization flow that checks the metadata of a request and a session
|
Forwarded Authentication is a simple per-request authorization flow that checks the metadata of a request and a session
|
||||||
cookie to determine if a user must be forwarded to the authentication portal.
|
cookie to determine if a user must be forwarded to the authentication portal.
|
||||||
|
|
||||||
Due to the fact a cookie is used, it's an intentional design decision that *__ALL__* applications/domains protected via
|
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.
|
this method *__MUST__* use secure schemes (`https` and `wss`) for all of their communication.
|
||||||
|
|
||||||
### OpenID Connect
|
### OpenID Connect
|
||||||
|
|
||||||
Only requires Authelia to be accessible via a secure scheme (`https`).
|
No additional requirements other than the use of the `https` scheme for Authelia itself exist excluding those mandated
|
||||||
|
by the relevant specifications.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
It's important to customize the configuration for *Authelia* in advance of deploying it. The configuration is static and
|
It's important to customize the configuration for *Authelia* in advance of deploying it. The configuration is static and
|
||||||
not configured via web GUI. You can find a
|
not configured via web GUI. You can find a configuration template named {{< github-link path="config.template.yml" >}}
|
||||||
[configuration template](https://github.com/authelia/authelia/blob/master/config.template.yml) on GitHub which can be
|
on GitHub which can be used as a basis for configuration, alternatively *Authelia* will write this template relevant for
|
||||||
used as a basis for configuration.
|
your version the first time it is started. Users should expect that they have to configure elements of this file as part
|
||||||
|
of initial setup.
|
||||||
|
|
||||||
The important sections to consider in initial configuration are as follows:
|
The important sections to consider in initial configuration are as follows:
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ serving Authelia at `auth.example.com`.
|
||||||
```nginx
|
```nginx
|
||||||
## Set $authelia_backend to route requests to the current domain by default
|
## Set $authelia_backend to route requests to the current domain by default
|
||||||
set $authelia_backend $http_host;
|
set $authelia_backend $http_host;
|
||||||
## In order for Webauthn to work with multiple domains authelia must operate on a separate subdomain
|
## In order for WebAuthn to work with multiple domains authelia must operate on a separate subdomain
|
||||||
## To use authelia on a separate subdomain:
|
## To use authelia on a separate subdomain:
|
||||||
## * comment the $authelia_backend line above
|
## * comment the $authelia_backend line above
|
||||||
## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf
|
## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf
|
||||||
|
@ -88,7 +88,7 @@ serving Authelia at `auth.example.com`.
|
||||||
```nginx
|
```nginx
|
||||||
## Set $authelia_backend to route requests to the current domain by default
|
## Set $authelia_backend to route requests to the current domain by default
|
||||||
# set $authelia_backend $http_host;
|
# set $authelia_backend $http_host;
|
||||||
## In order for Webauthn to work with multiple domains authelia must operate on a separate subdomain
|
## In order for WebAuthn to work with multiple domains authelia must operate on a separate subdomain
|
||||||
## To use authelia on a separate subdomain:
|
## To use authelia on a separate subdomain:
|
||||||
## * comment the $authelia_backend line above
|
## * comment the $authelia_backend line above
|
||||||
## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf
|
## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf
|
||||||
|
|
|
@ -44,7 +44,7 @@ case you have multiple devices available, you will be asked to select your prefe
|
||||||
### Why don't I have access to the *Push Notification* option?
|
### Why don't I have access to the *Push Notification* option?
|
||||||
|
|
||||||
It's likely that you have not configured __Authelia__ correctly. Please read this documentation again and be sure you
|
It's likely that you have not configured __Authelia__ correctly. Please read this documentation again and be sure you
|
||||||
had a look at [config.template.yml](https://github.com/authelia/authelia/blob/master/config.template.yml) and
|
had a look at {{< github-link path="config.template.yml" >}} and
|
||||||
[configuration documentation](../../../configuration/second-factor/duo.md).
|
[configuration documentation](../../../configuration/second-factor/duo.md).
|
||||||
|
|
||||||
[Duo]: https://duo.com/
|
[Duo]: https://duo.com/
|
||||||
|
|
|
@ -63,5 +63,5 @@ authelia storage user --help
|
||||||
* [authelia storage](authelia_storage.md) - Manage the Authelia storage
|
* [authelia storage](authelia_storage.md) - Manage the Authelia storage
|
||||||
* [authelia storage user identifiers](authelia_storage_user_identifiers.md) - Manage user opaque identifiers
|
* [authelia storage user identifiers](authelia_storage_user_identifiers.md) - Manage user opaque identifiers
|
||||||
* [authelia storage user totp](authelia_storage_user_totp.md) - Manage TOTP configurations
|
* [authelia storage user totp](authelia_storage_user_totp.md) - Manage TOTP configurations
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn
|
## authelia storage user webauthn
|
||||||
|
|
||||||
Manage Webauthn devices
|
Manage WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Manage Webauthn devices.
|
Manage WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows interacting with Webauthn devices.
|
This subcommand allows interacting with WebAuthn devices.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ authelia storage user webauthn --help
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user](authelia_storage_user.md) - Manages user settings
|
* [authelia storage user](authelia_storage_user.md) - Manages user settings
|
||||||
* [authelia storage user webauthn delete](authelia_storage_user_webauthn_delete.md) - Delete a Webauthn device
|
* [authelia storage user webauthn delete](authelia_storage_user_webauthn_delete.md) - Delete a WebAuthn device
|
||||||
* [authelia storage user webauthn export](authelia_storage_user_webauthn_export.md) - Perform exports of the Webauthn devices
|
* [authelia storage user webauthn export](authelia_storage_user_webauthn_export.md) - Perform exports of the WebAuthn devices
|
||||||
* [authelia storage user webauthn import](authelia_storage_user_webauthn_import.md) - Perform imports of the Webauthn devices
|
* [authelia storage user webauthn import](authelia_storage_user_webauthn_import.md) - Perform imports of the WebAuthn devices
|
||||||
* [authelia storage user webauthn list](authelia_storage_user_webauthn_list.md) - List Webauthn devices
|
* [authelia storage user webauthn list](authelia_storage_user_webauthn_list.md) - List WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn delete
|
## authelia storage user webauthn delete
|
||||||
|
|
||||||
Delete a Webauthn device
|
Delete a WebAuthn device
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Delete a Webauthn device.
|
Delete a WebAuthn device.
|
||||||
|
|
||||||
This subcommand allows deleting a Webauthn device directly from the database.
|
This subcommand allows deleting a WebAuthn device directly from the database.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn delete [username] [flags]
|
authelia storage user webauthn delete [username] [flags]
|
||||||
|
@ -75,5 +75,5 @@ authelia storage user webauthn delete --kid abc123 --encryption-key b3453fde-ecc
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn export
|
## authelia storage user webauthn export
|
||||||
|
|
||||||
Perform exports of the Webauthn devices
|
Perform exports of the WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Perform exports of the Webauthn devices.
|
Perform exports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows exporting Webauthn devices to various formats.
|
This subcommand allows exporting WebAuthn devices to various formats.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn export [flags]
|
authelia storage user webauthn export [flags]
|
||||||
|
@ -68,5 +68,5 @@ authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-27
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn import
|
## authelia storage user webauthn import
|
||||||
|
|
||||||
Perform imports of the Webauthn devices
|
Perform imports of the WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Perform imports of the Webauthn devices.
|
Perform imports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows importing Webauthn devices from various formats.
|
This subcommand allows importing WebAuthn devices from various formats.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn import <filename> [flags]
|
authelia storage user webauthn import <filename> [flags]
|
||||||
|
@ -67,5 +67,5 @@ authelia storage user webauthn import --file authelia.export.webauthn.yaml --enc
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn list
|
## authelia storage user webauthn list
|
||||||
|
|
||||||
List Webauthn devices
|
List WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
List Webauthn devices.
|
List WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows listing Webauthn devices.
|
This subcommand allows listing WebAuthn devices.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn list [username] [flags]
|
authelia storage user webauthn list [username] [flags]
|
||||||
|
@ -69,5 +69,5 @@ authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-942
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{- $repo := "authelia/authelia" }}{{ with .Get "repo" }}{{ $repo = . }}{{ end }}
|
||||||
|
{{- $branch := printf "v%s" .Site.Data.misc.latest }}{{ with .Get "branch" }}{{ $branch = . }}{{ end }}
|
||||||
|
{{- $path := "" }}{{ with .Get "path" }}{{ $path = . }}{{ end }}
|
||||||
|
{{- $link := printf "https://github.com/%s/blob/%s/%s" $repo $branch (urlquery $path) }}
|
||||||
|
{{- $name := "" }}
|
||||||
|
{{- with .Get "name" }}
|
||||||
|
{{- $name = . }}
|
||||||
|
{{- else }}
|
||||||
|
{{- if (eq $repo "authelia/authelia") }}
|
||||||
|
{{- $name = $path }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name = printf "https://github.com/%s/blob/%s/%s" $repo $branch $path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- "" -}}
|
||||||
|
<a href="{{ $link }}" target="_blank">{{ $name }}</a>
|
||||||
|
{{- "" -}}
|
|
@ -39,27 +39,27 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "7.21.0",
|
"@babel/cli": "7.21.0",
|
||||||
"@babel/core": "7.21.0",
|
"@babel/core": "7.21.4",
|
||||||
"@babel/preset-env": "7.20.2",
|
"@babel/preset-env": "7.21.4",
|
||||||
"@fullhuman/postcss-purgecss": "5.0.0",
|
"@fullhuman/postcss-purgecss": "5.0.0",
|
||||||
"@hyas/images": "0.3.2",
|
"@hyas/images": "0.3.2",
|
||||||
"@popperjs/core": "2.11.6",
|
"@popperjs/core": "2.11.7",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.14",
|
||||||
"bootstrap": "5.2.3",
|
"bootstrap": "5.2.3",
|
||||||
"bootstrap-icons": "1.10.3",
|
"bootstrap-icons": "1.10.4",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"eslint": "8.35.0",
|
"eslint": "8.38.0",
|
||||||
"exec-bin": "1.0.0",
|
"exec-bin": "1.0.0",
|
||||||
"flexsearch": "0.7.31",
|
"flexsearch": "0.7.31",
|
||||||
"highlight.js": "11.7.0",
|
"highlight.js": "11.7.0",
|
||||||
"hugo-installer": "4.0.1",
|
"hugo-installer": "4.0.1",
|
||||||
"instant.page": "5.1.1",
|
"instant.page": "5.2.0",
|
||||||
"katex": "0.16.4",
|
"katex": "0.16.4",
|
||||||
"lazysizes": "5.3.2",
|
"lazysizes": "5.3.2",
|
||||||
"markdownlint-cli2": "0.6.0",
|
"markdownlint-cli2": "0.6.0",
|
||||||
"netlify-plugin-submit-sitemap": "0.4.0",
|
"netlify-plugin-submit-sitemap": "0.4.0",
|
||||||
"node-fetch": "3.3.0",
|
"node-fetch": "3.3.1",
|
||||||
"postcss": "8.4.21",
|
"postcss": "8.4.21",
|
||||||
"postcss-cli": "10.1.0",
|
"postcss-cli": "10.1.0",
|
||||||
"purgecss-whitelister": "2.4.0",
|
"purgecss-whitelister": "2.4.0",
|
||||||
|
@ -68,6 +68,6 @@
|
||||||
"stylelint-config-standard-scss": "6.1.0"
|
"stylelint-config-standard-scss": "6.1.0"
|
||||||
},
|
},
|
||||||
"otherDependencies": {
|
"otherDependencies": {
|
||||||
"hugo": "0.111.2"
|
"hugo": "0.111.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2197
docs/pnpm-lock.yaml
2197
docs/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
6
go.mod
6
go.mod
|
@ -27,13 +27,13 @@ require (
|
||||||
github.com/knadh/koanf/providers/env v0.1.0
|
github.com/knadh/koanf/providers/env v0.1.0
|
||||||
github.com/knadh/koanf/providers/posflag v0.1.0
|
github.com/knadh/koanf/providers/posflag v0.1.0
|
||||||
github.com/knadh/koanf/providers/rawbytes v0.1.0
|
github.com/knadh/koanf/providers/rawbytes v0.1.0
|
||||||
github.com/knadh/koanf/v2 v2.0.0
|
github.com/knadh/koanf/v2 v2.0.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||||
github.com/ory/fosite v0.44.0
|
github.com/ory/fosite v0.44.0
|
||||||
github.com/ory/herodot v0.10.0
|
github.com/ory/herodot v0.10.2
|
||||||
github.com/ory/x v0.0.549
|
github.com/ory/x v0.0.551
|
||||||
github.com/otiai10/copy v1.10.0
|
github.com/otiai10/copy v1.10.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
|
|
12
go.sum
12
go.sum
|
@ -295,8 +295,8 @@ github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHY
|
||||||
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
|
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
|
||||||
github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME=
|
github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME=
|
||||||
github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c=
|
github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c=
|
||||||
github.com/knadh/koanf/v2 v2.0.0 h1:XPQ5ilNnwnNaHrfQ1YpTVhUAjcGHnEKA+lRpipQv02Y=
|
github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
|
||||||
github.com/knadh/koanf/v2 v2.0.0/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
|
github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
@ -349,10 +349,10 @@ github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8I
|
||||||
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI=
|
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI=
|
||||||
github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8=
|
github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8=
|
||||||
github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs=
|
github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs=
|
||||||
github.com/ory/herodot v0.10.0 h1:j4wDWezsHtZNTSWyXt0sVeQS3QUDCzpVWJMQx1A5Kmg=
|
github.com/ory/herodot v0.10.2 h1:gGvNMHgAwWzdP/eo+roSiT5CGssygHSjDU7MSQNlJ4E=
|
||||||
github.com/ory/herodot v0.10.0/go.mod h1:MMNmY6MG1uB6fnXYFaHoqdV23DTWctlPsmRCeq/2+wc=
|
github.com/ory/herodot v0.10.2/go.mod h1:MMNmY6MG1uB6fnXYFaHoqdV23DTWctlPsmRCeq/2+wc=
|
||||||
github.com/ory/x v0.0.549 h1:/ngQEYmHMEQAsYxK4uasAR9/WALxRLfHiDUPFQrD6/I=
|
github.com/ory/x v0.0.551 h1:U3z2bvSzAwDP0SWmbAdjzfvWPu4k+oWrPctoCdalGk0=
|
||||||
github.com/ory/x v0.0.549/go.mod h1:00UrEq/wEgXxpagcfjn5w2PsJPpfxAVnb94M+eg1bC0=
|
github.com/ory/x v0.0.551/go.mod h1:oRVemI3SQQOLvOCJWIRinHQKlgmay/NbwSyRUIsS/Yk=
|
||||||
github.com/otiai10/copy v1.10.0 h1:znyI7l134wNg/wDktoVQPxPkgvhDfGCYUasey+h0rDQ=
|
github.com/otiai10/copy v1.10.0 h1:znyI7l134wNg/wDktoVQPxPkgvhDfGCYUasey+h0rDQ=
|
||||||
github.com/otiai10/copy v1.10.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
|
github.com/otiai10/copy v1.10.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
|
||||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||||
|
|
|
@ -177,56 +177,56 @@ This subcommand allows manually adding an opaque identifier for a user to the da
|
||||||
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml
|
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml
|
||||||
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnShort = "Manage Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnShort = "Manage WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnLong = `Manage Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnLong = `Manage WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows interacting with Webauthn devices.`
|
This subcommand allows interacting with WebAuthn devices.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExample = `authelia storage user webauthn --help`
|
cmdAutheliaStorageUserWebAuthnExample = `authelia storage user webauthn --help`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnImportShort = "Perform imports of the Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnImportShort = "Perform imports of the WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnImportLong = `Perform imports of the Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnImportLong = `Perform imports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows importing Webauthn devices from various formats.`
|
This subcommand allows importing WebAuthn devices from various formats.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnImportExample = `authelia storage user webauthn export
|
cmdAutheliaStorageUserWebAuthnImportExample = `authelia storage user webauthn export
|
||||||
authelia storage user webauthn import --file authelia.export.webauthn.yaml
|
authelia storage user webauthn import --file authelia.export.webauthn.yaml
|
||||||
authelia storage user webauthn import --file authelia.export.webauthn.yaml --config config.yml
|
authelia storage user webauthn import --file authelia.export.webauthn.yaml --config config.yml
|
||||||
authelia storage user webauthn import --file authelia.export.webauthn.yaml --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user webauthn import --file authelia.export.webauthn.yaml --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExportShort = "Perform exports of the Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnExportShort = "Perform exports of the WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExportLong = `Perform exports of the Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnExportLong = `Perform exports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows exporting Webauthn devices to various formats.`
|
This subcommand allows exporting WebAuthn devices to various formats.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExportExample = `authelia storage user webauthn export
|
cmdAutheliaStorageUserWebAuthnExportExample = `authelia storage user webauthn export
|
||||||
authelia storage user webauthn export --file authelia.export.webauthn.yaml
|
authelia storage user webauthn export --file authelia.export.webauthn.yaml
|
||||||
authelia storage user webauthn export --config config.yml
|
authelia storage user webauthn export --config config.yml
|
||||||
authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnListShort = "List Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnListShort = "List WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnListLong = `List Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnListLong = `List WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows listing Webauthn devices.`
|
This subcommand allows listing WebAuthn devices.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnListExample = `authelia storage user webauthn list
|
cmdAutheliaStorageUserWebAuthnListExample = `authelia storage user webauthn list
|
||||||
authelia storage user webauthn list john
|
authelia storage user webauthn list john
|
||||||
authelia storage user webauthn list --config config.yml
|
authelia storage user webauthn list --config config.yml
|
||||||
authelia storage user webauthn list john --config config.yml
|
authelia storage user webauthn list john --config config.yml
|
||||||
authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
||||||
authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnDeleteShort = "Delete a Webauthn device"
|
cmdAutheliaStorageUserWebAuthnDeleteShort = "Delete a WebAuthn device"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnDeleteLong = `Delete a Webauthn device.
|
cmdAutheliaStorageUserWebAuthnDeleteLong = `Delete a WebAuthn device.
|
||||||
|
|
||||||
This subcommand allows deleting a Webauthn device directly from the database.`
|
This subcommand allows deleting a WebAuthn device directly from the database.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnDeleteExample = `authelia storage user webauthn delete john --all
|
cmdAutheliaStorageUserWebAuthnDeleteExample = `authelia storage user webauthn delete john --all
|
||||||
authelia storage user webauthn delete john --all --config config.yml
|
authelia storage user webauthn delete john --all --config config.yml
|
||||||
authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
||||||
authelia storage user webauthn delete john --description Primary
|
authelia storage user webauthn delete john --description Primary
|
||||||
|
|
|
@ -68,7 +68,7 @@ func storageTOTPGenerateRunEOptsFromFlags(flags *pflag.FlagSet) (force bool, fil
|
||||||
return force, filename, secret, nil
|
return force, filename, secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func storageWebauthnDeleteRunEOptsFromFlags(flags *pflag.FlagSet, args []string) (all, byKID bool, description, kid, user string, err error) {
|
func storageWebAuthnDeleteRunEOptsFromFlags(flags *pflag.FlagSet, args []string) (all, byKID bool, description, kid, user string, err error) {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
user = args[0]
|
user = args[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ func newStorageUserCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newStorageUserIdentifiersCmd(ctx),
|
newStorageUserIdentifiersCmd(ctx),
|
||||||
newStorageUserTOTPCmd(ctx),
|
newStorageUserTOTPCmd(ctx),
|
||||||
newStorageUserWebauthnCmd(ctx),
|
newStorageUserWebAuthnCmd(ctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -221,34 +221,34 @@ func newStorageUserIdentifiersAddCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "webauthn",
|
Use: "webauthn",
|
||||||
Short: cmdAutheliaStorageUserWebauthnShort,
|
Short: cmdAutheliaStorageUserWebAuthnShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnLong,
|
Long: cmdAutheliaStorageUserWebAuthnLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnExample,
|
Example: cmdAutheliaStorageUserWebAuthnExample,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newStorageUserWebauthnListCmd(ctx),
|
newStorageUserWebAuthnListCmd(ctx),
|
||||||
newStorageUserWebauthnDeleteCmd(ctx),
|
newStorageUserWebAuthnDeleteCmd(ctx),
|
||||||
newStorageUserWebauthnExportCmd(ctx),
|
newStorageUserWebAuthnExportCmd(ctx),
|
||||||
newStorageUserWebauthnImportCmd(ctx),
|
newStorageUserWebAuthnImportCmd(ctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: cmdUseImportFileName,
|
Use: cmdUseImportFileName,
|
||||||
Short: cmdAutheliaStorageUserWebauthnImportShort,
|
Short: cmdAutheliaStorageUserWebAuthnImportShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnImportLong,
|
Long: cmdAutheliaStorageUserWebAuthnImportLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnImportExample,
|
Example: cmdAutheliaStorageUserWebAuthnImportExample,
|
||||||
RunE: ctx.StorageUserWebauthnImportRunE,
|
RunE: ctx.StorageUserWebAuthnImportRunE,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
@ -257,13 +257,13 @@ func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: cmdUseExport,
|
Use: cmdUseExport,
|
||||||
Short: cmdAutheliaStorageUserWebauthnExportShort,
|
Short: cmdAutheliaStorageUserWebAuthnExportShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnExportLong,
|
Long: cmdAutheliaStorageUserWebAuthnExportLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnExportExample,
|
Example: cmdAutheliaStorageUserWebAuthnExportExample,
|
||||||
RunE: ctx.StorageUserWebauthnExportRunE,
|
RunE: ctx.StorageUserWebAuthnExportRunE,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
@ -274,13 +274,13 @@ func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "list [username]",
|
Use: "list [username]",
|
||||||
Short: cmdAutheliaStorageUserWebauthnListShort,
|
Short: cmdAutheliaStorageUserWebAuthnListShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnListLong,
|
Long: cmdAutheliaStorageUserWebAuthnListLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnListExample,
|
Example: cmdAutheliaStorageUserWebAuthnListExample,
|
||||||
RunE: ctx.StorageUserWebauthnListRunE,
|
RunE: ctx.StorageUserWebAuthnListRunE,
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
@ -289,13 +289,13 @@ func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "delete [username]",
|
Use: "delete [username]",
|
||||||
Short: cmdAutheliaStorageUserWebauthnDeleteShort,
|
Short: cmdAutheliaStorageUserWebAuthnDeleteShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnDeleteLong,
|
Long: cmdAutheliaStorageUserWebAuthnDeleteLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnDeleteExample,
|
Example: cmdAutheliaStorageUserWebAuthnDeleteExample,
|
||||||
RunE: ctx.StorageUserWebauthnDeleteRunE,
|
RunE: ctx.StorageUserWebAuthnDeleteRunE,
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
|
|
@ -415,7 +415,7 @@ func (ctx *CmdCtx) StorageSchemaInfoRunE(_ *cobra.Command, _ []string) (err erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []string) (err error) {
|
func (ctx *CmdCtx) StorageUserWebAuthnExportRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = ctx.providers.StorageProvider.Close()
|
_ = ctx.providers.StorageProvider.Close()
|
||||||
}()
|
}()
|
||||||
|
@ -443,11 +443,11 @@ func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []stri
|
||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
var (
|
var (
|
||||||
devices []model.WebauthnDevice
|
devices []model.WebAuthnDevice
|
||||||
)
|
)
|
||||||
|
|
||||||
export := &model.WebauthnDeviceExport{
|
export := &model.WebAuthnDeviceExport{
|
||||||
WebauthnDevices: nil,
|
WebAuthnDevices: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
for page := 0; true; page++ {
|
for page := 0; true; page++ {
|
||||||
|
@ -455,7 +455,7 @@ func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []stri
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
export.WebauthnDevices = append(export.WebauthnDevices, devices...)
|
export.WebAuthnDevices = append(export.WebAuthnDevices, devices...)
|
||||||
|
|
||||||
l := len(devices)
|
l := len(devices)
|
||||||
|
|
||||||
|
@ -476,12 +476,12 @@ func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []stri
|
||||||
return fmt.Errorf("error occurred writing to file '%s': %w", filename, err)
|
return fmt.Errorf("error occurred writing to file '%s': %w", filename, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(cliOutputFmtSuccessfulUserExportFile, count, "Webauthn devices", "YAML", filename)
|
fmt.Printf(cliOutputFmtSuccessfulUserExportFile, count, "WebAuthn devices", "YAML", filename)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *CmdCtx) StorageUserWebauthnImportRunE(cmd *cobra.Command, args []string) (err error) {
|
func (ctx *CmdCtx) StorageUserWebAuthnImportRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = ctx.providers.StorageProvider.Close()
|
_ = ctx.providers.StorageProvider.Close()
|
||||||
}()
|
}()
|
||||||
|
@ -507,46 +507,46 @@ func (ctx *CmdCtx) StorageUserWebauthnImportRunE(cmd *cobra.Command, args []stri
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
export := &model.WebauthnDeviceExport{}
|
export := &model.WebAuthnDeviceExport{}
|
||||||
|
|
||||||
if err = yaml.Unmarshal(data, export); err != nil {
|
if err = yaml.Unmarshal(data, export); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(export.WebauthnDevices) == 0 {
|
if len(export.WebAuthnDevices) == 0 {
|
||||||
return fmt.Errorf("can't import a YAML file without Webauthn devices data")
|
return fmt.Errorf("can't import a YAML file without WebAuthn devices data")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = ctx.CheckSchema(); err != nil {
|
if err = ctx.CheckSchema(); err != nil {
|
||||||
return storageWrapCheckSchemaErr(err)
|
return storageWrapCheckSchemaErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, device := range export.WebauthnDevices {
|
for _, device := range export.WebAuthnDevices {
|
||||||
if err = ctx.providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
if err = ctx.providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(cliOutputFmtSuccessfulUserImportFile, len(export.WebauthnDevices), "Webauthn devices", "YAML", filename)
|
fmt.Printf(cliOutputFmtSuccessfulUserImportFile, len(export.WebAuthnDevices), "WebAuthn devices", "YAML", filename)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorageUserWebauthnListRunE is the RunE for the authelia storage user webauthn list command.
|
// StorageUserWebAuthnListRunE is the RunE for the authelia storage user webauthn list command.
|
||||||
func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string) (err error) {
|
func (ctx *CmdCtx) StorageUserWebAuthnListRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = ctx.providers.StorageProvider.Close()
|
_ = ctx.providers.StorageProvider.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if len(args) == 0 || args[0] == "" {
|
if len(args) == 0 || args[0] == "" {
|
||||||
return ctx.StorageUserWebauthnListAllRunE(cmd, args)
|
return ctx.StorageUserWebAuthnListAllRunE(cmd, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = ctx.CheckSchema(); err != nil {
|
if err = ctx.CheckSchema(); err != nil {
|
||||||
return storageWrapCheckSchemaErr(err)
|
return storageWrapCheckSchemaErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var devices []model.WebauthnDevice
|
var devices []model.WebAuthnDevice
|
||||||
|
|
||||||
user := args[0]
|
user := args[0]
|
||||||
|
|
||||||
|
@ -558,7 +558,7 @@ func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return fmt.Errorf("can't list devices for user '%s': %w", user, err)
|
return fmt.Errorf("can't list devices for user '%s': %w", user, err)
|
||||||
default:
|
default:
|
||||||
fmt.Printf("Webauthn Devices for user '%s':\n\n", user)
|
fmt.Printf("WebAuthn Devices for user '%s':\n\n", user)
|
||||||
fmt.Printf("ID\tKID\tDescription\n")
|
fmt.Printf("ID\tKID\tDescription\n")
|
||||||
|
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
|
@ -569,8 +569,8 @@ func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorageUserWebauthnListAllRunE is the RunE for the authelia storage user webauthn list command when no args are specified.
|
// StorageUserWebAuthnListAllRunE is the RunE for the authelia storage user webauthn list command when no args are specified.
|
||||||
func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string) (err error) {
|
func (ctx *CmdCtx) StorageUserWebAuthnListAllRunE(_ *cobra.Command, _ []string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = ctx.providers.StorageProvider.Close()
|
_ = ctx.providers.StorageProvider.Close()
|
||||||
}()
|
}()
|
||||||
|
@ -579,7 +579,7 @@ func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string)
|
||||||
return storageWrapCheckSchemaErr(err)
|
return storageWrapCheckSchemaErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var devices []model.WebauthnDevice
|
var devices []model.WebAuthnDevice
|
||||||
|
|
||||||
limit := 10
|
limit := 10
|
||||||
|
|
||||||
|
@ -603,14 +603,14 @@ func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Webauthn Devices:\n\nID\tKID\tDescription\tUsername\n")
|
fmt.Printf("WebAuthn Devices:\n\nID\tKID\tDescription\tUsername\n")
|
||||||
fmt.Println(output.String())
|
fmt.Println(output.String())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorageUserWebauthnDeleteRunE is the RunE for the authelia storage user webauthn delete command.
|
// StorageUserWebAuthnDeleteRunE is the RunE for the authelia storage user webauthn delete command.
|
||||||
func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []string) (err error) {
|
func (ctx *CmdCtx) StorageUserWebAuthnDeleteRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = ctx.providers.StorageProvider.Close()
|
_ = ctx.providers.StorageProvider.Close()
|
||||||
}()
|
}()
|
||||||
|
@ -624,7 +624,7 @@ func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []stri
|
||||||
description, kid, user string
|
description, kid, user string
|
||||||
)
|
)
|
||||||
|
|
||||||
if all, byKID, description, kid, user, err = storageWebauthnDeleteRunEOptsFromFlags(cmd.Flags(), args); err != nil {
|
if all, byKID, description, kid, user, err = storageWebAuthnDeleteRunEOptsFromFlags(cmd.Flags(), args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -633,7 +633,7 @@ func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []stri
|
||||||
return fmt.Errorf("failed to delete webauthn device with kid '%s': %w", kid, err)
|
return fmt.Errorf("failed to delete webauthn device with kid '%s': %w", kid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully deleted Webauthn device with key id '%s'\n", kid)
|
fmt.Printf("Successfully deleted WebAuthn device with key id '%s'\n", kid)
|
||||||
} else {
|
} else {
|
||||||
err = ctx.providers.StorageProvider.DeleteWebauthnDeviceByUsername(ctx, user, description)
|
err = ctx.providers.StorageProvider.DeleteWebauthnDeviceByUsername(ctx, user, description)
|
||||||
|
|
||||||
|
@ -642,13 +642,13 @@ func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []stri
|
||||||
return fmt.Errorf("failed to delete all webauthn devices with username '%s': %w", user, err)
|
return fmt.Errorf("failed to delete all webauthn devices with username '%s': %w", user, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully deleted all Webauthn devices for user '%s'\n", user)
|
fmt.Printf("Successfully deleted all WebAuthn devices for user '%s'\n", user)
|
||||||
} else {
|
} else {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete webauthn device with username '%s' and description '%s': %w", user, description, err)
|
return fmt.Errorf("failed to delete webauthn device with username '%s' and description '%s': %w", user, description, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully deleted Webauthn device with description '%s' for user '%s'\n", description, user)
|
fmt.Printf("Successfully deleted WebAuthn device with description '%s' for user '%s'\n", description, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -215,13 +215,14 @@ const (
|
||||||
func loadXEnvCLIConfigValues(cmd *cobra.Command) (configs []string, filters []configuration.FileFilter, err error) {
|
func loadXEnvCLIConfigValues(cmd *cobra.Command) (configs []string, filters []configuration.FileFilter, err error) {
|
||||||
var (
|
var (
|
||||||
filterNames []string
|
filterNames []string
|
||||||
|
result XEnvCLIResult
|
||||||
)
|
)
|
||||||
|
|
||||||
if configs, _, err = loadXEnvCLIStringSliceValue(cmd, cmdFlagEnvNameConfig, cmdFlagNameConfig); err != nil {
|
if configs, result, err = loadXEnvCLIStringSliceValue(cmd, cmdFlagEnvNameConfig, cmdFlagNameConfig); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if configs, err = loadXNormalizedPaths(configs); err != nil {
|
if configs, err = loadXNormalizedPaths(configs, result); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +237,7 @@ func loadXEnvCLIConfigValues(cmd *cobra.Command) (configs []string, filters []co
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadXNormalizedPaths(paths []string) ([]string, error) {
|
func loadXNormalizedPaths(paths []string, result XEnvCLIResult) ([]string, error) {
|
||||||
var (
|
var (
|
||||||
configs, files, dirs []string
|
configs, files, dirs []string
|
||||||
err error
|
err error
|
||||||
|
@ -258,11 +259,16 @@ func loadXNormalizedPaths(paths []string) ([]string, error) {
|
||||||
files = append(files, path)
|
files = append(files, path)
|
||||||
default:
|
default:
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
switch result {
|
||||||
|
case XEnvCLIResultCLIImplicit:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
configs = append(configs, path)
|
configs = append(configs, path)
|
||||||
files = append(files, path)
|
files = append(files, path)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("error occurred stating file at path '%s': %w", path, err)
|
return nil, fmt.Errorf("error occurred stating file at path '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,7 @@ func TestLoadXNormalizedPaths(t *testing.T) {
|
||||||
ayml := filepath.Join(configdir, "a.yml")
|
ayml := filepath.Join(configdir, "a.yml")
|
||||||
byml := filepath.Join(configdir, "b.yml")
|
byml := filepath.Join(configdir, "b.yml")
|
||||||
cyml := filepath.Join(otherdir, "c.yml")
|
cyml := filepath.Join(otherdir, "c.yml")
|
||||||
|
dyml := filepath.Join(otherdir, "d.yml")
|
||||||
|
|
||||||
file, err = os.Create(ayml)
|
file, err = os.Create(ayml)
|
||||||
|
|
||||||
|
@ -142,30 +143,44 @@ func TestLoadXNormalizedPaths(t *testing.T) {
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
haveX XEnvCLIResult
|
||||||
have, expected []string
|
have, expected []string
|
||||||
expectedErr string
|
expectedErr string
|
||||||
}{
|
}{
|
||||||
{"ShouldAllowFiles",
|
{"ShouldAllowFiles",
|
||||||
|
XEnvCLIResultCLIImplicit, []string{ayml},
|
||||||
[]string{ayml},
|
[]string{ayml},
|
||||||
[]string{ayml}, "",
|
"",
|
||||||
|
},
|
||||||
|
{"ShouldSkipFilesNotExistImplicit",
|
||||||
|
XEnvCLIResultCLIImplicit, []string{dyml},
|
||||||
|
[]string(nil),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{"ShouldNotErrFilesNotExistExplicit",
|
||||||
|
XEnvCLIResultCLIExplicit, []string{dyml},
|
||||||
|
[]string{dyml},
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
{"ShouldAllowDirectories",
|
{"ShouldAllowDirectories",
|
||||||
|
XEnvCLIResultCLIImplicit, []string{configdir},
|
||||||
[]string{configdir},
|
[]string{configdir},
|
||||||
[]string{configdir}, "",
|
"",
|
||||||
},
|
},
|
||||||
{"ShouldAllowFilesDirectories",
|
{"ShouldAllowFilesDirectories",
|
||||||
|
XEnvCLIResultCLIImplicit, []string{ayml, otherdir},
|
||||||
[]string{ayml, otherdir},
|
[]string{ayml, otherdir},
|
||||||
[]string{ayml, otherdir}, "",
|
"",
|
||||||
},
|
},
|
||||||
{"ShouldRaiseErrOnOverlappingFilesDirectories",
|
{"ShouldRaiseErrOnOverlappingFilesDirectories",
|
||||||
[]string{ayml, configdir},
|
XEnvCLIResultCLIImplicit, []string{ayml, configdir},
|
||||||
nil, fmt.Sprintf("failed to load config directory '%s': the config file '%s' is in that directory which is not supported", configdir, ayml),
|
nil, fmt.Sprintf("failed to load config directory '%s': the config file '%s' is in that directory which is not supported", configdir, ayml),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
actual, actualErr := loadXNormalizedPaths(tc.have)
|
actual, actualErr := loadXNormalizedPaths(tc.have, tc.haveX)
|
||||||
|
|
||||||
assert.Equal(t, tc.expected, actual)
|
assert.Equal(t, tc.expected, actual)
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,28 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !oidc.IsPushedAuthorizedRequest(requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)) {
|
||||||
|
if err = client.ValidatePKCEPolicy(requester); err != nil {
|
||||||
|
rfc := fosite.ErrorToRFC6749Error(err)
|
||||||
|
|
||||||
|
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the PKCE policy: %s", requester.GetID(), client.GetID(), rfc.WithExposeDebug(true).GetDescription())
|
||||||
|
|
||||||
|
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.ValidateResponseModePolicy(requester); err != nil {
|
||||||
|
rfc := fosite.ErrorToRFC6749Error(err)
|
||||||
|
|
||||||
|
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the Response Mode: %s", requester.GetID(), client.GetID(), rfc.WithExposeDebug(true).GetDescription())
|
||||||
|
|
||||||
|
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err = client.ValidatePKCEPolicy(requester); err != nil {
|
if err = client.ValidatePKCEPolicy(requester); err != nil {
|
||||||
rfc := fosite.ErrorToRFC6749Error(err)
|
rfc := fosite.ErrorToRFC6749Error(err)
|
||||||
|
|
||||||
|
@ -175,9 +197,19 @@ func OpenIDConnectPushedAuthorizationRequest(ctx *middlewares.AutheliaCtx, rw ht
|
||||||
if err = client.ValidatePKCEPolicy(requester); err != nil {
|
if err = client.ValidatePKCEPolicy(requester); err != nil {
|
||||||
rfc := fosite.ErrorToRFC6749Error(err)
|
rfc := fosite.ErrorToRFC6749Error(err)
|
||||||
|
|
||||||
ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' failed to validate the PKCE policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription())
|
ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' failed to validate the PKCE policy: %s", requester.GetID(), client.GetID(), rfc.WithExposeDebug(true).GetDescription())
|
||||||
|
|
||||||
ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err)
|
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.ValidateResponseModePolicy(requester); err != nil {
|
||||||
|
rfc := fosite.ErrorToRFC6749Error(err)
|
||||||
|
|
||||||
|
ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' failed to validate the Response Mode: %s", requester.GetID(), client.GetID(), rfc.WithExposeDebug(true).GetDescription())
|
||||||
|
|
||||||
|
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
|
||||||
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
||||||
var (
|
var (
|
||||||
w *webauthn.WebAuthn
|
w *webauthn.WebAuthn
|
||||||
user *model.WebauthnUser
|
user *model.WebAuthnUser
|
||||||
userSession session.UserSession
|
userSession session.UserSession
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
@ -94,7 +94,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
w *webauthn.WebAuthn
|
w *webauthn.WebAuthn
|
||||||
user *model.WebauthnUser
|
user *model.WebAuthnUser
|
||||||
|
|
||||||
userSession session.UserSession
|
userSession session.UserSession
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, "Primary", credential)
|
device := model.NewWebAuthnDeviceFromCredential(w.Config.RPID, userSession.Username, "Primary", credential)
|
||||||
|
|
||||||
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
||||||
var (
|
var (
|
||||||
w *webauthn.WebAuthn
|
w *webauthn.WebAuthn
|
||||||
user *model.WebauthnUser
|
user *model.WebAuthnUser
|
||||||
userSession session.UserSession
|
userSession session.UserSession
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
@ -134,7 +134,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
var (
|
var (
|
||||||
assertionResponse *protocol.ParsedCredentialAssertionData
|
assertionResponse *protocol.ParsedCredentialAssertionData
|
||||||
credential *webauthn.Credential
|
credential *webauthn.Credential
|
||||||
user *model.WebauthnUser
|
user *model.WebAuthnUser
|
||||||
)
|
)
|
||||||
|
|
||||||
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
|
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
|
||||||
|
|
|
@ -62,7 +62,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
||||||
{
|
{
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "webauthn",
|
Method: "webauthn",
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
|
@ -70,7 +70,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
||||||
{
|
{
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "webauthn",
|
Method: "webauthn",
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
|
@ -78,7 +78,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
||||||
{
|
{
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "mobile_push",
|
Method: "mobile_push",
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
|
@ -128,7 +128,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("registered webauthn", func(t *testing.T) {
|
t.Run("registered webauthn", func(t *testing.T) {
|
||||||
assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn)
|
assert.Equal(t, resp.api.HasWebAuthn, actualPreferences.HasWebAuthn)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("registered totp", func(t *testing.T) {
|
t.Run("registered totp", func(t *testing.T) {
|
||||||
|
@ -160,13 +160,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
},
|
},
|
||||||
api: &model.UserInfo{
|
api: &model.UserInfo{
|
||||||
Method: "totp",
|
Method: "totp",
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
},
|
},
|
||||||
config: &schema.Configuration{},
|
config: &schema.Configuration{},
|
||||||
|
@ -178,13 +178,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
},
|
},
|
||||||
api: &model.UserInfo{
|
api: &model.UserInfo{
|
||||||
Method: "mobile_push",
|
Method: "mobile_push",
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
},
|
},
|
||||||
config: &schema.Configuration{},
|
config: &schema.Configuration{},
|
||||||
|
@ -196,13 +196,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
},
|
},
|
||||||
api: &model.UserInfo{
|
api: &model.UserInfo{
|
||||||
Method: "totp",
|
Method: "totp",
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
},
|
},
|
||||||
config: &schema.Configuration{DuoAPI: schema.DuoAPIConfiguration{Disable: true}},
|
config: &schema.Configuration{DuoAPI: schema.DuoAPIConfiguration{Disable: true}},
|
||||||
|
@ -214,13 +214,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
},
|
},
|
||||||
api: &model.UserInfo{
|
api: &model.UserInfo{
|
||||||
Method: "webauthn",
|
Method: "webauthn",
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
},
|
},
|
||||||
config: &schema.Configuration{
|
config: &schema.Configuration{
|
||||||
|
@ -236,13 +236,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
||||||
db: model.UserInfo{
|
db: model.UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
},
|
},
|
||||||
api: &model.UserInfo{
|
api: &model.UserInfo{
|
||||||
Method: "totp",
|
Method: "totp",
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
},
|
},
|
||||||
config: &schema.Configuration{},
|
config: &schema.Configuration{},
|
||||||
|
@ -322,7 +322,7 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("registered webauthn", func(t *testing.T) {
|
t.Run("registered webauthn", func(t *testing.T) {
|
||||||
assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn)
|
assert.Equal(t, resp.api.HasWebAuthn, actualPreferences.HasWebAuthn)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("registered totp", func(t *testing.T) {
|
t.Run("registered totp", func(t *testing.T) {
|
||||||
|
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/session"
|
"github.com/authelia/authelia/v4/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getWebAuthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSession) (user *model.WebauthnUser, err error) {
|
func getWebAuthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSession) (user *model.WebAuthnUser, err error) {
|
||||||
user = &model.WebauthnUser{
|
user = &model.WebAuthnUser{
|
||||||
Username: userSession.Username,
|
Username: userSession.Username,
|
||||||
DisplayName: userSession.DisplayName,
|
DisplayName: userSession.DisplayName,
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
||||||
DisplayName: "John Smith",
|
DisplayName: "John Smith",
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebauthnDevice{
|
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{
|
||||||
{
|
{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
RPID: "https://example.com",
|
RPID: "https://example.com",
|
||||||
|
@ -106,7 +106,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
|
||||||
Username: "john",
|
Username: "john",
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebauthnDevice{
|
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{
|
||||||
{
|
{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
RPID: "https://example.com",
|
RPID: "https://example.com",
|
||||||
|
|
|
@ -50,7 +50,7 @@ func (ctx *AutheliaCtx) AvailableSecondFactorMethods() (methods []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Configuration.Webauthn.Disable {
|
if !ctx.Configuration.Webauthn.Disable {
|
||||||
methods = append(methods, model.SecondFactorMethodWebauthn)
|
methods = append(methods, model.SecondFactorMethodWebAuthn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Configuration.DuoAPI.Disable {
|
if !ctx.Configuration.DuoAPI.Disable {
|
||||||
|
|
|
@ -235,15 +235,15 @@ func TestShouldReturnCorrectSecondFactorMethods(t *testing.T) {
|
||||||
|
|
||||||
mock.Ctx.Configuration.DuoAPI.Disable = true
|
mock.Ctx.Configuration.DuoAPI.Disable = true
|
||||||
|
|
||||||
assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebauthn}, mock.Ctx.AvailableSecondFactorMethods())
|
assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebAuthn}, mock.Ctx.AvailableSecondFactorMethods())
|
||||||
|
|
||||||
mock.Ctx.Configuration.DuoAPI.Disable = false
|
mock.Ctx.Configuration.DuoAPI.Disable = false
|
||||||
|
|
||||||
assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebauthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())
|
assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebAuthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())
|
||||||
|
|
||||||
mock.Ctx.Configuration.TOTP.Disable = true
|
mock.Ctx.Configuration.TOTP.Disable = true
|
||||||
|
|
||||||
assert.Equal(t, []string{model.SecondFactorMethodWebauthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())
|
assert.Equal(t, []string{model.SecondFactorMethodWebAuthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())
|
||||||
|
|
||||||
mock.Ctx.Configuration.Webauthn.Disable = true
|
mock.Ctx.Configuration.Webauthn.Disable = true
|
||||||
|
|
||||||
|
|
|
@ -421,10 +421,10 @@ func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifiers(arg0 interface{}) *
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadWebauthnDevices mocks base method.
|
// LoadWebauthnDevices mocks base method.
|
||||||
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebauthnDevice, error) {
|
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebAuthnDevice, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "LoadWebauthnDevices", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "LoadWebauthnDevices", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].([]model.WebauthnDevice)
|
ret0, _ := ret[0].([]model.WebAuthnDevice)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
@ -436,10 +436,10 @@ func (mr *MockStorageMockRecorder) LoadWebauthnDevices(arg0, arg1, arg2 interfac
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadWebauthnDevicesByUsername mocks base method.
|
// LoadWebauthnDevicesByUsername mocks base method.
|
||||||
func (m *MockStorage) LoadWebauthnDevicesByUsername(arg0 context.Context, arg1 string) ([]model.WebauthnDevice, error) {
|
func (m *MockStorage) LoadWebauthnDevicesByUsername(arg0 context.Context, arg1 string) ([]model.WebAuthnDevice, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "LoadWebauthnDevicesByUsername", arg0, arg1)
|
ret := m.ctrl.Call(m, "LoadWebauthnDevicesByUsername", arg0, arg1)
|
||||||
ret0, _ := ret[0].([]model.WebauthnDevice)
|
ret0, _ := ret[0].([]model.WebAuthnDevice)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
@ -690,7 +690,7 @@ func (mr *MockStorageMockRecorder) SaveUserOpaqueIdentifier(arg0, arg1 interface
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveWebauthnDevice mocks base method.
|
// SaveWebauthnDevice mocks base method.
|
||||||
func (m *MockStorage) SaveWebauthnDevice(arg0 context.Context, arg1 model.WebauthnDevice) error {
|
func (m *MockStorage) SaveWebauthnDevice(arg0 context.Context, arg1 model.WebAuthnDevice) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SaveWebauthnDevice", arg0, arg1)
|
ret := m.ctrl.Call(m, "SaveWebauthnDevice", arg0, arg1)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
|
|
@ -15,8 +15,8 @@ const (
|
||||||
// SecondFactorMethodTOTP method using Time-Based One-Time Password applications like Google Authenticator.
|
// SecondFactorMethodTOTP method using Time-Based One-Time Password applications like Google Authenticator.
|
||||||
SecondFactorMethodTOTP = "totp"
|
SecondFactorMethodTOTP = "totp"
|
||||||
|
|
||||||
// SecondFactorMethodWebauthn method using Webauthn devices like YubiKey's.
|
// SecondFactorMethodWebAuthn method using WebAuthn devices like YubiKey's.
|
||||||
SecondFactorMethodWebauthn = "webauthn"
|
SecondFactorMethodWebAuthn = "webauthn"
|
||||||
|
|
||||||
// SecondFactorMethodDuo method using Duo application to receive push notifications.
|
// SecondFactorMethodDuo method using Duo application to receive push notifications.
|
||||||
SecondFactorMethodDuo = "mobile_push"
|
SecondFactorMethodDuo = "mobile_push"
|
||||||
|
|
|
@ -25,9 +25,12 @@ type TOTPConfiguration struct {
|
||||||
Secret []byte `db:"secret" json:"-"`
|
Secret []byte `db:"secret" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastUsed provides LastUsedAt as a *time.Time instead of sql.NullTime.
|
||||||
func (c *TOTPConfiguration) LastUsed() *time.Time {
|
func (c *TOTPConfiguration) LastUsed() *time.Time {
|
||||||
if c.LastUsedAt.Valid {
|
if c.LastUsedAt.Valid {
|
||||||
return &c.LastUsedAt.Time
|
value := time.Unix(c.LastUsedAt.Time.Unix(), int64(c.LastUsedAt.Time.Nanosecond()))
|
||||||
|
|
||||||
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -73,9 +76,9 @@ func (c *TOTPConfiguration) Image(width, height int) (img image.Image, err error
|
||||||
return key.Image(width, height)
|
return key.Image(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalYAML marshals this model into YAML.
|
// ToData converts this TOTPConfiguration into the data format for exporting etc.
|
||||||
func (c *TOTPConfiguration) MarshalYAML() (any, error) {
|
func (c *TOTPConfiguration) ToData() TOTPConfigurationData {
|
||||||
o := TOTPConfigurationData{
|
return TOTPConfigurationData{
|
||||||
CreatedAt: c.CreatedAt,
|
CreatedAt: c.CreatedAt,
|
||||||
LastUsedAt: c.LastUsed(),
|
LastUsedAt: c.LastUsed(),
|
||||||
Username: c.Username,
|
Username: c.Username,
|
||||||
|
@ -85,8 +88,11 @@ func (c *TOTPConfiguration) MarshalYAML() (any, error) {
|
||||||
Period: c.Period,
|
Period: c.Period,
|
||||||
Secret: base64.StdEncoding.EncodeToString(c.Secret),
|
Secret: base64.StdEncoding.EncodeToString(c.Secret),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return yaml.Marshal(o)
|
// MarshalYAML marshals this model into YAML.
|
||||||
|
func (c *TOTPConfiguration) MarshalYAML() (any, error) {
|
||||||
|
return c.ToData(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML unmarshalls YAML into this model.
|
// UnmarshalYAML unmarshalls YAML into this model.
|
||||||
|
@ -127,7 +133,30 @@ type TOTPConfigurationData struct {
|
||||||
Secret string `yaml:"secret"`
|
Secret string `yaml:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TOTPConfigurationDataExport represents a TOTPConfiguration export file.
|
||||||
|
type TOTPConfigurationDataExport struct {
|
||||||
|
TOTPConfigurations []TOTPConfigurationData `yaml:"totp_configurations"`
|
||||||
|
}
|
||||||
|
|
||||||
// TOTPConfigurationExport represents a TOTPConfiguration export file.
|
// TOTPConfigurationExport represents a TOTPConfiguration export file.
|
||||||
type TOTPConfigurationExport struct {
|
type TOTPConfigurationExport struct {
|
||||||
TOTPConfigurations []TOTPConfiguration `yaml:"totp_configurations"`
|
TOTPConfigurations []TOTPConfiguration `yaml:"totp_configurations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToData converts this TOTPConfigurationExport into a TOTPConfigurationDataExport.
|
||||||
|
func (export TOTPConfigurationExport) ToData() TOTPConfigurationDataExport {
|
||||||
|
data := TOTPConfigurationDataExport{
|
||||||
|
TOTPConfigurations: make([]TOTPConfigurationData, len(export.TOTPConfigurations)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, config := range export.TOTPConfigurations {
|
||||||
|
data.TOTPConfigurations[i] = config.ToData()
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalYAML marshals this model into YAML.
|
||||||
|
func (export TOTPConfigurationExport) MarshalYAML() (any, error) {
|
||||||
|
return export.ToData(), nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -75,3 +78,62 @@ func TestShouldReturnImage(t *testing.T) {
|
||||||
assert.Equal(t, 41, img.Bounds().Dx())
|
assert.Equal(t, 41, img.Bounds().Dx())
|
||||||
assert.Equal(t, 41, img.Bounds().Dy())
|
assert.Equal(t, 41, img.Bounds().Dy())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTOTPConfigurationImportExport(t *testing.T) {
|
||||||
|
have := TOTPConfigurationExport{
|
||||||
|
TOTPConfigurations: []TOTPConfiguration{
|
||||||
|
{
|
||||||
|
ID: 0,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
LastUsedAt: sql.NullTime{Valid: false},
|
||||||
|
Username: "john",
|
||||||
|
Issuer: "example",
|
||||||
|
Algorithm: "SHA1",
|
||||||
|
Digits: 6,
|
||||||
|
Period: 30,
|
||||||
|
Secret: MustRead(80),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
LastUsedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
|
Username: "abc",
|
||||||
|
Issuer: "example2",
|
||||||
|
Algorithm: "SHA512",
|
||||||
|
Digits: 8,
|
||||||
|
Period: 90,
|
||||||
|
Secret: MustRead(120),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(&have)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
imported := TOTPConfigurationExport{}
|
||||||
|
|
||||||
|
require.NoError(t, yaml.Unmarshal(out, &imported))
|
||||||
|
|
||||||
|
require.Equal(t, len(have.TOTPConfigurations), len(imported.TOTPConfigurations))
|
||||||
|
|
||||||
|
for i, actual := range imported.TOTPConfigurations {
|
||||||
|
t.Run(actual.Username, func(t *testing.T) {
|
||||||
|
expected := have.TOTPConfigurations[i]
|
||||||
|
|
||||||
|
if expected.ID != 0 {
|
||||||
|
assert.NotEqual(t, expected.ID, actual.ID)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected.Username, actual.Username)
|
||||||
|
assert.Equal(t, expected.Issuer, actual.Issuer)
|
||||||
|
assert.Equal(t, expected.Algorithm, actual.Algorithm)
|
||||||
|
assert.Equal(t, expected.Digits, actual.Digits)
|
||||||
|
assert.Equal(t, expected.Period, actual.Period)
|
||||||
|
assert.WithinDuration(t, expected.CreatedAt, actual.CreatedAt, time.Second)
|
||||||
|
assert.WithinDuration(t, expected.LastUsedAt.Time, actual.LastUsedAt.Time, time.Second)
|
||||||
|
assert.Equal(t, expected.LastUsedAt.Valid, actual.LastUsedAt.Valid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,21 +1,11 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
|
||||||
args := fosite.Arguments{"abc", "123"}
|
|
||||||
|
|
||||||
x := StringSlicePipeDelimited(args)
|
|
||||||
|
|
||||||
fmt.Println(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDatabaseModelTypeIP(t *testing.T) {
|
func TestDatabaseModelTypeIP(t *testing.T) {
|
||||||
ip := IP{}
|
ip := IP{}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ type UserInfo struct {
|
||||||
// True if a TOTP device has been registered.
|
// True if a TOTP device has been registered.
|
||||||
HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"`
|
HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"`
|
||||||
|
|
||||||
// True if a Webauthn device has been registered.
|
// True if a WebAuthn device has been registered.
|
||||||
HasWebauthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
|
HasWebAuthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
|
||||||
|
|
||||||
// True if a duo device has been configured as the preferred.
|
// True if a duo device has been configured as the preferred.
|
||||||
HasDuo bool `db:"has_duo" json:"has_duo" valid:"required"`
|
HasDuo bool `db:"has_duo" json:"has_duo" valid:"required"`
|
||||||
|
@ -31,7 +31,7 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string, fallback strin
|
||||||
|
|
||||||
before := i.Method
|
before := i.Method
|
||||||
|
|
||||||
totp, webauthn, duo := utils.IsStringInSlice(SecondFactorMethodTOTP, methods), utils.IsStringInSlice(SecondFactorMethodWebauthn, methods), utils.IsStringInSlice(SecondFactorMethodDuo, methods)
|
totp, webauthn, duo := utils.IsStringInSlice(SecondFactorMethodTOTP, methods), utils.IsStringInSlice(SecondFactorMethodWebAuthn, methods), utils.IsStringInSlice(SecondFactorMethodDuo, methods)
|
||||||
|
|
||||||
if i.Method == "" && utils.IsStringInSlice(fallback, methods) {
|
if i.Method == "" && utils.IsStringInSlice(fallback, methods) {
|
||||||
i.Method = fallback
|
i.Method = fallback
|
||||||
|
@ -50,8 +50,8 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
|
||||||
switch {
|
switch {
|
||||||
case i.HasTOTP && totp:
|
case i.HasTOTP && totp:
|
||||||
i.Method = SecondFactorMethodTOTP
|
i.Method = SecondFactorMethodTOTP
|
||||||
case i.HasWebauthn && webauthn:
|
case i.HasWebAuthn && webauthn:
|
||||||
i.Method = SecondFactorMethodWebauthn
|
i.Method = SecondFactorMethodWebAuthn
|
||||||
case i.HasDuo && duo:
|
case i.HasDuo && duo:
|
||||||
i.Method = SecondFactorMethodDuo
|
i.Method = SecondFactorMethodDuo
|
||||||
case fallback != "" && utils.IsStringInSlice(fallback, methods):
|
case fallback != "" && utils.IsStringInSlice(fallback, methods):
|
||||||
|
@ -59,7 +59,7 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
|
||||||
case totp:
|
case totp:
|
||||||
i.Method = SecondFactorMethodTOTP
|
i.Method = SecondFactorMethodTOTP
|
||||||
case webauthn:
|
case webauthn:
|
||||||
i.Method = SecondFactorMethodWebauthn
|
i.Method = SecondFactorMethodWebAuthn
|
||||||
case duo:
|
case duo:
|
||||||
i.Method = SecondFactorMethodDuo
|
i.Method = SecondFactorMethodDuo
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
|
|
||||||
has := ""
|
has := ""
|
||||||
|
|
||||||
if have.HasTOTP || have.HasDuo || have.HasWebauthn {
|
if have.HasTOTP || have.HasDuo || have.HasWebAuthn {
|
||||||
has += " has"
|
has += " has"
|
||||||
|
|
||||||
if have.HasTOTP {
|
if have.HasTOTP {
|
||||||
|
@ -31,8 +31,8 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
has += " " + SecondFactorMethodDuo
|
has += " " + SecondFactorMethodDuo
|
||||||
}
|
}
|
||||||
|
|
||||||
if have.HasWebauthn {
|
if have.HasWebAuthn {
|
||||||
has += " " + SecondFactorMethodWebauthn
|
has += " " + SecondFactorMethodWebAuthn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,60 +62,60 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP},
|
methods: []string{SecondFactorMethodTOTP},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP},
|
methods: []string{SecondFactorMethodTOTP},
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -125,15 +125,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn},
|
methods: []string{SecondFactorMethodWebAuthn},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -141,31 +141,31 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: false,
|
changed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -173,15 +173,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -189,13 +189,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -205,13 +205,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: nil,
|
methods: nil,
|
||||||
changed: false,
|
changed: false,
|
||||||
|
@ -221,15 +221,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
fallback: SecondFactorMethodDuo,
|
fallback: SecondFactorMethodDuo,
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
|
@ -238,15 +238,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn},
|
||||||
fallback: SecondFactorMethodDuo,
|
fallback: SecondFactorMethodDuo,
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
|
@ -255,15 +255,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -271,30 +271,30 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
fallback: SecondFactorMethodWebauthn,
|
fallback: SecondFactorMethodWebAuthn,
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo},
|
||||||
fallback: SecondFactorMethodDuo,
|
fallback: SecondFactorMethodDuo,
|
||||||
|
|
|
@ -17,15 +17,15 @@ const (
|
||||||
attestationTypeFIDOU2F = "fido-u2f"
|
attestationTypeFIDOU2F = "fido-u2f"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebauthnUser is an object to represent a user for the Webauthn lib.
|
// WebAuthnUser is an object to represent a user for the WebAuthn lib.
|
||||||
type WebauthnUser struct {
|
type WebAuthnUser struct {
|
||||||
Username string
|
Username string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
Devices []WebauthnDevice
|
Devices []WebAuthnDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasFIDOU2F returns true if the user has any attestation type `fido-u2f` devices.
|
// HasFIDOU2F returns true if the user has any attestation type `fido-u2f` devices.
|
||||||
func (w WebauthnUser) HasFIDOU2F() bool {
|
func (w WebAuthnUser) HasFIDOU2F() bool {
|
||||||
for _, c := range w.Devices {
|
for _, c := range w.Devices {
|
||||||
if c.AttestationType == attestationTypeFIDOU2F {
|
if c.AttestationType == attestationTypeFIDOU2F {
|
||||||
return true
|
return true
|
||||||
|
@ -36,27 +36,27 @@ func (w WebauthnUser) HasFIDOU2F() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnID implements the webauthn.User interface.
|
// WebAuthnID implements the webauthn.User interface.
|
||||||
func (w WebauthnUser) WebAuthnID() []byte {
|
func (w WebAuthnUser) WebAuthnID() []byte {
|
||||||
return []byte(w.Username)
|
return []byte(w.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnName implements the webauthn.User interface.
|
// WebAuthnName implements the webauthn.User interface.
|
||||||
func (w WebauthnUser) WebAuthnName() string {
|
func (w WebAuthnUser) WebAuthnName() string {
|
||||||
return w.Username
|
return w.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnDisplayName implements the webauthn.User interface.
|
// WebAuthnDisplayName implements the webauthn.User interface.
|
||||||
func (w WebauthnUser) WebAuthnDisplayName() string {
|
func (w WebAuthnUser) WebAuthnDisplayName() string {
|
||||||
return w.DisplayName
|
return w.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnIcon implements the webauthn.User interface.
|
// WebAuthnIcon implements the webauthn.User interface.
|
||||||
func (w WebauthnUser) WebAuthnIcon() string {
|
func (w WebAuthnUser) WebAuthnIcon() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnCredentials implements the webauthn.User interface.
|
// WebAuthnCredentials implements the webauthn.User interface.
|
||||||
func (w WebauthnUser) WebAuthnCredentials() (credentials []webauthn.Credential) {
|
func (w WebAuthnUser) WebAuthnCredentials() (credentials []webauthn.Credential) {
|
||||||
credentials = make([]webauthn.Credential, len(w.Devices))
|
credentials = make([]webauthn.Credential, len(w.Devices))
|
||||||
|
|
||||||
var credential webauthn.Credential
|
var credential webauthn.Credential
|
||||||
|
@ -96,7 +96,7 @@ func (w WebauthnUser) WebAuthnCredentials() (credentials []webauthn.Credential)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebAuthnCredentialDescriptors decodes the users credentials into protocol.CredentialDescriptor's.
|
// WebAuthnCredentialDescriptors decodes the users credentials into protocol.CredentialDescriptor's.
|
||||||
func (w WebauthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) {
|
func (w WebAuthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) {
|
||||||
credentials := w.WebAuthnCredentials()
|
credentials := w.WebAuthnCredentials()
|
||||||
|
|
||||||
descriptors = make([]protocol.CredentialDescriptor, len(credentials))
|
descriptors = make([]protocol.CredentialDescriptor, len(credentials))
|
||||||
|
@ -108,15 +108,15 @@ func (w WebauthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.Cr
|
||||||
return descriptors
|
return descriptors
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebauthnDeviceFromCredential creates a WebauthnDevice from a webauthn.Credential.
|
// NewWebAuthnDeviceFromCredential creates a WebAuthnDevice from a webauthn.Credential.
|
||||||
func NewWebauthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebauthnDevice) {
|
func NewWebAuthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebAuthnDevice) {
|
||||||
transport := make([]string, len(credential.Transport))
|
transport := make([]string, len(credential.Transport))
|
||||||
|
|
||||||
for i, t := range credential.Transport {
|
for i, t := range credential.Transport {
|
||||||
transport[i] = string(t)
|
transport[i] = string(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
device = WebauthnDevice{
|
device = WebAuthnDevice{
|
||||||
RPID: rpid,
|
RPID: rpid,
|
||||||
Username: username,
|
Username: username,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
|
@ -137,8 +137,8 @@ func NewWebauthnDeviceFromCredential(rpid, username, description string, credent
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebauthnDevice represents a Webauthn Device in the database storage.
|
// WebAuthnDevice represents a WebAuthn Device in the database storage.
|
||||||
type WebauthnDevice struct {
|
type WebAuthnDevice struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
LastUsedAt sql.NullTime `db:"last_used_at"`
|
LastUsedAt sql.NullTime `db:"last_used_at"`
|
||||||
|
@ -154,8 +154,8 @@ type WebauthnDevice struct {
|
||||||
CloneWarning bool `db:"clone_warning"`
|
CloneWarning bool `db:"clone_warning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSignInInfo adjusts the values of the WebauthnDevice after a sign in.
|
// UpdateSignInInfo adjusts the values of the WebAuthnDevice after a sign in.
|
||||||
func (d *WebauthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
|
func (d *WebAuthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
|
||||||
d.LastUsedAt = sql.NullTime{Time: now, Valid: true}
|
d.LastUsedAt = sql.NullTime{Time: now, Valid: true}
|
||||||
|
|
||||||
d.SignCount = signCount
|
d.SignCount = signCount
|
||||||
|
@ -172,17 +172,20 @@ func (d *WebauthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *WebauthnDevice) LastUsed() *time.Time {
|
// LastUsed provides LastUsedAt as a *time.Time instead of sql.NullTime.
|
||||||
|
func (d *WebAuthnDevice) LastUsed() *time.Time {
|
||||||
if d.LastUsedAt.Valid {
|
if d.LastUsedAt.Valid {
|
||||||
return &d.LastUsedAt.Time
|
value := time.Unix(d.LastUsedAt.Time.Unix(), int64(d.LastUsedAt.Time.Nanosecond()))
|
||||||
|
|
||||||
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalYAML marshals this model into YAML.
|
// ToData converts this WebAuthnDevice into the data format for exporting etc.
|
||||||
func (d *WebauthnDevice) MarshalYAML() (any, error) {
|
func (d *WebAuthnDevice) ToData() WebAuthnDeviceData {
|
||||||
o := WebauthnDeviceData{
|
return WebAuthnDeviceData{
|
||||||
CreatedAt: d.CreatedAt,
|
CreatedAt: d.CreatedAt,
|
||||||
LastUsedAt: d.LastUsed(),
|
LastUsedAt: d.LastUsed(),
|
||||||
RPID: d.RPID,
|
RPID: d.RPID,
|
||||||
|
@ -196,13 +199,16 @@ func (d *WebauthnDevice) MarshalYAML() (any, error) {
|
||||||
SignCount: d.SignCount,
|
SignCount: d.SignCount,
|
||||||
CloneWarning: d.CloneWarning,
|
CloneWarning: d.CloneWarning,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return yaml.Marshal(o)
|
// MarshalYAML marshals this model into YAML.
|
||||||
|
func (d *WebAuthnDevice) MarshalYAML() (any, error) {
|
||||||
|
return d.ToData(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML unmarshalls YAML into this model.
|
// UnmarshalYAML unmarshalls YAML into this model.
|
||||||
func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||||
o := &WebauthnDeviceData{}
|
o := &WebAuthnDeviceData{}
|
||||||
|
|
||||||
if err = value.Decode(o); err != nil {
|
if err = value.Decode(o); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -246,8 +252,8 @@ func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebauthnDeviceData represents a Webauthn Device in the database storage.
|
// WebAuthnDeviceData represents a WebAuthn Device in the database storage.
|
||||||
type WebauthnDeviceData struct {
|
type WebAuthnDeviceData struct {
|
||||||
CreatedAt time.Time `yaml:"created_at"`
|
CreatedAt time.Time `yaml:"created_at"`
|
||||||
LastUsedAt *time.Time `yaml:"last_used_at"`
|
LastUsedAt *time.Time `yaml:"last_used_at"`
|
||||||
RPID string `yaml:"rpid"`
|
RPID string `yaml:"rpid"`
|
||||||
|
@ -262,7 +268,30 @@ type WebauthnDeviceData struct {
|
||||||
CloneWarning bool `yaml:"clone_warning"`
|
CloneWarning bool `yaml:"clone_warning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebauthnDeviceExport represents a WebauthnDevice export file.
|
// WebAuthnDeviceExport represents a WebAuthnDevice export file.
|
||||||
type WebauthnDeviceExport struct {
|
type WebAuthnDeviceExport struct {
|
||||||
WebauthnDevices []WebauthnDevice `yaml:"webauthn_devices"`
|
WebAuthnDevices []WebAuthnDevice `yaml:"webauthn_devices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebAuthnDeviceDataExport represents a WebAuthnDevice export file.
|
||||||
|
type WebAuthnDeviceDataExport struct {
|
||||||
|
WebAuthnDevices []WebAuthnDeviceData `yaml:"webauthn_devices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToData converts this WebAuthnDeviceExport into a WebAuthnDeviceDataExport.
|
||||||
|
func (export WebAuthnDeviceExport) ToData() WebAuthnDeviceDataExport {
|
||||||
|
data := WebAuthnDeviceDataExport{
|
||||||
|
WebAuthnDevices: make([]WebAuthnDeviceData, len(export.WebAuthnDevices)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, device := range export.WebAuthnDevices {
|
||||||
|
data.WebAuthnDevices[i] = device.ToData()
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalYAML marshals this model into YAML.
|
||||||
|
func (export WebAuthnDeviceExport) MarshalYAML() (any, error) {
|
||||||
|
return export.ToData(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebAuthnDeviceImportExport(t *testing.T) {
|
||||||
|
have := WebAuthnDeviceExport{
|
||||||
|
WebAuthnDevices: []WebAuthnDevice{
|
||||||
|
{
|
||||||
|
ID: 0,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
LastUsedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
|
RPID: "example",
|
||||||
|
Username: "john",
|
||||||
|
Description: "akey",
|
||||||
|
KID: NewBase64(MustRead(20)),
|
||||||
|
PublicKey: MustRead(128),
|
||||||
|
AttestationType: "fido-u2f",
|
||||||
|
Transport: "",
|
||||||
|
AAGUID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||||
|
SignCount: 20,
|
||||||
|
CloneWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 0,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
LastUsedAt: sql.NullTime{Valid: false},
|
||||||
|
RPID: "example2",
|
||||||
|
Username: "john2",
|
||||||
|
Description: "bkey",
|
||||||
|
KID: NewBase64(MustRead(60)),
|
||||||
|
PublicKey: MustRead(64),
|
||||||
|
AttestationType: "packed",
|
||||||
|
Transport: "",
|
||||||
|
AAGUID: uuid.NullUUID{Valid: false},
|
||||||
|
SignCount: 30,
|
||||||
|
CloneWarning: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(&have)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
imported := WebAuthnDeviceExport{}
|
||||||
|
|
||||||
|
require.NoError(t, yaml.Unmarshal(out, &imported))
|
||||||
|
require.Equal(t, len(have.WebAuthnDevices), len(imported.WebAuthnDevices))
|
||||||
|
|
||||||
|
for i, actual := range imported.WebAuthnDevices {
|
||||||
|
t.Run(actual.Description, func(t *testing.T) {
|
||||||
|
expected := have.WebAuthnDevices[i]
|
||||||
|
|
||||||
|
assert.Equal(t, expected.KID, actual.KID)
|
||||||
|
assert.Equal(t, expected.PublicKey, actual.PublicKey)
|
||||||
|
assert.Equal(t, expected.SignCount, actual.SignCount)
|
||||||
|
assert.Equal(t, expected.AttestationType, actual.AttestationType)
|
||||||
|
assert.Equal(t, expected.RPID, actual.RPID)
|
||||||
|
assert.Equal(t, expected.AAGUID.Valid, actual.AAGUID.Valid)
|
||||||
|
assert.Equal(t, expected.AAGUID.UUID, actual.AAGUID.UUID)
|
||||||
|
assert.WithinDuration(t, expected.CreatedAt, actual.CreatedAt, time.Second)
|
||||||
|
assert.WithinDuration(t, expected.LastUsedAt.Time, actual.LastUsedAt.Time, time.Second)
|
||||||
|
assert.Equal(t, expected.LastUsedAt.Valid, actual.LastUsedAt.Valid)
|
||||||
|
assert.Equal(t, expected.CloneWarning, actual.CloneWarning)
|
||||||
|
assert.Equal(t, expected.Description, actual.Description)
|
||||||
|
assert.Equal(t, expected.Username, actual.Username)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustRead(n int) []byte {
|
||||||
|
data := make([]byte, n)
|
||||||
|
|
||||||
|
if _, err := rand.Read(data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/x/errorsx"
|
"github.com/ory/x/errorsx"
|
||||||
|
|
||||||
|
@ -30,7 +28,7 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
|
||||||
RedirectURIs: config.RedirectURIs,
|
RedirectURIs: config.RedirectURIs,
|
||||||
GrantTypes: config.GrantTypes,
|
GrantTypes: config.GrantTypes,
|
||||||
ResponseTypes: config.ResponseTypes,
|
ResponseTypes: config.ResponseTypes,
|
||||||
ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault},
|
ResponseModes: []fosite.ResponseModeType{},
|
||||||
|
|
||||||
EnforcePAR: config.EnforcePAR,
|
EnforcePAR: config.EnforcePAR,
|
||||||
|
|
||||||
|
@ -73,21 +71,44 @@ func (c *Client) ValidatePKCEPolicy(r fosite.Requester) (err error) {
|
||||||
|
|
||||||
// ValidatePARPolicy is a helper function to validate additional policy constraints on a per-client basis.
|
// ValidatePARPolicy is a helper function to validate additional policy constraints on a per-client basis.
|
||||||
func (c *Client) ValidatePARPolicy(r fosite.Requester, prefix string) (err error) {
|
func (c *Client) ValidatePARPolicy(r fosite.Requester, prefix string) (err error) {
|
||||||
form := r.GetRequestForm()
|
|
||||||
|
|
||||||
if c.EnforcePAR {
|
if c.EnforcePAR {
|
||||||
if requestURI := form.Get(FormParameterRequestURI); !strings.HasPrefix(requestURI, prefix) {
|
if !IsPushedAuthorizedRequest(r, prefix) {
|
||||||
if requestURI == "" {
|
switch requestURI := r.GetRequestForm().Get(FormParameterRequestURI); requestURI {
|
||||||
|
case "":
|
||||||
return errorsx.WithStack(ErrPAREnforcedClientMissingPAR.WithDebug("The request_uri parameter was empty."))
|
return errorsx.WithStack(ErrPAREnforcedClientMissingPAR.WithDebug("The request_uri parameter was empty."))
|
||||||
}
|
default:
|
||||||
|
|
||||||
return errorsx.WithStack(ErrPAREnforcedClientMissingPAR.WithDebugf("The request_uri parameter '%s' is malformed.", requestURI))
|
return errorsx.WithStack(ErrPAREnforcedClientMissingPAR.WithDebugf("The request_uri parameter '%s' is malformed.", requestURI))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateResponseModePolicy is an additional check to the response mode parameter to ensure if it's omitted that the
|
||||||
|
// default response mode for the fosite.AuthorizeRequester is permitted.
|
||||||
|
func (c *Client) ValidateResponseModePolicy(r fosite.AuthorizeRequester) (err error) {
|
||||||
|
if r.GetResponseMode() != fosite.ResponseModeDefault {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := r.GetDefaultResponseMode()
|
||||||
|
|
||||||
|
modes := c.GetResponseModes()
|
||||||
|
|
||||||
|
if len(modes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mode := range modes {
|
||||||
|
if m == mode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorsx.WithStack(fosite.ErrUnsupportedResponseMode.WithHintf(`The request omitted the response_mode making the default response_mode "%s" based on the other authorization request parameters but registered OAuth 2.0 client doesn't support this response_mode`, m))
|
||||||
|
}
|
||||||
|
|
||||||
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
|
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
|
||||||
func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool {
|
func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool {
|
||||||
if level == authentication.NotAuthenticated {
|
if level == authentication.NotAuthenticated {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
|
@ -19,8 +20,7 @@ func TestNewClient(t *testing.T) {
|
||||||
assert.Equal(t, "", blankClient.ID)
|
assert.Equal(t, "", blankClient.ID)
|
||||||
assert.Equal(t, "", blankClient.Description)
|
assert.Equal(t, "", blankClient.Description)
|
||||||
assert.Equal(t, "", blankClient.Description)
|
assert.Equal(t, "", blankClient.Description)
|
||||||
require.Len(t, blankClient.ResponseModes, 1)
|
assert.Len(t, blankClient.ResponseModes, 0)
|
||||||
assert.Equal(t, fosite.ResponseModeDefault, blankClient.ResponseModes[0])
|
|
||||||
|
|
||||||
exampleConfig := schema.OpenIDConnectClientConfiguration{
|
exampleConfig := schema.OpenIDConnectClientConfiguration{
|
||||||
ID: "myapp",
|
ID: "myapp",
|
||||||
|
@ -36,11 +36,10 @@ func TestNewClient(t *testing.T) {
|
||||||
|
|
||||||
exampleClient := NewClient(exampleConfig)
|
exampleClient := NewClient(exampleConfig)
|
||||||
assert.Equal(t, "myapp", exampleClient.ID)
|
assert.Equal(t, "myapp", exampleClient.ID)
|
||||||
require.Len(t, exampleClient.ResponseModes, 4)
|
require.Len(t, exampleClient.ResponseModes, 3)
|
||||||
assert.Equal(t, fosite.ResponseModeDefault, exampleClient.ResponseModes[0])
|
assert.Equal(t, fosite.ResponseModeFormPost, exampleClient.ResponseModes[0])
|
||||||
assert.Equal(t, fosite.ResponseModeFormPost, exampleClient.ResponseModes[1])
|
assert.Equal(t, fosite.ResponseModeQuery, exampleClient.ResponseModes[1])
|
||||||
assert.Equal(t, fosite.ResponseModeQuery, exampleClient.ResponseModes[2])
|
assert.Equal(t, fosite.ResponseModeFragment, exampleClient.ResponseModes[2])
|
||||||
assert.Equal(t, fosite.ResponseModeFragment, exampleClient.ResponseModes[3])
|
|
||||||
assert.Equal(t, authorization.TwoFactor, exampleClient.Policy)
|
assert.Equal(t, authorization.TwoFactor, exampleClient.Policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +225,7 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
expected string
|
expected string
|
||||||
r *fosite.Request
|
r *fosite.Request
|
||||||
err string
|
err string
|
||||||
|
desc string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest",
|
"ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest",
|
||||||
|
@ -235,6 +235,7 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
"",
|
"",
|
||||||
&fosite.Request{},
|
&fosite.Request{},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ShouldEnforcePKCEAndErrorOnNonPKCERequest",
|
"ShouldEnforcePKCEAndErrorOnNonPKCERequest",
|
||||||
|
@ -244,6 +245,7 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
"",
|
"",
|
||||||
&fosite.Request{},
|
&fosite.Request{},
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
|
"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. 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.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ShouldEnforcePKCEAndNotErrorOnPKCERequest",
|
"ShouldEnforcePKCEAndNotErrorOnPKCERequest",
|
||||||
|
@ -253,6 +255,7 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
"",
|
"",
|
||||||
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
|
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest",
|
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest",
|
||||||
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
||||||
|
@ -261,6 +264,7 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
"S256",
|
"S256",
|
||||||
&fosite.Request{},
|
&fosite.Request{},
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
|
"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must include a code_challenge when performing the authorize code flow, but it is missing. The server is configured in a way that enforces PKCE for this client.",
|
||||||
},
|
},
|
||||||
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod",
|
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod",
|
||||||
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
||||||
|
@ -269,6 +273,7 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
"S256",
|
"S256",
|
||||||
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
|
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
|
"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Client must use code_challenge_method=S256, is not allowed. The server is configured in a way that enforces PKCE S256 as challenge method for this client.",
|
||||||
},
|
},
|
||||||
{"ShouldEnforcePKCEFromChallengeMethodAndNotErrorOnValidRequest",
|
{"ShouldEnforcePKCEFromChallengeMethodAndNotErrorOnValidRequest",
|
||||||
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
||||||
|
@ -277,6 +282,7 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
"S256",
|
"S256",
|
||||||
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}, "code_challenge_method": {"S256"}}},
|
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}, "code_challenge_method": {"S256"}}},
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,7 +298,136 @@ func TestNewClientPKCE(t *testing.T) {
|
||||||
err := client.ValidatePKCEPolicy(tc.r)
|
err := client.ValidatePKCEPolicy(tc.r)
|
||||||
|
|
||||||
if tc.err != "" {
|
if tc.err != "" {
|
||||||
|
require.NotNil(t, err)
|
||||||
assert.EqualError(t, err, tc.err)
|
assert.EqualError(t, err, tc.err)
|
||||||
|
assert.Equal(t, tc.desc, fosite.ErrorToRFC6749Error(err).WithExposeDebug(true).GetDescription())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClientPAR(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have schema.OpenIDConnectClientConfiguration
|
||||||
|
expected bool
|
||||||
|
r *fosite.Request
|
||||||
|
err string
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ShouldNotEnforcEPARAndNotErrorOnNonPARRequest",
|
||||||
|
schema.OpenIDConnectClientConfiguration{},
|
||||||
|
false,
|
||||||
|
&fosite.Request{},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldEnforcePARAndErrorOnNonPARRequest",
|
||||||
|
schema.OpenIDConnectClientConfiguration{EnforcePAR: true},
|
||||||
|
true,
|
||||||
|
&fosite.Request{},
|
||||||
|
"invalid_request",
|
||||||
|
"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Pushed Authorization Requests are enforced for this client but no such request was sent. The request_uri parameter was empty.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldEnforcePARAndErrorOnNonPARRequest",
|
||||||
|
schema.OpenIDConnectClientConfiguration{EnforcePAR: true},
|
||||||
|
true,
|
||||||
|
&fosite.Request{Form: map[string][]string{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},
|
||||||
|
true,
|
||||||
|
&fosite.Request{Form: map[string][]string{FormParameterRequestURI: {fmt.Sprintf("%sabc", urnPARPrefix)}}},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
client := NewClient(tc.have)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected, client.EnforcePAR)
|
||||||
|
|
||||||
|
if tc.r != nil {
|
||||||
|
err := client.ValidatePARPolicy(tc.r, urnPARPrefix)
|
||||||
|
|
||||||
|
if tc.err != "" {
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.EqualError(t, err, tc.err)
|
||||||
|
assert.Equal(t, tc.desc, fosite.ErrorToRFC6749Error(err).WithExposeDebug(true).GetDescription())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClientResponseModes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have schema.OpenIDConnectClientConfiguration
|
||||||
|
expected []fosite.ResponseModeType
|
||||||
|
r *fosite.AuthorizeRequest
|
||||||
|
err string
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ShouldEnforceResponseModePolicyAndAllowDefaultModeQuery",
|
||||||
|
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{ResponseModeQuery}},
|
||||||
|
[]fosite.ResponseModeType{fosite.ResponseModeQuery},
|
||||||
|
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{FormParameterResponseMode: nil}}},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldEnforceResponseModePolicyAndFailOnDefaultMode",
|
||||||
|
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{ResponseModeFormPost}},
|
||||||
|
[]fosite.ResponseModeType{fosite.ResponseModeFormPost},
|
||||||
|
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{FormParameterResponseMode: nil}}},
|
||||||
|
"unsupported_response_mode",
|
||||||
|
"The authorization server does not support obtaining a response using this response mode. The request omitted the response_mode making the default response_mode 'query' based on the other authorization request parameters but registered OAuth 2.0 client doesn't support this response_mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldNotEnforceConfiguredResponseMode",
|
||||||
|
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{ResponseModeFormPost}},
|
||||||
|
[]fosite.ResponseModeType{fosite.ResponseModeFormPost},
|
||||||
|
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeQuery, Request: fosite.Request{Form: map[string][]string{FormParameterResponseMode: {ResponseModeQuery}}}},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ShouldNotEnforceUnconfiguredResponseMode",
|
||||||
|
schema.OpenIDConnectClientConfiguration{ResponseModes: []string{}},
|
||||||
|
[]fosite.ResponseModeType{},
|
||||||
|
&fosite.AuthorizeRequest{DefaultResponseMode: fosite.ResponseModeQuery, ResponseMode: fosite.ResponseModeDefault, Request: fosite.Request{Form: map[string][]string{FormParameterResponseMode: {ResponseModeQuery}}}},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
client := NewClient(tc.have)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected, client.GetResponseModes())
|
||||||
|
|
||||||
|
if tc.r != nil {
|
||||||
|
err := client.ValidateResponseModePolicy(tc.r)
|
||||||
|
|
||||||
|
if tc.err != "" {
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.EqualError(t, err, tc.err)
|
||||||
|
assert.Equal(t, tc.desc, fosite.ErrorToRFC6749Error(err).WithExposeDebug(true).GetDescription())
|
||||||
} else {
|
} else {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ const (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FormParameterRequestURI = "request_uri"
|
FormParameterRequestURI = "request_uri"
|
||||||
|
FormParameterResponseMode = "response_mode"
|
||||||
FormParameterCodeChallenge = "code_challenge"
|
FormParameterCodeChallenge = "code_challenge"
|
||||||
FormParameterCodeChallengeMethod = "code_challenge_method"
|
FormParameterCodeChallengeMethod = "code_challenge_method"
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ory/fosite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsPushedAuthorizedRequest returns true if the requester has a PushedAuthorizationRequest redirect_uri value.
|
||||||
|
func IsPushedAuthorizedRequest(r fosite.Requester, prefix string) bool {
|
||||||
|
return strings.HasPrefix(r.GetRequestForm().Get(FormParameterRequestURI), prefix)
|
||||||
|
}
|
|
@ -38,12 +38,12 @@ type Provider interface {
|
||||||
LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error)
|
LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error)
|
||||||
LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []model.TOTPConfiguration, err error)
|
LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []model.TOTPConfiguration, err error)
|
||||||
|
|
||||||
SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error)
|
SaveWebauthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error)
|
||||||
UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error)
|
UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error)
|
||||||
DeleteWebauthnDevice(ctx context.Context, kid string) (err error)
|
DeleteWebauthnDevice(ctx context.Context, kid string) (err error)
|
||||||
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error)
|
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error)
|
||||||
LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error)
|
LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error)
|
||||||
LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebauthnDevice, err error)
|
LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebAuthnDevice, err error)
|
||||||
|
|
||||||
SavePreferredDuoDevice(ctx context.Context, device model.DuoDevice) (err error)
|
SavePreferredDuoDevice(ctx context.Context, device model.DuoDevice) (err error)
|
||||||
DeletePreferredDuoDevice(ctx context.Context, username string) (err error)
|
DeletePreferredDuoDevice(ctx context.Context, username string) (err error)
|
||||||
|
|
|
@ -882,7 +882,7 @@ func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page in
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveWebauthnDevice saves a registered Webauthn device.
|
// SaveWebauthnDevice saves a registered Webauthn device.
|
||||||
func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error) {
|
func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error) {
|
||||||
if device.PublicKey, err = p.encrypt(device.PublicKey); err != nil {
|
if device.PublicKey, err = p.encrypt(device.PublicKey); err != nil {
|
||||||
return fmt.Errorf("error encrypting Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err)
|
return fmt.Errorf("error encrypting Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err)
|
||||||
}
|
}
|
||||||
|
@ -937,8 +937,8 @@ func (p *SQLProvider) DeleteWebauthnDeviceByUsername(ctx context.Context, userna
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadWebauthnDevices loads Webauthn device registrations.
|
// LoadWebauthnDevices loads Webauthn device registrations.
|
||||||
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error) {
|
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error) {
|
||||||
devices = make([]model.WebauthnDevice, 0, limit)
|
devices = make([]model.WebAuthnDevice, 0, limit)
|
||||||
|
|
||||||
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevices, limit, limit*page); err != nil {
|
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevices, limit, limit*page); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
@ -958,7 +958,7 @@ func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadWebauthnDevicesByUsername loads all webauthn devices registration for a given username.
|
// LoadWebauthnDevicesByUsername loads all webauthn devices registration for a given username.
|
||||||
func (p *SQLProvider) LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebauthnDevice, err error) {
|
func (p *SQLProvider) LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebAuthnDevice, err error) {
|
||||||
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevicesByUsername, username); err != nil {
|
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevicesByUsername, username); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, ErrNoWebauthnDevice
|
return nil, ErrNoWebauthnDevice
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
envoy:
|
envoy:
|
||||||
image: envoyproxy/envoy:v1.25.4
|
image: envoyproxy/envoy:v1.25.5
|
||||||
volumes:
|
volumes:
|
||||||
- ./example/compose/envoy/envoy.yaml:/etc/envoy/envoy.yaml
|
- ./example/compose/envoy/envoy.yaml:/etc/envoy/envoy.yaml
|
||||||
- ./common/pki:/pki
|
- ./common/pki:/pki
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<form method="post" action="{{ .RedirURL }}">
|
<form method="post" action="{{ .RedirURL }}">
|
||||||
{{ range $key,$value := .Parameters }}
|
{{ range $key, $value := .Parameters }}
|
||||||
{{ range $parameter:= $value}}
|
{{ range $parameter := $value }}
|
||||||
<input type="hidden" name="{{$key}}" value="{{$parameter}}"/>
|
<input type="hidden" name="{{ $key }}" value="{{ $parameter }}"/>
|
||||||
{{end}}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.vitest-preview
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|
111
web/package.json
111
web/package.json
|
@ -6,15 +6,8 @@
|
||||||
"peerDependencyRules": {
|
"peerDependencyRules": {
|
||||||
"allowedVersions": {
|
"allowedVersions": {
|
||||||
"@types/react": "18",
|
"@types/react": "18",
|
||||||
"react": "18",
|
"react": "18"
|
||||||
"react-dom": "18"
|
}
|
||||||
},
|
|
||||||
"ignoreMissing": [
|
|
||||||
"@babel/core",
|
|
||||||
"@babel/plugin-syntax-flow",
|
|
||||||
"@babel/plugin-transform-react-jsx",
|
|
||||||
"prop-types"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -26,8 +19,8 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@mui/icons-material": "5.11.16",
|
"@mui/icons-material": "5.11.16",
|
||||||
"@mui/material": "5.11.16",
|
"@mui/material": "5.12.0",
|
||||||
"@mui/styles": "5.11.16",
|
"@mui/styles": "5.12.0",
|
||||||
"axios": "1.3.5",
|
"axios": "1.3.5",
|
||||||
"broadcast-channel": "5.0.3",
|
"broadcast-channel": "5.0.3",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
|
@ -49,83 +42,14 @@
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"coverage": "VITE_COVERAGE=true vite build",
|
"coverage": "VITE_COVERAGE=true vite build",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
"test": "jest --coverage --no-cache",
|
"test": "vitest run --coverage",
|
||||||
|
"test:watch": "vitest --coverage",
|
||||||
|
"test:preview": "vitest-preview",
|
||||||
"report": "nyc report -r clover -r json -r lcov -r text"
|
"report": "nyc report -r clover -r json -r lcov -r text"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
},
|
},
|
||||||
"jest": {
|
|
||||||
"roots": [
|
|
||||||
"<rootDir>/src"
|
|
||||||
],
|
|
||||||
"collectCoverageFrom": [
|
|
||||||
"src/**/*.{js,jsx,ts,tsx}",
|
|
||||||
"!src/**/*.d.ts"
|
|
||||||
],
|
|
||||||
"setupFilesAfterEnv": [
|
|
||||||
"<rootDir>/src/setupTests.js"
|
|
||||||
],
|
|
||||||
"testMatch": [
|
|
||||||
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
|
||||||
],
|
|
||||||
"testEnvironment": "jsdom",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": [
|
|
||||||
"esbuild-jest",
|
|
||||||
{
|
|
||||||
"sourcemap": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"^.+\\.(css|png|svg)$": "jest-transform-stub"
|
|
||||||
},
|
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$"
|
|
||||||
],
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"^@root/(.*)$": [
|
|
||||||
"<rootDir>/src/$1"
|
|
||||||
],
|
|
||||||
"^@assets/(.*)$": [
|
|
||||||
"<rootDir>/src/assets/$1"
|
|
||||||
],
|
|
||||||
"^@components/(.*)$": [
|
|
||||||
"<rootDir>/src/components/$1"
|
|
||||||
],
|
|
||||||
"^@constants/(.*)$": [
|
|
||||||
"<rootDir>/src/constants/$1"
|
|
||||||
],
|
|
||||||
"^@hooks/(.*)$": [
|
|
||||||
"<rootDir>/src/hooks/$1"
|
|
||||||
],
|
|
||||||
"^@i18n/(.*)$": [
|
|
||||||
"<rootDir>/src/i18n/$1"
|
|
||||||
],
|
|
||||||
"^@layouts/(.*)$": [
|
|
||||||
"<rootDir>/src/layouts/$1"
|
|
||||||
],
|
|
||||||
"^@models/(.*)$": [
|
|
||||||
"<rootDir>/src/models/$1"
|
|
||||||
],
|
|
||||||
"^@services/(.*)$": [
|
|
||||||
"<rootDir>/src/services/$1"
|
|
||||||
],
|
|
||||||
"^@themes/(.*)$": [
|
|
||||||
"<rootDir>/src/themes/$1"
|
|
||||||
],
|
|
||||||
"^@utils/(.*)$": [
|
|
||||||
"<rootDir>/src/utils/$1"
|
|
||||||
],
|
|
||||||
"^@views/(.*)$": [
|
|
||||||
"<rootDir>/src/views/$1"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"watchPlugins": [
|
|
||||||
"jest-watch-typeahead/filename",
|
|
||||||
"jest-watch-typeahead/testname"
|
|
||||||
],
|
|
||||||
"resetMocks": true
|
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
@ -147,17 +71,17 @@
|
||||||
"@limegrass/eslint-plugin-import-alias": "1.0.6",
|
"@limegrass/eslint-plugin-import-alias": "1.0.6",
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/react": "14.0.0",
|
"@testing-library/react": "14.0.0",
|
||||||
"@types/jest": "29.5.0",
|
|
||||||
"@types/node": "18.15.11",
|
"@types/node": "18.15.11",
|
||||||
"@types/qrcode.react": "1.0.2",
|
"@types/qrcode.react": "1.0.2",
|
||||||
"@types/react": "18.0.33",
|
"@types/react": "18.0.35",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
|
"@types/testing-library__jest-dom": "5.14.5",
|
||||||
"@types/zxcvbn": "4.4.1",
|
"@types/zxcvbn": "4.4.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||||
"@typescript-eslint/parser": "5.57.1",
|
"@typescript-eslint/parser": "5.58.0",
|
||||||
"@vitejs/plugin-react": "3.1.0",
|
"@vitejs/plugin-react": "3.1.0",
|
||||||
"esbuild": "0.17.15",
|
"@vitest/coverage-istanbul": "0.30.1",
|
||||||
"esbuild-jest": "0.5.0",
|
"esbuild": "0.17.16",
|
||||||
"eslint": "8.38.0",
|
"eslint": "8.38.0",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-config-react-app": "7.0.1",
|
"eslint-config-react-app": "7.0.1",
|
||||||
|
@ -168,11 +92,8 @@
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
|
"happy-dom": "9.3.2",
|
||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"jest": "29.5.0",
|
|
||||||
"jest-environment-jsdom": "29.5.0",
|
|
||||||
"jest-transform-stub": "2.0.0",
|
|
||||||
"jest-watch-typeahead": "2.2.2",
|
|
||||||
"prettier": "2.8.7",
|
"prettier": "2.8.7",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
|
@ -180,6 +101,8 @@
|
||||||
"vite-plugin-eslint": "1.8.1",
|
"vite-plugin-eslint": "1.8.1",
|
||||||
"vite-plugin-istanbul": "4.0.1",
|
"vite-plugin-istanbul": "4.0.1",
|
||||||
"vite-plugin-svgr": "2.4.0",
|
"vite-plugin-svgr": "2.4.0",
|
||||||
"vite-tsconfig-paths": "4.0.8"
|
"vite-tsconfig-paths": "4.2.0",
|
||||||
|
"vitest": "0.30.1",
|
||||||
|
"vitest-preview": "0.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6177
web/pnpm-lock.yaml
6177
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,34 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
import NotificationBar from "@components/NotificationBar";
|
import NotificationBar from "@components/NotificationBar";
|
||||||
|
import NotificationsContext from "@hooks/NotificationsContext";
|
||||||
|
import { Notification } from "@models/Notifications";
|
||||||
|
|
||||||
|
const testNotification: Notification = {
|
||||||
|
message: "Test notification",
|
||||||
|
level: "success",
|
||||||
|
timeout: 3,
|
||||||
|
};
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
render(<NotificationBar onClose={() => {}} />);
|
render(<NotificationBar onClose={() => {}} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("displays notification message and level correctly", async () => {
|
||||||
|
render(
|
||||||
|
<NotificationsContext.Provider value={{ notification: testNotification, setNotification: () => {} }}>
|
||||||
|
<NotificationBar onClose={() => {}} />
|
||||||
|
</NotificationsContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const alert = await screen.getByRole("alert");
|
||||||
|
const message = await screen.findByText(testNotification.message);
|
||||||
|
|
||||||
|
expect(alert).toHaveClass(
|
||||||
|
`MuiAlert-filled${testNotification.level.charAt(0).toUpperCase() + testNotification.level.substring(1)}`,
|
||||||
|
{ exact: false },
|
||||||
|
);
|
||||||
|
expect(message).toHaveTextContent(testNotification.message);
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
|
||||||
|
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
withTranslation: () => (Component: any) => {
|
||||||
|
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
|
||||||
|
return Component;
|
||||||
|
},
|
||||||
|
Trans: ({ children }: any) => children,
|
||||||
|
useTranslation: () => {
|
||||||
|
return {
|
||||||
|
t: (str) => str,
|
||||||
|
i18n: {
|
||||||
|
changeLanguage: () => new Promise(() => {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||||
|
|
||||||
|
global.localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders privacy policy and accepts when Accept button is clicked", () => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||||
|
document.body.setAttribute("data-privacypolicyaccept", "true");
|
||||||
|
|
||||||
|
const { container } = render(<PrivacyPolicyDrawer />);
|
||||||
|
fireEvent.click(screen.getByText("Accept"));
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when privacy policy is disabled", () => {
|
||||||
|
render(<PrivacyPolicyDrawer />);
|
||||||
|
expect(screen.queryByText("Privacy Policy")).toBeNull();
|
||||||
|
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
|
||||||
|
expect(screen.queryByText("Accept")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when acceptance is not required", () => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||||
|
|
||||||
|
render(<PrivacyPolicyDrawer />);
|
||||||
|
expect(screen.queryByText("Privacy Policy")).toBeNull();
|
||||||
|
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
|
||||||
|
expect(screen.queryByText("Accept")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when already accepted", () => {
|
||||||
|
global.localStorage.setItem("privacy-policy-accepted", "true");
|
||||||
|
|
||||||
|
const { container } = render(<PrivacyPolicyDrawer />);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
|
||||||
|
import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
|
||||||
|
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
withTranslation: () => (Component: any) => {
|
||||||
|
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
|
||||||
|
return Component;
|
||||||
|
},
|
||||||
|
Trans: ({ children }: any) => children,
|
||||||
|
useTranslation: () => {
|
||||||
|
return {
|
||||||
|
t: (str) => str,
|
||||||
|
i18n: {
|
||||||
|
changeLanguage: () => new Promise(() => {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("renders a link to the privacy policy with the correct text", () => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||||
|
|
||||||
|
const { getByRole } = render(<PrivacyPolicyLink />);
|
||||||
|
const link = getByRole("link");
|
||||||
|
expect(link).toHaveAttribute("href", "http://example.com/privacy-policy");
|
||||||
|
expect(link).toHaveTextContent("Privacy Policy");
|
||||||
|
});
|
|
@ -1,9 +1,37 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { act, render } from "@testing-library/react";
|
||||||
|
|
||||||
import TimerIcon from "@components/TimerIcon";
|
import TimerIcon from "@components/TimerIcon";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers().setSystemTime(new Date(2023, 1, 1, 8));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
render(<TimerIcon width={32} height={32} period={30} />);
|
render(<TimerIcon width={32} height={32} period={30} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders a timer icon with updating progress for a given period", async () => {
|
||||||
|
const { container } = render(<TimerIcon width={32} height={32} period={30} />);
|
||||||
|
const initialProgress =
|
||||||
|
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
|
||||||
|
"stroke-dasharray",
|
||||||
|
);
|
||||||
|
expect(initialProgress).toBe("0 31.6");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedProgress =
|
||||||
|
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
|
||||||
|
"stroke-dasharray",
|
||||||
|
);
|
||||||
|
expect(updatedProgress).toBe("3.16 31.6");
|
||||||
|
expect(Number(updatedProgress!.split(/\s(.+)/)[0])).toBeGreaterThan(Number(initialProgress!.split(/\s(.+)/)[0]));
|
||||||
|
});
|
||||||
|
|
|
@ -2,12 +2,41 @@ import React from "react";
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
|
|
||||||
import TypographyWithTooltip from "@components/TypographyWithTootip";
|
import TypographyWithTooltip, { Props } from "@components/TypographyWithTooltip";
|
||||||
|
|
||||||
|
const defaultProps: Props = {
|
||||||
|
variant: "h5",
|
||||||
|
value: "Example",
|
||||||
|
};
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
render(<TypographyWithTooltip value={"Example"} variant={"h5"} />);
|
render(<TypographyWithTooltip {...defaultProps} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders with tooltip without crashing", () => {
|
it("renders with tooltip without crashing", () => {
|
||||||
render(<TypographyWithTooltip value={"Example"} tooltip={"A tooltip"} variant={"h5"} />);
|
const props: Props = {
|
||||||
|
...defaultProps,
|
||||||
|
tooltip: "A tooltip",
|
||||||
|
};
|
||||||
|
render(<TypographyWithTooltip {...props} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the text correctly", () => {
|
||||||
|
const props: Props = {
|
||||||
|
...defaultProps,
|
||||||
|
value: "Test text",
|
||||||
|
};
|
||||||
|
const { getByText } = render(<TypographyWithTooltip {...props} />);
|
||||||
|
const element = getByText(props.value!);
|
||||||
|
expect(element).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the tooltip correctly", () => {
|
||||||
|
const props: Props = {
|
||||||
|
...defaultProps,
|
||||||
|
tooltip: "Test tooltip",
|
||||||
|
};
|
||||||
|
const { getByText } = render(<TypographyWithTooltip {...props} />);
|
||||||
|
const element = getByText(props.value!);
|
||||||
|
expect(element).toHaveAttribute("aria-label", props.tooltip);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
||||||
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
|
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
|
||||||
import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
|
import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
|
||||||
import TypographyWithTooltip from "@components/TypographyWithTootip";
|
import TypographyWithTooltip from "@components/TypographyWithTooltip";
|
||||||
import { getLogoOverride, getPrivacyPolicyEnabled } from "@utils/Configuration";
|
import { getLogoOverride, getPrivacyPolicyEnabled } from "@utils/Configuration";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
|
|
||||||
document.body.setAttribute("data-basepath", "");
|
|
||||||
document.body.setAttribute("data-duoselfenrollment", "true");
|
|
||||||
document.body.setAttribute("data-rememberme", "true");
|
|
||||||
document.body.setAttribute("data-resetpassword", "true");
|
|
||||||
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
|
||||||
document.body.setAttribute("data-privacypolicyurl", "");
|
|
||||||
document.body.setAttribute("data-privacypolicyaccept", "false");
|
|
||||||
document.body.setAttribute("data-theme", "light");
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import matchers, { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Vi {
|
||||||
|
interface JestAssertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend(matchers);
|
||||||
|
|
||||||
|
const localStorageMock = (function () {
|
||||||
|
let store = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getItem(key) {
|
||||||
|
return store[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem(key, value) {
|
||||||
|
store[key] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
store = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(key) {
|
||||||
|
delete store[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
getAll() {
|
||||||
|
return store;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||||
|
|
||||||
|
document.body.setAttribute("data-basepath", "");
|
||||||
|
document.body.setAttribute("data-duoselfenrollment", "true");
|
||||||
|
document.body.setAttribute("data-rememberme", "true");
|
||||||
|
document.body.setAttribute("data-resetpassword", "true");
|
||||||
|
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||||
|
document.body.setAttribute("data-theme", "light");
|
|
@ -21,7 +21,7 @@
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
"types": ["@types/jest", "vite/client", "vite-plugin-svgr/client"],
|
"types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
|
@ -5,18 +5,17 @@ import istanbul from "vite-plugin-istanbul";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const isCoverage = process.env.VITE_COVERAGE === "true";
|
const isCoverage = process.env.VITE_COVERAGE === "true";
|
||||||
const sourcemap = isCoverage ? "inline" : undefined;
|
const sourcemap = isCoverage ? "inline" : undefined;
|
||||||
|
|
||||||
const istanbulPlugin = isCoverage
|
const istanbulPlugin = isCoverage
|
||||||
? istanbul({
|
? istanbul({
|
||||||
include: "src/*",
|
checkProd: false,
|
||||||
exclude: ["node_modules"],
|
exclude: ["node_modules"],
|
||||||
extension: [".js", ".jsx", ".ts", ".tsx"],
|
extension: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
checkProd: false,
|
|
||||||
forceBuildInstrument: true,
|
forceBuildInstrument: true,
|
||||||
|
include: "src/*",
|
||||||
requireEnv: true,
|
requireEnv: true,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -24,14 +23,11 @@ export default defineConfig(({ mode }) => {
|
||||||
return {
|
return {
|
||||||
base: "./",
|
base: "./",
|
||||||
build: {
|
build: {
|
||||||
sourcemap,
|
|
||||||
outDir: "../internal/server/public_html",
|
|
||||||
emptyOutDir: true,
|
|
||||||
assetsDir: "static",
|
assetsDir: "static",
|
||||||
|
emptyOutDir: true,
|
||||||
|
outDir: "../internal/server/public_html",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: `static/js/[name].[hash].js`,
|
|
||||||
chunkFileNames: `static/js/[name].[hash].js`,
|
|
||||||
assetFileNames: ({ name }) => {
|
assetFileNames: ({ name }) => {
|
||||||
if (name && name.endsWith(".css")) {
|
if (name && name.endsWith(".css")) {
|
||||||
return "static/css/[name].[hash].[ext]";
|
return "static/css/[name].[hash].[ext]";
|
||||||
|
@ -39,12 +35,26 @@ export default defineConfig(({ mode }) => {
|
||||||
|
|
||||||
return "static/media/[name].[hash].[ext]";
|
return "static/media/[name].[hash].[ext]";
|
||||||
},
|
},
|
||||||
|
chunkFileNames: `static/js/[name].[hash].js`,
|
||||||
|
entryFileNames: `static/js/[name].[hash].js`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
sourcemap,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
|
||||||
open: false,
|
open: false,
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
coverage: {
|
||||||
|
provider: "istanbul",
|
||||||
|
},
|
||||||
|
environment: "happy-dom",
|
||||||
|
globals: true,
|
||||||
|
onConsoleLog(log) {
|
||||||
|
if (log.includes('No routes matched location "blank"')) return false;
|
||||||
|
},
|
||||||
|
setupFiles: ["src/setupTests.ts"],
|
||||||
},
|
},
|
||||||
plugins: [eslintPlugin({ cache: false }), istanbulPlugin, react(), svgr(), tsconfigPaths()],
|
plugins: [eslintPlugin({ cache: false }), istanbulPlugin, react(), svgr(), tsconfigPaths()],
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue