Merge branch 'master' into fix-pp-layout
commit
92f43a4061
|
@ -7,5 +7,5 @@
|
|||
package cmd
|
||||
|
||||
const (
|
||||
versionSwaggerUI = "4.18.1"
|
||||
versionSwaggerUI = "4.18.2"
|
||||
)
|
||||
|
|
|
@ -49,7 +49,7 @@ authelia configuration, and authelia database prior to attempting to do so._
|
|||
Notable Missing Features from this build:
|
||||
|
||||
- OpenID Connect 1.0 PAR
|
||||
- Multi-Device Webauthn
|
||||
- Multi-Device WebAuthn
|
||||
- Device Registration OTP
|
||||
|
||||
- Container Images:
|
||||
|
@ -144,7 +144,7 @@ Please see the [roadmap](../../roadmap/active/openid-connect.md) for more inform
|
|||
|
||||
##### 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._
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
##### 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
|
||||
[User Control Panel](#user-dashboard--control-panel).
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ authelia --config configuration.yml,config-acl.yml,config-other.yml
|
|||
```
|
||||
|
||||
Authelia's configuration files use the YAML format. A template with all possible options can be found at the root of the
|
||||
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
|
||||
configured in multiple files. If you wish to split these into their own files that is fine, but if you have two files that
|
||||
|
|
|
@ -16,9 +16,8 @@ toc: true
|
|||
|
||||
We document the configuration in two ways:
|
||||
|
||||
1. The [YAML] configuration template
|
||||
[config.template.yml](https://github.com/authelia/authelia/blob/master/config.template.yml) has comments with very
|
||||
limited documentation on the effective use of a particular option. All documentation lines start with `##`. Lines
|
||||
1. The [YAML] configuration template {{< github-link path="config.template.yml" >}} has comments with very 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.
|
||||
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
|
||||
|
|
|
@ -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 |
|
||||
| 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 |
|
||||
| 9 | 4.38.0 | Fix a PostgreSQL NOT NULL constraint issue on the `aaguid` column of the `webauthn_devices` table |
|
||||
|
|
|
@ -38,6 +38,23 @@ The additional tools are recommended:
|
|||
* [yamllint]
|
||||
* [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
|
||||
|
||||
There is a scripting context provided with __Authelia__ which can easily be configured. It allows running integration
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "Testing"
|
||||
description: "Authelia Development 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
|
||||
images: []
|
||||
menu:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "Amir Zarrinkafsh"
|
||||
date: 2022-06-15T17:51:47+10:00
|
||||
date: 2023-03-19T16:29:12+10:00
|
||||
draft: false
|
||||
images: []
|
||||
---
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "Clément Michaud"
|
||||
date: 2022-06-15T17:51:47+10:00
|
||||
date: 2023-03-19T16:29:12+10:00
|
||||
draft: false
|
||||
images: []
|
||||
---
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "Manuel Nuñez"
|
||||
date: 2022-06-15T17:51:47+10:00
|
||||
date: 2023-03-19T16:29:12+10:00
|
||||
draft: false
|
||||
images: []
|
||||
---
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "About"
|
||||
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
|
||||
images: []
|
||||
aliases:
|
||||
|
|
|
@ -25,8 +25,8 @@ bootstrapping *Authelia*.
|
|||
|
||||
We publish two example [systemd] unit files:
|
||||
|
||||
* [authelia.service](https://github.com/authelia/authelia/blob/master/authelia.service)
|
||||
* [authelia@.service](https://github.com/authelia/authelia/blob/master/authelia%40.service)
|
||||
* {{< github-link path="authelia.service" >}}
|
||||
* {{< github-link path="authelia@.service" >}}
|
||||
|
||||
## Arch Linux
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "Firezone"
|
||||
description: "Integrating Firezone with the Authelia OpenID Connect Provider."
|
||||
lead: ""
|
||||
date: 2023-03-25T13:07:02+10:00
|
||||
date: 2023-03-28T20:29:13+11:00
|
||||
draft: false
|
||||
images: []
|
||||
menu:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "MinIO"
|
||||
description: "Integrating MinIO with the Authelia OpenID Connect Provider."
|
||||
lead: ""
|
||||
date: 2022-06-15T17:51:47+10:00
|
||||
date: 2023-03-21T11:21:23+11:00
|
||||
draft: false
|
||||
images: []
|
||||
menu:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "Misago"
|
||||
description: "Integrating Misago with the Authelia OpenID Connect Provider."
|
||||
lead: ""
|
||||
date: 2023-03-04T13:20:00+00:00
|
||||
date: 2023-03-14T08:51:13+11:00
|
||||
draft: false
|
||||
images: []
|
||||
menu:
|
||||
|
|
|
@ -23,24 +23,31 @@ common scenarios however those using more advanced architectures are likely goin
|
|||
help with answering less specific questions about this and it may be possible if provided adequate information more
|
||||
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 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.
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
[configuration template](https://github.com/authelia/authelia/blob/master/config.template.yml) on GitHub which can be
|
||||
used as a basis for configuration.
|
||||
not configured via web GUI. You can find a configuration template named {{< github-link path="config.template.yml" >}}
|
||||
on GitHub which can be used as a basis for configuration, alternatively *Authelia* will write this template relevant for
|
||||
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:
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ serving Authelia at `auth.example.com`.
|
|||
```nginx
|
||||
## Set $authelia_backend to route requests to the current domain by default
|
||||
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:
|
||||
## * comment the $authelia_backend line above
|
||||
## * 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
|
||||
## Set $authelia_backend to route requests to the current domain by default
|
||||
# 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:
|
||||
## * comment the $authelia_backend line above
|
||||
## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf
|
||||
|
|
|
@ -44,7 +44,7 @@ case you have multiple devices available, you will be asked to select your prefe
|
|||
### Why don't I have access to the *Push Notification* option?
|
||||
|
||||
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).
|
||||
|
||||
[Duo]: https://duo.com/
|
||||
|
|
|
@ -63,5 +63,5 @@ authelia storage user --help
|
|||
* [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 totp](authelia_storage_user_totp.md) - Manage TOTP configurations
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
|||
|
||||
## authelia storage user webauthn
|
||||
|
||||
Manage Webauthn devices
|
||||
Manage WebAuthn devices
|
||||
|
||||
### Synopsis
|
||||
|
||||
Manage Webauthn devices.
|
||||
Manage WebAuthn devices.
|
||||
|
||||
This subcommand allows interacting with Webauthn devices.
|
||||
This subcommand allows interacting with WebAuthn devices.
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -61,8 +61,8 @@ authelia storage user webauthn --help
|
|||
### SEE ALSO
|
||||
|
||||
* [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 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 list](authelia_storage_user_webauthn_list.md) - List Webauthn devices
|
||||
* [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 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
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
|||
|
||||
## authelia storage user webauthn delete
|
||||
|
||||
Delete a Webauthn device
|
||||
Delete a WebAuthn device
|
||||
|
||||
### 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]
|
||||
|
@ -75,5 +75,5 @@ authelia storage user webauthn delete --kid abc123 --encryption-key b3453fde-ecc
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
|||
|
||||
## authelia storage user webauthn export
|
||||
|
||||
Perform exports of the Webauthn devices
|
||||
Perform exports of the WebAuthn devices
|
||||
|
||||
### 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]
|
||||
|
@ -68,5 +68,5 @@ authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-27
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
|||
|
||||
## authelia storage user webauthn import
|
||||
|
||||
Perform imports of the Webauthn devices
|
||||
Perform imports of the WebAuthn devices
|
||||
|
||||
### 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]
|
||||
|
@ -67,5 +67,5 @@ authelia storage user webauthn import --file authelia.export.webauthn.yaml --enc
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
|||
|
||||
## authelia storage user webauthn list
|
||||
|
||||
List Webauthn devices
|
||||
List WebAuthn devices
|
||||
|
||||
### 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]
|
||||
|
@ -69,5 +69,5 @@ authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-942
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{{- $repo := "authelia/authelia" }}{{ with .Get "repo" }}{{ $repo = . }}{{ end }}
|
||||
{{- $branch := printf "v%s" .Site.Data.misc.latest }}{{ with .Get "branch" }}{{ $branch = . }}{{ end }}
|
||||
{{- $path := "" }}{{ with .Get "path" }}{{ $path = . }}{{ end }}
|
||||
{{- $link := printf "https://github.com/%s/blob/%s/%s" $repo $branch (urlquery $path) }}
|
||||
{{- $name := "" }}
|
||||
{{- with .Get "name" }}
|
||||
{{- $name = . }}
|
||||
{{- else }}
|
||||
{{- if (eq $repo "authelia/authelia") }}
|
||||
{{- $name = $path }}
|
||||
{{- else }}
|
||||
{{- $name = printf "https://github.com/%s/blob/%s/%s" $repo $branch $path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- "" -}}
|
||||
<a href="{{ $link }}" target="_blank">{{ $name }}</a>
|
||||
{{- "" -}}
|
|
@ -39,27 +39,27 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.21.0",
|
||||
"@babel/core": "7.21.0",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/core": "7.21.4",
|
||||
"@babel/preset-env": "7.21.4",
|
||||
"@fullhuman/postcss-purgecss": "5.0.0",
|
||||
"@hyas/images": "0.3.2",
|
||||
"@popperjs/core": "2.11.6",
|
||||
"@popperjs/core": "2.11.7",
|
||||
"auto-changelog": "2.4.0",
|
||||
"autoprefixer": "10.4.13",
|
||||
"autoprefixer": "10.4.14",
|
||||
"bootstrap": "5.2.3",
|
||||
"bootstrap-icons": "1.10.3",
|
||||
"bootstrap-icons": "1.10.4",
|
||||
"clipboard": "2.0.11",
|
||||
"eslint": "8.35.0",
|
||||
"eslint": "8.38.0",
|
||||
"exec-bin": "1.0.0",
|
||||
"flexsearch": "0.7.31",
|
||||
"highlight.js": "11.7.0",
|
||||
"hugo-installer": "4.0.1",
|
||||
"instant.page": "5.1.1",
|
||||
"instant.page": "5.2.0",
|
||||
"katex": "0.16.4",
|
||||
"lazysizes": "5.3.2",
|
||||
"markdownlint-cli2": "0.6.0",
|
||||
"netlify-plugin-submit-sitemap": "0.4.0",
|
||||
"node-fetch": "3.3.0",
|
||||
"node-fetch": "3.3.1",
|
||||
"postcss": "8.4.21",
|
||||
"postcss-cli": "10.1.0",
|
||||
"purgecss-whitelister": "2.4.0",
|
||||
|
@ -68,6 +68,6 @@
|
|||
"stylelint-config-standard-scss": "6.1.0"
|
||||
},
|
||||
"otherDependencies": {
|
||||
"hugo": "0.111.2"
|
||||
"hugo": "0.111.3"
|
||||
}
|
||||
}
|
||||
|
|
2197
docs/pnpm-lock.yaml
2197
docs/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
6
go.mod
6
go.mod
|
@ -27,13 +27,13 @@ require (
|
|||
github.com/knadh/koanf/providers/env v0.1.0
|
||||
github.com/knadh/koanf/providers/posflag 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/mitchellh/mapstructure v1.5.0
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/ory/fosite v0.44.0
|
||||
github.com/ory/herodot v0.10.0
|
||||
github.com/ory/x v0.0.549
|
||||
github.com/ory/herodot v0.10.2
|
||||
github.com/ory/x v0.0.551
|
||||
github.com/otiai10/copy v1.10.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.4.0
|
||||
|
|
12
go.sum
12
go.sum
|
@ -295,8 +295,8 @@ github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHY
|
|||
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
|
||||
github.com/knadh/koanf/providers/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/v2 v2.0.0 h1:XPQ5ilNnwnNaHrfQ1YpTVhUAjcGHnEKA+lRpipQv02Y=
|
||||
github.com/knadh/koanf/v2 v2.0.0/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
|
||||
github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
|
||||
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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
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-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8=
|
||||
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.0/go.mod h1:MMNmY6MG1uB6fnXYFaHoqdV23DTWctlPsmRCeq/2+wc=
|
||||
github.com/ory/x v0.0.549 h1:/ngQEYmHMEQAsYxK4uasAR9/WALxRLfHiDUPFQrD6/I=
|
||||
github.com/ory/x v0.0.549/go.mod h1:00UrEq/wEgXxpagcfjn5w2PsJPpfxAVnb94M+eg1bC0=
|
||||
github.com/ory/herodot v0.10.2 h1:gGvNMHgAwWzdP/eo+roSiT5CGssygHSjDU7MSQNlJ4E=
|
||||
github.com/ory/herodot v0.10.2/go.mod h1:MMNmY6MG1uB6fnXYFaHoqdV23DTWctlPsmRCeq/2+wc=
|
||||
github.com/ory/x v0.0.551 h1:U3z2bvSzAwDP0SWmbAdjzfvWPu4k+oWrPctoCdalGk0=
|
||||
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/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
|
||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||
|
|
|
@ -177,56 +177,56 @@ This subcommand allows manually adding an opaque identifier for a user to the da
|
|||
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml
|
||||
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --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 --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`
|
||||
|
||||
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 --config config.yml
|
||||
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 --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 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 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
||||
authelia storage user webauthn delete john --description Primary
|
||||
|
|
|
@ -68,7 +68,7 @@ func storageTOTPGenerateRunEOptsFromFlags(flags *pflag.FlagSet) (force bool, fil
|
|||
return force, filename, secret, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
user = args[0]
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ func newStorageUserCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
|||
cmd.AddCommand(
|
||||
newStorageUserIdentifiersCmd(ctx),
|
||||
newStorageUserTOTPCmd(ctx),
|
||||
newStorageUserWebauthnCmd(ctx),
|
||||
newStorageUserWebAuthnCmd(ctx),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
@ -221,34 +221,34 @@ func newStorageUserIdentifiersAddCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func newStorageUserWebauthnCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
func newStorageUserWebAuthnCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "webauthn",
|
||||
Short: cmdAutheliaStorageUserWebauthnShort,
|
||||
Long: cmdAutheliaStorageUserWebauthnLong,
|
||||
Example: cmdAutheliaStorageUserWebauthnExample,
|
||||
Short: cmdAutheliaStorageUserWebAuthnShort,
|
||||
Long: cmdAutheliaStorageUserWebAuthnLong,
|
||||
Example: cmdAutheliaStorageUserWebAuthnExample,
|
||||
Args: cobra.NoArgs,
|
||||
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newStorageUserWebauthnListCmd(ctx),
|
||||
newStorageUserWebauthnDeleteCmd(ctx),
|
||||
newStorageUserWebauthnExportCmd(ctx),
|
||||
newStorageUserWebauthnImportCmd(ctx),
|
||||
newStorageUserWebAuthnListCmd(ctx),
|
||||
newStorageUserWebAuthnDeleteCmd(ctx),
|
||||
newStorageUserWebAuthnExportCmd(ctx),
|
||||
newStorageUserWebAuthnImportCmd(ctx),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
func newStorageUserWebAuthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: cmdUseImportFileName,
|
||||
Short: cmdAutheliaStorageUserWebauthnImportShort,
|
||||
Long: cmdAutheliaStorageUserWebauthnImportLong,
|
||||
Example: cmdAutheliaStorageUserWebauthnImportExample,
|
||||
RunE: ctx.StorageUserWebauthnImportRunE,
|
||||
Short: cmdAutheliaStorageUserWebAuthnImportShort,
|
||||
Long: cmdAutheliaStorageUserWebAuthnImportLong,
|
||||
Example: cmdAutheliaStorageUserWebAuthnImportExample,
|
||||
RunE: ctx.StorageUserWebAuthnImportRunE,
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
||||
DisableAutoGenTag: true,
|
||||
|
@ -257,13 +257,13 @@ func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
func newStorageUserWebAuthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: cmdUseExport,
|
||||
Short: cmdAutheliaStorageUserWebauthnExportShort,
|
||||
Long: cmdAutheliaStorageUserWebauthnExportLong,
|
||||
Example: cmdAutheliaStorageUserWebauthnExportExample,
|
||||
RunE: ctx.StorageUserWebauthnExportRunE,
|
||||
Short: cmdAutheliaStorageUserWebAuthnExportShort,
|
||||
Long: cmdAutheliaStorageUserWebAuthnExportLong,
|
||||
Example: cmdAutheliaStorageUserWebAuthnExportExample,
|
||||
RunE: ctx.StorageUserWebAuthnExportRunE,
|
||||
Args: cobra.NoArgs,
|
||||
|
||||
DisableAutoGenTag: true,
|
||||
|
@ -274,13 +274,13 @@ func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
func newStorageUserWebAuthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "list [username]",
|
||||
Short: cmdAutheliaStorageUserWebauthnListShort,
|
||||
Long: cmdAutheliaStorageUserWebauthnListLong,
|
||||
Example: cmdAutheliaStorageUserWebauthnListExample,
|
||||
RunE: ctx.StorageUserWebauthnListRunE,
|
||||
Short: cmdAutheliaStorageUserWebAuthnListShort,
|
||||
Long: cmdAutheliaStorageUserWebAuthnListLong,
|
||||
Example: cmdAutheliaStorageUserWebAuthnListExample,
|
||||
RunE: ctx.StorageUserWebAuthnListRunE,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
|
||||
DisableAutoGenTag: true,
|
||||
|
@ -289,13 +289,13 @@ func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func newStorageUserWebauthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
func newStorageUserWebAuthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "delete [username]",
|
||||
Short: cmdAutheliaStorageUserWebauthnDeleteShort,
|
||||
Long: cmdAutheliaStorageUserWebauthnDeleteLong,
|
||||
Example: cmdAutheliaStorageUserWebauthnDeleteExample,
|
||||
RunE: ctx.StorageUserWebauthnDeleteRunE,
|
||||
Short: cmdAutheliaStorageUserWebAuthnDeleteShort,
|
||||
Long: cmdAutheliaStorageUserWebAuthnDeleteLong,
|
||||
Example: cmdAutheliaStorageUserWebAuthnDeleteExample,
|
||||
RunE: ctx.StorageUserWebAuthnDeleteRunE,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
|
||||
DisableAutoGenTag: true,
|
||||
|
|
|
@ -415,7 +415,7 @@ func (ctx *CmdCtx) StorageSchemaInfoRunE(_ *cobra.Command, _ []string) (err erro
|
|||
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() {
|
||||
_ = ctx.providers.StorageProvider.Close()
|
||||
}()
|
||||
|
@ -443,11 +443,11 @@ func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []stri
|
|||
count := 0
|
||||
|
||||
var (
|
||||
devices []model.WebauthnDevice
|
||||
devices []model.WebAuthnDevice
|
||||
)
|
||||
|
||||
export := &model.WebauthnDeviceExport{
|
||||
WebauthnDevices: nil,
|
||||
export := &model.WebAuthnDeviceExport{
|
||||
WebAuthnDevices: nil,
|
||||
}
|
||||
|
||||
for page := 0; true; page++ {
|
||||
|
@ -455,7 +455,7 @@ func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []stri
|
|||
return err
|
||||
}
|
||||
|
||||
export.WebauthnDevices = append(export.WebauthnDevices, devices...)
|
||||
export.WebAuthnDevices = append(export.WebAuthnDevices, 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)
|
||||
}
|
||||
|
||||
fmt.Printf(cliOutputFmtSuccessfulUserExportFile, count, "Webauthn devices", "YAML", filename)
|
||||
fmt.Printf(cliOutputFmtSuccessfulUserExportFile, count, "WebAuthn devices", "YAML", filename)
|
||||
|
||||
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() {
|
||||
_ = ctx.providers.StorageProvider.Close()
|
||||
}()
|
||||
|
@ -507,46 +507,46 @@ func (ctx *CmdCtx) StorageUserWebauthnImportRunE(cmd *cobra.Command, args []stri
|
|||
return err
|
||||
}
|
||||
|
||||
export := &model.WebauthnDeviceExport{}
|
||||
export := &model.WebAuthnDeviceExport{}
|
||||
|
||||
if err = yaml.Unmarshal(data, export); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(export.WebauthnDevices) == 0 {
|
||||
return fmt.Errorf("can't import a YAML file without Webauthn devices data")
|
||||
if len(export.WebAuthnDevices) == 0 {
|
||||
return fmt.Errorf("can't import a YAML file without WebAuthn devices data")
|
||||
}
|
||||
|
||||
if err = ctx.CheckSchema(); err != nil {
|
||||
return storageWrapCheckSchemaErr(err)
|
||||
}
|
||||
|
||||
for _, device := range export.WebauthnDevices {
|
||||
for _, device := range export.WebAuthnDevices {
|
||||
if err = ctx.providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(cliOutputFmtSuccessfulUserImportFile, len(export.WebauthnDevices), "Webauthn devices", "YAML", filename)
|
||||
fmt.Printf(cliOutputFmtSuccessfulUserImportFile, len(export.WebAuthnDevices), "WebAuthn devices", "YAML", filename)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageUserWebauthnListRunE is the RunE for the authelia storage user webauthn list command.
|
||||
func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
// StorageUserWebAuthnListRunE is the RunE for the authelia storage user webauthn list command.
|
||||
func (ctx *CmdCtx) StorageUserWebAuthnListRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
defer func() {
|
||||
_ = ctx.providers.StorageProvider.Close()
|
||||
}()
|
||||
|
||||
if len(args) == 0 || args[0] == "" {
|
||||
return ctx.StorageUserWebauthnListAllRunE(cmd, args)
|
||||
return ctx.StorageUserWebAuthnListAllRunE(cmd, args)
|
||||
}
|
||||
|
||||
if err = ctx.CheckSchema(); err != nil {
|
||||
return storageWrapCheckSchemaErr(err)
|
||||
}
|
||||
|
||||
var devices []model.WebauthnDevice
|
||||
var devices []model.WebAuthnDevice
|
||||
|
||||
user := args[0]
|
||||
|
||||
|
@ -558,7 +558,7 @@ func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string
|
|||
case err != nil:
|
||||
return fmt.Errorf("can't list devices for user '%s': %w", user, err)
|
||||
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")
|
||||
|
||||
for _, device := range devices {
|
||||
|
@ -569,8 +569,8 @@ func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string
|
|||
return nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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) {
|
||||
defer func() {
|
||||
_ = ctx.providers.StorageProvider.Close()
|
||||
}()
|
||||
|
@ -579,7 +579,7 @@ func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string)
|
|||
return storageWrapCheckSchemaErr(err)
|
||||
}
|
||||
|
||||
var devices []model.WebauthnDevice
|
||||
var devices []model.WebAuthnDevice
|
||||
|
||||
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())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageUserWebauthnDeleteRunE is the RunE for the authelia storage user webauthn delete command.
|
||||
func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
// StorageUserWebAuthnDeleteRunE is the RunE for the authelia storage user webauthn delete command.
|
||||
func (ctx *CmdCtx) StorageUserWebAuthnDeleteRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
defer func() {
|
||||
_ = ctx.providers.StorageProvider.Close()
|
||||
}()
|
||||
|
@ -624,7 +624,7 @@ func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []stri
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete webauthn device with username '%s' and description '%s': %w", user, description, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully deleted Webauthn device with description '%s' for user '%s'\n", description, user)
|
||||
fmt.Printf("Successfully deleted WebAuthn device with description '%s' for user '%s'\n", description, user)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -215,13 +215,14 @@ const (
|
|||
func loadXEnvCLIConfigValues(cmd *cobra.Command) (configs []string, filters []configuration.FileFilter, err error) {
|
||||
var (
|
||||
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
|
||||
}
|
||||
|
||||
if configs, err = loadXNormalizedPaths(configs); err != nil {
|
||||
if configs, err = loadXNormalizedPaths(configs, result); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
|
@ -236,7 +237,7 @@ func loadXEnvCLIConfigValues(cmd *cobra.Command) (configs []string, filters []co
|
|||
return
|
||||
}
|
||||
|
||||
func loadXNormalizedPaths(paths []string) ([]string, error) {
|
||||
func loadXNormalizedPaths(paths []string, result XEnvCLIResult) ([]string, error) {
|
||||
var (
|
||||
configs, files, dirs []string
|
||||
err error
|
||||
|
@ -258,10 +259,15 @@ func loadXNormalizedPaths(paths []string) ([]string, error) {
|
|||
files = append(files, path)
|
||||
default:
|
||||
if os.IsNotExist(err) {
|
||||
configs = append(configs, path)
|
||||
files = append(files, path)
|
||||
switch result {
|
||||
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)
|
||||
|
|
|
@ -96,6 +96,7 @@ func TestLoadXNormalizedPaths(t *testing.T) {
|
|||
ayml := filepath.Join(configdir, "a.yml")
|
||||
byml := filepath.Join(configdir, "b.yml")
|
||||
cyml := filepath.Join(otherdir, "c.yml")
|
||||
dyml := filepath.Join(otherdir, "d.yml")
|
||||
|
||||
file, err = os.Create(ayml)
|
||||
|
||||
|
@ -142,30 +143,44 @@ func TestLoadXNormalizedPaths(t *testing.T) {
|
|||
|
||||
testCases := []struct {
|
||||
name string
|
||||
haveX XEnvCLIResult
|
||||
have, expected []string
|
||||
expectedErr string
|
||||
}{
|
||||
{"ShouldAllowFiles",
|
||||
XEnvCLIResultCLIImplicit, []string{ayml},
|
||||
[]string{ayml},
|
||||
[]string{ayml}, "",
|
||||
"",
|
||||
},
|
||||
{"ShouldSkipFilesNotExistImplicit",
|
||||
XEnvCLIResultCLIImplicit, []string{dyml},
|
||||
[]string(nil),
|
||||
"",
|
||||
},
|
||||
{"ShouldNotErrFilesNotExistExplicit",
|
||||
XEnvCLIResultCLIExplicit, []string{dyml},
|
||||
[]string{dyml},
|
||||
"",
|
||||
},
|
||||
{"ShouldAllowDirectories",
|
||||
XEnvCLIResultCLIImplicit, []string{configdir},
|
||||
[]string{configdir},
|
||||
[]string{configdir}, "",
|
||||
"",
|
||||
},
|
||||
{"ShouldAllowFilesDirectories",
|
||||
XEnvCLIResultCLIImplicit, []string{ayml, otherdir},
|
||||
[]string{ayml, otherdir},
|
||||
[]string{ayml, otherdir}, "",
|
||||
"",
|
||||
},
|
||||
{"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),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
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)
|
||||
|
||||
|
|
|
@ -63,6 +63,28 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
|
|||
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 {
|
||||
rfc := fosite.ErrorToRFC6749Error(err)
|
||||
|
||||
|
@ -175,9 +197,19 @@ func OpenIDConnectPushedAuthorizationRequest(ctx *middlewares.AutheliaCtx, rw ht
|
|||
if err = client.ValidatePKCEPolicy(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 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
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
|
|||
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
|
||||
var (
|
||||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
user *model.WebAuthnUser
|
||||
userSession session.UserSession
|
||||
err error
|
||||
)
|
||||
|
@ -94,7 +94,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
var (
|
||||
err error
|
||||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
user *model.WebAuthnUser
|
||||
|
||||
userSession session.UserSession
|
||||
|
||||
|
@ -150,7 +150,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
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 {
|
||||
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
|
||||
var (
|
||||
w *webauthn.WebAuthn
|
||||
user *model.WebauthnUser
|
||||
user *model.WebAuthnUser
|
||||
userSession session.UserSession
|
||||
err error
|
||||
)
|
||||
|
@ -134,7 +134,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
var (
|
||||
assertionResponse *protocol.ParsedCredentialAssertionData
|
||||
credential *webauthn.Credential
|
||||
user *model.WebauthnUser
|
||||
user *model.WebAuthnUser
|
||||
)
|
||||
|
||||
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
|||
{
|
||||
db: model.UserInfo{
|
||||
Method: "webauthn",
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
HasTOTP: true,
|
||||
},
|
||||
err: nil,
|
||||
|
@ -70,7 +70,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
|||
{
|
||||
db: model.UserInfo{
|
||||
Method: "webauthn",
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
HasTOTP: false,
|
||||
},
|
||||
err: nil,
|
||||
|
@ -78,7 +78,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
|
|||
{
|
||||
db: model.UserInfo{
|
||||
Method: "mobile_push",
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasTOTP: false,
|
||||
},
|
||||
err: nil,
|
||||
|
@ -128,7 +128,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(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) {
|
||||
|
@ -160,13 +160,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
|||
db: model.UserInfo{
|
||||
Method: "",
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasDuo: false,
|
||||
},
|
||||
api: &model.UserInfo{
|
||||
Method: "totp",
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasDuo: false,
|
||||
},
|
||||
config: &schema.Configuration{},
|
||||
|
@ -178,13 +178,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
|||
db: model.UserInfo{
|
||||
Method: "",
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasDuo: true,
|
||||
},
|
||||
api: &model.UserInfo{
|
||||
Method: "mobile_push",
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasDuo: true,
|
||||
},
|
||||
config: &schema.Configuration{},
|
||||
|
@ -196,13 +196,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
|||
db: model.UserInfo{
|
||||
Method: "",
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasDuo: true,
|
||||
},
|
||||
api: &model.UserInfo{
|
||||
Method: "totp",
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasDuo: true,
|
||||
},
|
||||
config: &schema.Configuration{DuoAPI: schema.DuoAPIConfiguration{Disable: true}},
|
||||
|
@ -214,13 +214,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
|||
db: model.UserInfo{
|
||||
Method: "",
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
HasDuo: true,
|
||||
},
|
||||
api: &model.UserInfo{
|
||||
Method: "webauthn",
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
HasDuo: true,
|
||||
},
|
||||
config: &schema.Configuration{
|
||||
|
@ -236,13 +236,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
|
|||
db: model.UserInfo{
|
||||
Method: "",
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
HasDuo: false,
|
||||
},
|
||||
api: &model.UserInfo{
|
||||
Method: "totp",
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
HasDuo: true,
|
||||
},
|
||||
config: &schema.Configuration{},
|
||||
|
@ -322,7 +322,7 @@ func TestUserInfoEndpoint_SetDefaultMethod(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) {
|
||||
|
|
|
@ -12,8 +12,8 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
func getWebAuthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSession) (user *model.WebauthnUser, err error) {
|
||||
user = &model.WebauthnUser{
|
||||
func getWebAuthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSession) (user *model.WebAuthnUser, err error) {
|
||||
user = &model.WebAuthnUser{
|
||||
Username: userSession.Username,
|
||||
DisplayName: userSession.DisplayName,
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ func TestWebauthnGetUser(t *testing.T) {
|
|||
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,
|
||||
RPID: "https://example.com",
|
||||
|
@ -106,7 +106,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
|
|||
Username: "john",
|
||||
}
|
||||
|
||||
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebauthnDevice{
|
||||
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{
|
||||
{
|
||||
ID: 1,
|
||||
RPID: "https://example.com",
|
||||
|
|
|
@ -50,7 +50,7 @@ func (ctx *AutheliaCtx) AvailableSecondFactorMethods() (methods []string) {
|
|||
}
|
||||
|
||||
if !ctx.Configuration.Webauthn.Disable {
|
||||
methods = append(methods, model.SecondFactorMethodWebauthn)
|
||||
methods = append(methods, model.SecondFactorMethodWebAuthn)
|
||||
}
|
||||
|
||||
if !ctx.Configuration.DuoAPI.Disable {
|
||||
|
|
|
@ -235,15 +235,15 @@ func TestShouldReturnCorrectSecondFactorMethods(t *testing.T) {
|
|||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -421,10 +421,10 @@ func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifiers(arg0 interface{}) *
|
|||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "LoadWebauthnDevices", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]model.WebauthnDevice)
|
||||
ret0, _ := ret[0].([]model.WebAuthnDevice)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
@ -436,10 +436,10 @@ func (mr *MockStorageMockRecorder) LoadWebauthnDevices(arg0, arg1, arg2 interfac
|
|||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "LoadWebauthnDevicesByUsername", arg0, arg1)
|
||||
ret0, _ := ret[0].([]model.WebauthnDevice)
|
||||
ret0, _ := ret[0].([]model.WebAuthnDevice)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
@ -690,7 +690,7 @@ func (mr *MockStorageMockRecorder) SaveUserOpaqueIdentifier(arg0, arg1 interface
|
|||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "SaveWebauthnDevice", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
|
|
|
@ -15,8 +15,8 @@ const (
|
|||
// SecondFactorMethodTOTP method using Time-Based One-Time Password applications like Google Authenticator.
|
||||
SecondFactorMethodTOTP = "totp"
|
||||
|
||||
// SecondFactorMethodWebauthn method using Webauthn devices like YubiKey's.
|
||||
SecondFactorMethodWebauthn = "webauthn"
|
||||
// SecondFactorMethodWebAuthn method using WebAuthn devices like YubiKey's.
|
||||
SecondFactorMethodWebAuthn = "webauthn"
|
||||
|
||||
// SecondFactorMethodDuo method using Duo application to receive push notifications.
|
||||
SecondFactorMethodDuo = "mobile_push"
|
||||
|
|
|
@ -25,9 +25,12 @@ type TOTPConfiguration struct {
|
|||
Secret []byte `db:"secret" json:"-"`
|
||||
}
|
||||
|
||||
// LastUsed provides LastUsedAt as a *time.Time instead of sql.NullTime.
|
||||
func (c *TOTPConfiguration) LastUsed() *time.Time {
|
||||
if c.LastUsedAt.Valid {
|
||||
return &c.LastUsedAt.Time
|
||||
value := time.Unix(c.LastUsedAt.Time.Unix(), int64(c.LastUsedAt.Time.Nanosecond()))
|
||||
|
||||
return &value
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -73,9 +76,9 @@ func (c *TOTPConfiguration) Image(width, height int) (img image.Image, err error
|
|||
return key.Image(width, height)
|
||||
}
|
||||
|
||||
// MarshalYAML marshals this model into YAML.
|
||||
func (c *TOTPConfiguration) MarshalYAML() (any, error) {
|
||||
o := TOTPConfigurationData{
|
||||
// ToData converts this TOTPConfiguration into the data format for exporting etc.
|
||||
func (c *TOTPConfiguration) ToData() TOTPConfigurationData {
|
||||
return TOTPConfigurationData{
|
||||
CreatedAt: c.CreatedAt,
|
||||
LastUsedAt: c.LastUsed(),
|
||||
Username: c.Username,
|
||||
|
@ -85,8 +88,11 @@ func (c *TOTPConfiguration) MarshalYAML() (any, error) {
|
|||
Period: c.Period,
|
||||
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.
|
||||
|
@ -127,7 +133,30 @@ type TOTPConfigurationData struct {
|
|||
Secret string `yaml:"secret"`
|
||||
}
|
||||
|
||||
// TOTPConfigurationDataExport represents a TOTPConfiguration export file.
|
||||
type TOTPConfigurationDataExport struct {
|
||||
TOTPConfigurations []TOTPConfigurationData `yaml:"totp_configurations"`
|
||||
}
|
||||
|
||||
// TOTPConfigurationExport represents a TOTPConfiguration export file.
|
||||
type TOTPConfigurationExport struct {
|
||||
TOTPConfigurations []TOTPConfiguration `yaml:"totp_configurations"`
|
||||
}
|
||||
|
||||
// ToData converts this TOTPConfigurationExport into a TOTPConfigurationDataExport.
|
||||
func (export TOTPConfigurationExport) ToData() TOTPConfigurationDataExport {
|
||||
data := TOTPConfigurationDataExport{
|
||||
TOTPConfigurations: make([]TOTPConfigurationData, len(export.TOTPConfigurations)),
|
||||
}
|
||||
|
||||
for i, config := range export.TOTPConfigurations {
|
||||
data.TOTPConfigurations[i] = config.ToData()
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// MarshalYAML marshals this model into YAML.
|
||||
func (export TOTPConfigurationExport) MarshalYAML() (any, error) {
|
||||
return export.ToData(), nil
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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().Dy())
|
||||
}
|
||||
|
||||
func TestTOTPConfigurationImportExport(t *testing.T) {
|
||||
have := TOTPConfigurationExport{
|
||||
TOTPConfigurations: []TOTPConfiguration{
|
||||
{
|
||||
ID: 0,
|
||||
CreatedAt: time.Now(),
|
||||
LastUsedAt: sql.NullTime{Valid: false},
|
||||
Username: "john",
|
||||
Issuer: "example",
|
||||
Algorithm: "SHA1",
|
||||
Digits: 6,
|
||||
Period: 30,
|
||||
Secret: MustRead(80),
|
||||
},
|
||||
{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
LastUsedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||
Username: "abc",
|
||||
Issuer: "example2",
|
||||
Algorithm: "SHA512",
|
||||
Digits: 8,
|
||||
Period: 90,
|
||||
Secret: MustRead(120),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(&have)
|
||||
require.NoError(t, err)
|
||||
|
||||
imported := TOTPConfigurationExport{}
|
||||
|
||||
require.NoError(t, yaml.Unmarshal(out, &imported))
|
||||
|
||||
require.Equal(t, len(have.TOTPConfigurations), len(imported.TOTPConfigurations))
|
||||
|
||||
for i, actual := range imported.TOTPConfigurations {
|
||||
t.Run(actual.Username, func(t *testing.T) {
|
||||
expected := have.TOTPConfigurations[i]
|
||||
|
||||
if expected.ID != 0 {
|
||||
assert.NotEqual(t, expected.ID, actual.ID)
|
||||
} else {
|
||||
assert.Equal(t, expected.ID, actual.ID)
|
||||
}
|
||||
|
||||
assert.Equal(t, expected.Username, actual.Username)
|
||||
assert.Equal(t, expected.Issuer, actual.Issuer)
|
||||
assert.Equal(t, expected.Algorithm, actual.Algorithm)
|
||||
assert.Equal(t, expected.Digits, actual.Digits)
|
||||
assert.Equal(t, expected.Period, actual.Period)
|
||||
assert.WithinDuration(t, expected.CreatedAt, actual.CreatedAt, time.Second)
|
||||
assert.WithinDuration(t, expected.LastUsedAt.Time, actual.LastUsedAt.Time, time.Second)
|
||||
assert.Equal(t, expected.LastUsedAt.Valid, actual.LastUsedAt.Valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,11 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"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) {
|
||||
ip := IP{}
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ type UserInfo struct {
|
|||
// True if a TOTP device has been registered.
|
||||
HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"`
|
||||
|
||||
// True if a Webauthn device has been registered.
|
||||
HasWebauthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
|
||||
// True if a WebAuthn device has been registered.
|
||||
HasWebAuthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
|
||||
|
||||
// True if a duo device has been configured as the preferred.
|
||||
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
|
||||
|
||||
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) {
|
||||
i.Method = fallback
|
||||
|
@ -50,8 +50,8 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
|
|||
switch {
|
||||
case i.HasTOTP && totp:
|
||||
i.Method = SecondFactorMethodTOTP
|
||||
case i.HasWebauthn && webauthn:
|
||||
i.Method = SecondFactorMethodWebauthn
|
||||
case i.HasWebAuthn && webauthn:
|
||||
i.Method = SecondFactorMethodWebAuthn
|
||||
case i.HasDuo && duo:
|
||||
i.Method = SecondFactorMethodDuo
|
||||
case fallback != "" && utils.IsStringInSlice(fallback, methods):
|
||||
|
@ -59,7 +59,7 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
|
|||
case totp:
|
||||
i.Method = SecondFactorMethodTOTP
|
||||
case webauthn:
|
||||
i.Method = SecondFactorMethodWebauthn
|
||||
i.Method = SecondFactorMethodWebAuthn
|
||||
case duo:
|
||||
i.Method = SecondFactorMethodDuo
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
|
||||
has := ""
|
||||
|
||||
if have.HasTOTP || have.HasDuo || have.HasWebauthn {
|
||||
if have.HasTOTP || have.HasDuo || have.HasWebAuthn {
|
||||
has += " has"
|
||||
|
||||
if have.HasTOTP {
|
||||
|
@ -31,8 +31,8 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
has += " " + SecondFactorMethodDuo
|
||||
}
|
||||
|
||||
if have.HasWebauthn {
|
||||
has += " " + SecondFactorMethodWebauthn
|
||||
if have.HasWebAuthn {
|
||||
has += " " + SecondFactorMethodWebAuthn
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,60 +62,60 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: true,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: true,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
have: UserInfo{
|
||||
HasDuo: true,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: true,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
have: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: true,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: true,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodTOTP},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
have: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodTOTP},
|
||||
changed: true,
|
||||
|
@ -125,15 +125,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodWebauthn},
|
||||
methods: []string{SecondFactorMethodWebAuthn},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
|
@ -141,31 +141,31 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodDuo,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodDuo},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
have: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||
changed: false,
|
||||
},
|
||||
{
|
||||
|
@ -173,15 +173,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: "",
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
|
@ -189,13 +189,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: "",
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodDuo,
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
methods: []string{SecondFactorMethodDuo},
|
||||
changed: true,
|
||||
|
@ -205,13 +205,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: "",
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: "",
|
||||
HasDuo: false,
|
||||
HasTOTP: true,
|
||||
HasWebauthn: true,
|
||||
HasWebAuthn: true,
|
||||
},
|
||||
methods: nil,
|
||||
changed: false,
|
||||
|
@ -221,15 +221,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: "",
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodDuo,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||
fallback: SecondFactorMethodDuo,
|
||||
changed: true,
|
||||
},
|
||||
|
@ -238,15 +238,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: "",
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn},
|
||||
fallback: SecondFactorMethodDuo,
|
||||
changed: true,
|
||||
},
|
||||
|
@ -255,15 +255,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: true,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodDuo,
|
||||
HasDuo: true,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
|
@ -271,30 +271,30 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
|||
Method: SecondFactorMethodTOTP,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||
fallback: SecondFactorMethodWebauthn,
|
||||
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||
fallback: SecondFactorMethodWebAuthn,
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
have: UserInfo{
|
||||
Method: SecondFactorMethodWebauthn,
|
||||
Method: SecondFactorMethodWebAuthn,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
want: UserInfo{
|
||||
Method: SecondFactorMethodDuo,
|
||||
HasDuo: false,
|
||||
HasTOTP: false,
|
||||
HasWebauthn: false,
|
||||
HasWebAuthn: false,
|
||||
},
|
||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo},
|
||||
fallback: SecondFactorMethodDuo,
|
||||
|
|
|
@ -17,15 +17,15 @@ const (
|
|||
attestationTypeFIDOU2F = "fido-u2f"
|
||||
)
|
||||
|
||||
// WebauthnUser is an object to represent a user for the Webauthn lib.
|
||||
type WebauthnUser struct {
|
||||
// WebAuthnUser is an object to represent a user for the WebAuthn lib.
|
||||
type WebAuthnUser struct {
|
||||
Username string
|
||||
DisplayName string
|
||||
Devices []WebauthnDevice
|
||||
Devices []WebAuthnDevice
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if c.AttestationType == attestationTypeFIDOU2F {
|
||||
return true
|
||||
|
@ -36,27 +36,27 @@ func (w WebauthnUser) HasFIDOU2F() bool {
|
|||
}
|
||||
|
||||
// WebAuthnID implements the webauthn.User interface.
|
||||
func (w WebauthnUser) WebAuthnID() []byte {
|
||||
func (w WebAuthnUser) WebAuthnID() []byte {
|
||||
return []byte(w.Username)
|
||||
}
|
||||
|
||||
// WebAuthnName implements the webauthn.User interface.
|
||||
func (w WebauthnUser) WebAuthnName() string {
|
||||
func (w WebAuthnUser) WebAuthnName() string {
|
||||
return w.Username
|
||||
}
|
||||
|
||||
// WebAuthnDisplayName implements the webauthn.User interface.
|
||||
func (w WebauthnUser) WebAuthnDisplayName() string {
|
||||
func (w WebAuthnUser) WebAuthnDisplayName() string {
|
||||
return w.DisplayName
|
||||
}
|
||||
|
||||
// WebAuthnIcon implements the webauthn.User interface.
|
||||
func (w WebauthnUser) WebAuthnIcon() string {
|
||||
func (w WebAuthnUser) WebAuthnIcon() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
||||
var credential webauthn.Credential
|
||||
|
@ -96,7 +96,7 @@ func (w WebauthnUser) WebAuthnCredentials() (credentials []webauthn.Credential)
|
|||
}
|
||||
|
||||
// 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()
|
||||
|
||||
descriptors = make([]protocol.CredentialDescriptor, len(credentials))
|
||||
|
@ -108,15 +108,15 @@ func (w WebauthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.Cr
|
|||
return descriptors
|
||||
}
|
||||
|
||||
// NewWebauthnDeviceFromCredential creates a WebauthnDevice from a webauthn.Credential.
|
||||
func NewWebauthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebauthnDevice) {
|
||||
// NewWebAuthnDeviceFromCredential creates a WebAuthnDevice from a webauthn.Credential.
|
||||
func NewWebAuthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebAuthnDevice) {
|
||||
transport := make([]string, len(credential.Transport))
|
||||
|
||||
for i, t := range credential.Transport {
|
||||
transport[i] = string(t)
|
||||
}
|
||||
|
||||
device = WebauthnDevice{
|
||||
device = WebAuthnDevice{
|
||||
RPID: rpid,
|
||||
Username: username,
|
||||
CreatedAt: time.Now(),
|
||||
|
@ -137,8 +137,8 @@ func NewWebauthnDeviceFromCredential(rpid, username, description string, credent
|
|||
return device
|
||||
}
|
||||
|
||||
// WebauthnDevice represents a Webauthn Device in the database storage.
|
||||
type WebauthnDevice struct {
|
||||
// WebAuthnDevice represents a WebAuthn Device in the database storage.
|
||||
type WebAuthnDevice struct {
|
||||
ID int `db:"id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
LastUsedAt sql.NullTime `db:"last_used_at"`
|
||||
|
@ -154,8 +154,8 @@ type WebauthnDevice struct {
|
|||
CloneWarning bool `db:"clone_warning"`
|
||||
}
|
||||
|
||||
// UpdateSignInInfo adjusts the values of the WebauthnDevice after a sign in.
|
||||
func (d *WebauthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
|
||||
// UpdateSignInInfo adjusts the values of the WebAuthnDevice after a sign in.
|
||||
func (d *WebAuthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
|
||||
d.LastUsedAt = sql.NullTime{Time: now, Valid: true}
|
||||
|
||||
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 {
|
||||
return &d.LastUsedAt.Time
|
||||
value := time.Unix(d.LastUsedAt.Time.Unix(), int64(d.LastUsedAt.Time.Nanosecond()))
|
||||
|
||||
return &value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML marshals this model into YAML.
|
||||
func (d *WebauthnDevice) MarshalYAML() (any, error) {
|
||||
o := WebauthnDeviceData{
|
||||
// ToData converts this WebAuthnDevice into the data format for exporting etc.
|
||||
func (d *WebAuthnDevice) ToData() WebAuthnDeviceData {
|
||||
return WebAuthnDeviceData{
|
||||
CreatedAt: d.CreatedAt,
|
||||
LastUsedAt: d.LastUsed(),
|
||||
RPID: d.RPID,
|
||||
|
@ -196,13 +199,16 @@ func (d *WebauthnDevice) MarshalYAML() (any, error) {
|
|||
SignCount: d.SignCount,
|
||||
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.
|
||||
func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||
o := &WebauthnDeviceData{}
|
||||
func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||
o := &WebAuthnDeviceData{}
|
||||
|
||||
if err = value.Decode(o); err != nil {
|
||||
return err
|
||||
|
@ -246,8 +252,8 @@ func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// WebauthnDeviceData represents a Webauthn Device in the database storage.
|
||||
type WebauthnDeviceData struct {
|
||||
// WebAuthnDeviceData represents a WebAuthn Device in the database storage.
|
||||
type WebAuthnDeviceData struct {
|
||||
CreatedAt time.Time `yaml:"created_at"`
|
||||
LastUsedAt *time.Time `yaml:"last_used_at"`
|
||||
RPID string `yaml:"rpid"`
|
||||
|
@ -262,7 +268,30 @@ type WebauthnDeviceData struct {
|
|||
CloneWarning bool `yaml:"clone_warning"`
|
||||
}
|
||||
|
||||
// WebauthnDeviceExport represents a WebauthnDevice export file.
|
||||
type WebauthnDeviceExport struct {
|
||||
WebauthnDevices []WebauthnDevice `yaml:"webauthn_devices"`
|
||||
// WebAuthnDeviceExport represents a WebAuthnDevice export file.
|
||||
type WebAuthnDeviceExport struct {
|
||||
WebAuthnDevices []WebAuthnDevice `yaml:"webauthn_devices"`
|
||||
}
|
||||
|
||||
// WebAuthnDeviceDataExport represents a WebAuthnDevice export file.
|
||||
type WebAuthnDeviceDataExport struct {
|
||||
WebAuthnDevices []WebAuthnDeviceData `yaml:"webauthn_devices"`
|
||||
}
|
||||
|
||||
// ToData converts this WebAuthnDeviceExport into a WebAuthnDeviceDataExport.
|
||||
func (export WebAuthnDeviceExport) ToData() WebAuthnDeviceDataExport {
|
||||
data := WebAuthnDeviceDataExport{
|
||||
WebAuthnDevices: make([]WebAuthnDeviceData, len(export.WebAuthnDevices)),
|
||||
}
|
||||
|
||||
for i, device := range export.WebAuthnDevices {
|
||||
data.WebAuthnDevices[i] = device.ToData()
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// MarshalYAML marshals this model into YAML.
|
||||
func (export WebAuthnDeviceExport) MarshalYAML() (any, error) {
|
||||
return export.ToData(), nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestWebAuthnDeviceImportExport(t *testing.T) {
|
||||
have := WebAuthnDeviceExport{
|
||||
WebAuthnDevices: []WebAuthnDevice{
|
||||
{
|
||||
ID: 0,
|
||||
CreatedAt: time.Now(),
|
||||
LastUsedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||
RPID: "example",
|
||||
Username: "john",
|
||||
Description: "akey",
|
||||
KID: NewBase64(MustRead(20)),
|
||||
PublicKey: MustRead(128),
|
||||
AttestationType: "fido-u2f",
|
||||
Transport: "",
|
||||
AAGUID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
SignCount: 20,
|
||||
CloneWarning: false,
|
||||
},
|
||||
{
|
||||
ID: 0,
|
||||
CreatedAt: time.Now(),
|
||||
LastUsedAt: sql.NullTime{Valid: false},
|
||||
RPID: "example2",
|
||||
Username: "john2",
|
||||
Description: "bkey",
|
||||
KID: NewBase64(MustRead(60)),
|
||||
PublicKey: MustRead(64),
|
||||
AttestationType: "packed",
|
||||
Transport: "",
|
||||
AAGUID: uuid.NullUUID{Valid: false},
|
||||
SignCount: 30,
|
||||
CloneWarning: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(&have)
|
||||
require.NoError(t, err)
|
||||
|
||||
imported := WebAuthnDeviceExport{}
|
||||
|
||||
require.NoError(t, yaml.Unmarshal(out, &imported))
|
||||
require.Equal(t, len(have.WebAuthnDevices), len(imported.WebAuthnDevices))
|
||||
|
||||
for i, actual := range imported.WebAuthnDevices {
|
||||
t.Run(actual.Description, func(t *testing.T) {
|
||||
expected := have.WebAuthnDevices[i]
|
||||
|
||||
assert.Equal(t, expected.KID, actual.KID)
|
||||
assert.Equal(t, expected.PublicKey, actual.PublicKey)
|
||||
assert.Equal(t, expected.SignCount, actual.SignCount)
|
||||
assert.Equal(t, expected.AttestationType, actual.AttestationType)
|
||||
assert.Equal(t, expected.RPID, actual.RPID)
|
||||
assert.Equal(t, expected.AAGUID.Valid, actual.AAGUID.Valid)
|
||||
assert.Equal(t, expected.AAGUID.UUID, actual.AAGUID.UUID)
|
||||
assert.WithinDuration(t, expected.CreatedAt, actual.CreatedAt, time.Second)
|
||||
assert.WithinDuration(t, expected.LastUsedAt.Time, actual.LastUsedAt.Time, time.Second)
|
||||
assert.Equal(t, expected.LastUsedAt.Valid, actual.LastUsedAt.Valid)
|
||||
assert.Equal(t, expected.CloneWarning, actual.CloneWarning)
|
||||
assert.Equal(t, expected.Description, actual.Description)
|
||||
assert.Equal(t, expected.Username, actual.Username)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func MustRead(n int) []byte {
|
||||
data := make([]byte, n)
|
||||
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/x/errorsx"
|
||||
|
||||
|
@ -30,7 +28,7 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
|
|||
RedirectURIs: config.RedirectURIs,
|
||||
GrantTypes: config.GrantTypes,
|
||||
ResponseTypes: config.ResponseTypes,
|
||||
ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault},
|
||||
ResponseModes: []fosite.ResponseModeType{},
|
||||
|
||||
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.
|
||||
func (c *Client) ValidatePARPolicy(r fosite.Requester, prefix string) (err error) {
|
||||
form := r.GetRequestForm()
|
||||
|
||||
if c.EnforcePAR {
|
||||
if requestURI := form.Get(FormParameterRequestURI); !strings.HasPrefix(requestURI, prefix) {
|
||||
if requestURI == "" {
|
||||
if !IsPushedAuthorizedRequest(r, prefix) {
|
||||
switch requestURI := r.GetRequestForm().Get(FormParameterRequestURI); requestURI {
|
||||
case "":
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool {
|
||||
if level == authentication.NotAuthenticated {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
|
@ -19,8 +20,7 @@ func TestNewClient(t *testing.T) {
|
|||
assert.Equal(t, "", blankClient.ID)
|
||||
assert.Equal(t, "", blankClient.Description)
|
||||
assert.Equal(t, "", blankClient.Description)
|
||||
require.Len(t, blankClient.ResponseModes, 1)
|
||||
assert.Equal(t, fosite.ResponseModeDefault, blankClient.ResponseModes[0])
|
||||
assert.Len(t, blankClient.ResponseModes, 0)
|
||||
|
||||
exampleConfig := schema.OpenIDConnectClientConfiguration{
|
||||
ID: "myapp",
|
||||
|
@ -36,11 +36,10 @@ func TestNewClient(t *testing.T) {
|
|||
|
||||
exampleClient := NewClient(exampleConfig)
|
||||
assert.Equal(t, "myapp", exampleClient.ID)
|
||||
require.Len(t, exampleClient.ResponseModes, 4)
|
||||
assert.Equal(t, fosite.ResponseModeDefault, exampleClient.ResponseModes[0])
|
||||
assert.Equal(t, fosite.ResponseModeFormPost, exampleClient.ResponseModes[1])
|
||||
assert.Equal(t, fosite.ResponseModeQuery, exampleClient.ResponseModes[2])
|
||||
assert.Equal(t, fosite.ResponseModeFragment, exampleClient.ResponseModes[3])
|
||||
require.Len(t, exampleClient.ResponseModes, 3)
|
||||
assert.Equal(t, fosite.ResponseModeFormPost, exampleClient.ResponseModes[0])
|
||||
assert.Equal(t, fosite.ResponseModeQuery, exampleClient.ResponseModes[1])
|
||||
assert.Equal(t, fosite.ResponseModeFragment, exampleClient.ResponseModes[2])
|
||||
assert.Equal(t, authorization.TwoFactor, exampleClient.Policy)
|
||||
}
|
||||
|
||||
|
@ -226,6 +225,7 @@ func TestNewClientPKCE(t *testing.T) {
|
|||
expected string
|
||||
r *fosite.Request
|
||||
err string
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
"ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest",
|
||||
|
@ -235,6 +235,7 @@ func TestNewClientPKCE(t *testing.T) {
|
|||
"",
|
||||
&fosite.Request{},
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"ShouldEnforcePKCEAndErrorOnNonPKCERequest",
|
||||
|
@ -244,6 +245,7 @@ func TestNewClientPKCE(t *testing.T) {
|
|||
"",
|
||||
&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. 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",
|
||||
|
@ -253,6 +255,7 @@ func TestNewClientPKCE(t *testing.T) {
|
|||
"",
|
||||
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest",
|
||||
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
||||
|
@ -261,6 +264,7 @@ func TestNewClientPKCE(t *testing.T) {
|
|||
"S256",
|
||||
&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. Clients must include a code_challenge when performing the authorize code flow, but it is missing. The server is configured in a way that enforces PKCE for this client.",
|
||||
},
|
||||
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod",
|
||||
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
||||
|
@ -269,6 +273,7 @@ func TestNewClientPKCE(t *testing.T) {
|
|||
"S256",
|
||||
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
|
||||
"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",
|
||||
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
|
||||
|
@ -277,6 +282,7 @@ func TestNewClientPKCE(t *testing.T) {
|
|||
"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)
|
||||
|
||||
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 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 {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -112,6 +112,7 @@ const (
|
|||
|
||||
const (
|
||||
FormParameterRequestURI = "request_uri"
|
||||
FormParameterResponseMode = "response_mode"
|
||||
FormParameterCodeChallenge = "code_challenge"
|
||||
FormParameterCodeChallengeMethod = "code_challenge_method"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
)
|
||||
|
||||
// IsPushedAuthorizedRequest returns true if the requester has a PushedAuthorizationRequest redirect_uri value.
|
||||
func IsPushedAuthorizedRequest(r fosite.Requester, prefix string) bool {
|
||||
return strings.HasPrefix(r.GetRequestForm().Get(FormParameterRequestURI), prefix)
|
||||
}
|
|
@ -38,12 +38,12 @@ type Provider interface {
|
|||
LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error)
|
||||
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)
|
||||
DeleteWebauthnDevice(ctx context.Context, kid string) (err error)
|
||||
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (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)
|
||||
LoadWebauthnDevices(ctx context.Context, limit, page int) (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)
|
||||
DeletePreferredDuoDevice(ctx context.Context, username string) (err error)
|
||||
|
|
|
@ -882,7 +882,7 @@ func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page in
|
|||
}
|
||||
|
||||
// SaveWebauthnDevice saves a registered Webauthn device.
|
||||
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 {
|
||||
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.
|
||||
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error) {
|
||||
devices = make([]model.WebauthnDevice, 0, limit)
|
||||
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error) {
|
||||
devices = make([]model.WebAuthnDevice, 0, limit)
|
||||
|
||||
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevices, limit, limit*page); err != nil {
|
||||
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.
|
||||
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 errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNoWebauthnDevice
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
version: '3'
|
||||
services:
|
||||
envoy:
|
||||
image: envoyproxy/envoy:v1.25.4
|
||||
image: envoyproxy/envoy:v1.25.5
|
||||
volumes:
|
||||
- ./example/compose/envoy/envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./common/pki:/pki
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
</head>
|
||||
<body>
|
||||
<form method="post" action="{{ .RedirURL }}">
|
||||
{{ range $key,$value := .Parameters }}
|
||||
{{ range $parameter:= $value}}
|
||||
<input type="hidden" name="{{$key}}" value="{{$parameter}}"/>
|
||||
{{end}}
|
||||
{{ range $key, $value := .Parameters }}
|
||||
{{ range $parameter := $value }}
|
||||
<input type="hidden" name="{{ $key }}" value="{{ $parameter }}"/>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</form>
|
||||
</body>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
.env.test.local
|
||||
.env.production.local
|
||||
.eslintcache
|
||||
.vitest-preview
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
111
web/package.json
111
web/package.json
|
@ -6,15 +6,8 @@
|
|||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"@types/react": "18",
|
||||
"react": "18",
|
||||
"react-dom": "18"
|
||||
},
|
||||
"ignoreMissing": [
|
||||
"@babel/core",
|
||||
"@babel/plugin-syntax-flow",
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
"prop-types"
|
||||
]
|
||||
"react": "18"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -26,8 +19,8 @@
|
|||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@mui/icons-material": "5.11.16",
|
||||
"@mui/material": "5.11.16",
|
||||
"@mui/styles": "5.11.16",
|
||||
"@mui/material": "5.12.0",
|
||||
"@mui/styles": "5.12.0",
|
||||
"axios": "1.3.5",
|
||||
"broadcast-channel": "5.0.3",
|
||||
"classnames": "2.3.2",
|
||||
|
@ -49,83 +42,14 @@
|
|||
"build": "vite build",
|
||||
"coverage": "VITE_COVERAGE=true vite build",
|
||||
"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"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"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": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
|
@ -147,17 +71,17 @@
|
|||
"@limegrass/eslint-plugin-import-alias": "1.0.6",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/qrcode.react": "1.0.2",
|
||||
"@types/react": "18.0.33",
|
||||
"@types/react": "18.0.35",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/testing-library__jest-dom": "5.14.5",
|
||||
"@types/zxcvbn": "4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||
"@typescript-eslint/parser": "5.58.0",
|
||||
"@vitejs/plugin-react": "3.1.0",
|
||||
"esbuild": "0.17.15",
|
||||
"esbuild-jest": "0.5.0",
|
||||
"@vitest/coverage-istanbul": "0.30.1",
|
||||
"esbuild": "0.17.16",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
|
@ -168,11 +92,8 @@
|
|||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"happy-dom": "9.3.2",
|
||||
"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",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"typescript": "5.0.4",
|
||||
|
@ -180,6 +101,8 @@
|
|||
"vite-plugin-eslint": "1.8.1",
|
||||
"vite-plugin-istanbul": "4.0.1",
|
||||
"vite-plugin-svgr": "2.4.0",
|
||||
"vite-tsconfig-paths": "4.0.8"
|
||||
"vite-tsconfig-paths": "4.2.0",
|
||||
"vitest": "0.30.1",
|
||||
"vitest-preview": "0.0.1"
|
||||
}
|
||||
}
|
||||
|
|
6177
web/pnpm-lock.yaml
6177
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,34 @@
|
|||
import React from "react";
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
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", () => {
|
||||
render(<NotificationBar onClose={() => {}} />);
|
||||
});
|
||||
|
||||
it("displays notification message and level correctly", async () => {
|
||||
render(
|
||||
<NotificationsContext.Provider value={{ notification: testNotification, setNotification: () => {} }}>
|
||||
<NotificationBar onClose={() => {}} />
|
||||
</NotificationsContext.Provider>,
|
||||
);
|
||||
|
||||
const alert = await screen.getByRole("alert");
|
||||
const message = await screen.findByText(testNotification.message);
|
||||
|
||||
expect(alert).toHaveClass(
|
||||
`MuiAlert-filled${testNotification.level.charAt(0).toUpperCase() + testNotification.level.substring(1)}`,
|
||||
{ exact: false },
|
||||
);
|
||||
expect(message).toHaveTextContent(testNotification.message);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import React from "react";
|
||||
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach } from "vitest";
|
||||
|
||||
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
withTranslation: () => (Component: any) => {
|
||||
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
|
||||
return Component;
|
||||
},
|
||||
Trans: ({ children }: any) => children,
|
||||
useTranslation: () => {
|
||||
return {
|
||||
t: (str) => str,
|
||||
i18n: {
|
||||
changeLanguage: () => new Promise(() => {}),
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.setAttribute("data-privacypolicyurl", "");
|
||||
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||
|
||||
global.localStorage.clear();
|
||||
});
|
||||
|
||||
it("renders privacy policy and accepts when Accept button is clicked", () => {
|
||||
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||
document.body.setAttribute("data-privacypolicyaccept", "true");
|
||||
|
||||
const { container } = render(<PrivacyPolicyDrawer />);
|
||||
fireEvent.click(screen.getByText("Accept"));
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("does not render when privacy policy is disabled", () => {
|
||||
render(<PrivacyPolicyDrawer />);
|
||||
expect(screen.queryByText("Privacy Policy")).toBeNull();
|
||||
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
|
||||
expect(screen.queryByText("Accept")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render when acceptance is not required", () => {
|
||||
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||
|
||||
render(<PrivacyPolicyDrawer />);
|
||||
expect(screen.queryByText("Privacy Policy")).toBeNull();
|
||||
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
|
||||
expect(screen.queryByText("Accept")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render when already accepted", () => {
|
||||
global.localStorage.setItem("privacy-policy-accepted", "true");
|
||||
|
||||
const { container } = render(<PrivacyPolicyDrawer />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
withTranslation: () => (Component: any) => {
|
||||
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
|
||||
return Component;
|
||||
},
|
||||
Trans: ({ children }: any) => children,
|
||||
useTranslation: () => {
|
||||
return {
|
||||
t: (str) => str,
|
||||
i18n: {
|
||||
changeLanguage: () => new Promise(() => {}),
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
it("renders a link to the privacy policy with the correct text", () => {
|
||||
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||
|
||||
const { getByRole } = render(<PrivacyPolicyLink />);
|
||||
const link = getByRole("link");
|
||||
expect(link).toHaveAttribute("href", "http://example.com/privacy-policy");
|
||||
expect(link).toHaveTextContent("Privacy Policy");
|
||||
});
|
|
@ -1,9 +1,37 @@
|
|||
import React from "react";
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { act, render } from "@testing-library/react";
|
||||
|
||||
import TimerIcon from "@components/TimerIcon";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers().setSystemTime(new Date(2023, 1, 1, 8));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
render(<TimerIcon width={32} height={32} period={30} />);
|
||||
});
|
||||
|
||||
it("renders a timer icon with updating progress for a given period", async () => {
|
||||
const { container } = render(<TimerIcon width={32} height={32} period={30} />);
|
||||
const initialProgress =
|
||||
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
|
||||
"stroke-dasharray",
|
||||
);
|
||||
expect(initialProgress).toBe("0 31.6");
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(3000);
|
||||
});
|
||||
|
||||
const updatedProgress =
|
||||
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
|
||||
"stroke-dasharray",
|
||||
);
|
||||
expect(updatedProgress).toBe("3.16 31.6");
|
||||
expect(Number(updatedProgress!.split(/\s(.+)/)[0])).toBeGreaterThan(Number(initialProgress!.split(/\s(.+)/)[0]));
|
||||
});
|
||||
|
|
|
@ -2,12 +2,41 @@ import React from "react";
|
|||
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
import TypographyWithTooltip from "@components/TypographyWithTootip";
|
||||
import TypographyWithTooltip, { Props } from "@components/TypographyWithTooltip";
|
||||
|
||||
const defaultProps: Props = {
|
||||
variant: "h5",
|
||||
value: "Example",
|
||||
};
|
||||
|
||||
it("renders without crashing", () => {
|
||||
render(<TypographyWithTooltip value={"Example"} variant={"h5"} />);
|
||||
render(<TypographyWithTooltip {...defaultProps} />);
|
||||
});
|
||||
|
||||
it("renders with tooltip without crashing", () => {
|
||||
render(<TypographyWithTooltip value={"Example"} tooltip={"A tooltip"} variant={"h5"} />);
|
||||
const props: Props = {
|
||||
...defaultProps,
|
||||
tooltip: "A tooltip",
|
||||
};
|
||||
render(<TypographyWithTooltip {...props} />);
|
||||
});
|
||||
|
||||
it("renders the text correctly", () => {
|
||||
const props: Props = {
|
||||
...defaultProps,
|
||||
value: "Test text",
|
||||
};
|
||||
const { getByText } = render(<TypographyWithTooltip {...props} />);
|
||||
const element = getByText(props.value!);
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the tooltip correctly", () => {
|
||||
const props: Props = {
|
||||
...defaultProps,
|
||||
tooltip: "Test tooltip",
|
||||
};
|
||||
const { getByText } = render(<TypographyWithTooltip {...props} />);
|
||||
const element = getByText(props.value!);
|
||||
expect(element).toHaveAttribute("aria-label", props.tooltip);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
||||
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
|
||||
import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
|
||||
import TypographyWithTooltip from "@components/TypographyWithTootip";
|
||||
import TypographyWithTooltip from "@components/TypographyWithTooltip";
|
||||
import { getLogoOverride, getPrivacyPolicyEnabled } from "@utils/Configuration";
|
||||
|
||||
export interface Props {
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import "@testing-library/jest-dom";
|
||||
|
||||
document.body.setAttribute("data-basepath", "");
|
||||
document.body.setAttribute("data-duoselfenrollment", "true");
|
||||
document.body.setAttribute("data-rememberme", "true");
|
||||
document.body.setAttribute("data-resetpassword", "true");
|
||||
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
||||
document.body.setAttribute("data-privacypolicyurl", "");
|
||||
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||
document.body.setAttribute("data-theme", "light");
|
|
@ -0,0 +1,46 @@
|
|||
import matchers, { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
|
||||
|
||||
declare global {
|
||||
namespace Vi {
|
||||
interface JestAssertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend(matchers);
|
||||
|
||||
const localStorageMock = (function () {
|
||||
let store = {};
|
||||
|
||||
return {
|
||||
getItem(key) {
|
||||
return store[key];
|
||||
},
|
||||
|
||||
setItem(key, value) {
|
||||
store[key] = value;
|
||||
},
|
||||
|
||||
clear() {
|
||||
store = {};
|
||||
},
|
||||
|
||||
removeItem(key) {
|
||||
delete store[key];
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return store;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||
|
||||
document.body.setAttribute("data-basepath", "");
|
||||
document.body.setAttribute("data-duoselfenrollment", "true");
|
||||
document.body.setAttribute("data-rememberme", "true");
|
||||
document.body.setAttribute("data-resetpassword", "true");
|
||||
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
||||
document.body.setAttribute("data-privacypolicyurl", "");
|
||||
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||
document.body.setAttribute("data-theme", "light");
|
|
@ -21,7 +21,7 @@
|
|||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"types": ["@types/jest", "vite/client", "vite-plugin-svgr/client"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
@ -5,18 +5,17 @@ import istanbul from "vite-plugin-istanbul";
|
|||
import svgr from "vite-plugin-svgr";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
// @ts-ignore
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isCoverage = process.env.VITE_COVERAGE === "true";
|
||||
const sourcemap = isCoverage ? "inline" : undefined;
|
||||
|
||||
const istanbulPlugin = isCoverage
|
||||
? istanbul({
|
||||
include: "src/*",
|
||||
checkProd: false,
|
||||
exclude: ["node_modules"],
|
||||
extension: [".js", ".jsx", ".ts", ".tsx"],
|
||||
checkProd: false,
|
||||
forceBuildInstrument: true,
|
||||
include: "src/*",
|
||||
requireEnv: true,
|
||||
})
|
||||
: undefined;
|
||||
|
@ -24,14 +23,11 @@ export default defineConfig(({ mode }) => {
|
|||
return {
|
||||
base: "./",
|
||||
build: {
|
||||
sourcemap,
|
||||
outDir: "../internal/server/public_html",
|
||||
emptyOutDir: true,
|
||||
assetsDir: "static",
|
||||
emptyOutDir: true,
|
||||
outDir: "../internal/server/public_html",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `static/js/[name].[hash].js`,
|
||||
chunkFileNames: `static/js/[name].[hash].js`,
|
||||
assetFileNames: ({ name }) => {
|
||||
if (name && name.endsWith(".css")) {
|
||||
return "static/css/[name].[hash].[ext]";
|
||||
|
@ -39,12 +35,26 @@ export default defineConfig(({ mode }) => {
|
|||
|
||||
return "static/media/[name].[hash].[ext]";
|
||||
},
|
||||
chunkFileNames: `static/js/[name].[hash].js`,
|
||||
entryFileNames: `static/js/[name].[hash].js`,
|
||||
},
|
||||
},
|
||||
sourcemap,
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
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()],
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue