Merge branch 'master' into fix-pp-layout

fix-pp-layout
James Elliott 2023-04-13 20:33:36 +10:00 committed by GitHub
commit 92f43a4061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 4288 additions and 5584 deletions

View File

@ -7,5 +7,5 @@
package cmd package cmd
const ( const (
versionSwaggerUI = "4.18.1" versionSwaggerUI = "4.18.2"
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: []
--- ---

View File

@ -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: []
--- ---

View File

@ -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: []
--- ---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

6
go.mod
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,10 +259,15 @@ func loadXNormalizedPaths(paths []string) ([]string, error) {
files = append(files, path) files = append(files, path)
default: default:
if os.IsNotExist(err) { if os.IsNotExist(err) {
configs = append(configs, path) switch result {
files = append(files, path) case XEnvCLIResultCLIImplicit:
continue
default:
configs = append(configs, 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
web/.gitignore vendored
View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()],
}; };