Merge branch 'master' into fix-pp-layout

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

View File

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

View File

@ -49,7 +49,7 @@ authelia configuration, and authelia database prior to attempting to do so._
Notable Missing Features from this build:
- 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).

View File

@ -61,7 +61,7 @@ authelia --config configuration.yml,config-acl.yml,config-other.yml
```
Authelia's configuration files use the YAML format. A template with all possible options can be found at the root of the
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

View File

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

View File

@ -37,3 +37,4 @@ this instance if you wanted to downgrade to pre1 you would need to use an Authel
| 6 | 4.37.0 | Adjusted the OpenID Connect tables to allow pre-configured consent improvements |
| 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 |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,24 +23,31 @@ common scenarios however those using more advanced architectures are likely goin
help with answering less specific questions about this and it may be possible if provided adequate information more
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:

View File

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

View File

@ -44,7 +44,7 @@ case you have multiple devices available, you will be asked to select your prefe
### Why don't I have access to the *Push Notification* option?
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/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
{{- $repo := "authelia/authelia" }}{{ with .Get "repo" }}{{ $repo = . }}{{ end }}
{{- $branch := printf "v%s" .Site.Data.misc.latest }}{{ with .Get "branch" }}{{ $branch = . }}{{ end }}
{{- $path := "" }}{{ with .Get "path" }}{{ $path = . }}{{ end }}
{{- $link := printf "https://github.com/%s/blob/%s/%s" $repo $branch (urlquery $path) }}
{{- $name := "" }}
{{- with .Get "name" }}
{{- $name = . }}
{{- else }}
{{- if (eq $repo "authelia/authelia") }}
{{- $name = $path }}
{{- else }}
{{- $name = printf "https://github.com/%s/blob/%s/%s" $repo $branch $path }}
{{- end }}
{{- end }}
{{- "" -}}
<a href="{{ $link }}" target="_blank">{{ $name }}</a>
{{- "" -}}

View File

@ -39,27 +39,27 @@
},
"devDependencies": {
"@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"
}
}

File diff suppressed because it is too large Load Diff

6
go.mod
View File

@ -27,13 +27,13 @@ require (
github.com/knadh/koanf/providers/env v0.1.0
github.com/knadh/koanf/providers/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
View File

@ -295,8 +295,8 @@ github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHY
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
github.com/knadh/koanf/providers/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=

View File

@ -177,56 +177,56 @@ This subcommand allows manually adding an opaque identifier for a user to the da
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,88 @@
package model
import (
"crypto/rand"
"database/sql"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestWebAuthnDeviceImportExport(t *testing.T) {
have := WebAuthnDeviceExport{
WebAuthnDevices: []WebAuthnDevice{
{
ID: 0,
CreatedAt: time.Now(),
LastUsedAt: sql.NullTime{Time: time.Now(), Valid: true},
RPID: "example",
Username: "john",
Description: "akey",
KID: NewBase64(MustRead(20)),
PublicKey: MustRead(128),
AttestationType: "fido-u2f",
Transport: "",
AAGUID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
SignCount: 20,
CloneWarning: false,
},
{
ID: 0,
CreatedAt: time.Now(),
LastUsedAt: sql.NullTime{Valid: false},
RPID: "example2",
Username: "john2",
Description: "bkey",
KID: NewBase64(MustRead(60)),
PublicKey: MustRead(64),
AttestationType: "packed",
Transport: "",
AAGUID: uuid.NullUUID{Valid: false},
SignCount: 30,
CloneWarning: true,
},
},
}
out, err := yaml.Marshal(&have)
require.NoError(t, err)
imported := WebAuthnDeviceExport{}
require.NoError(t, yaml.Unmarshal(out, &imported))
require.Equal(t, len(have.WebAuthnDevices), len(imported.WebAuthnDevices))
for i, actual := range imported.WebAuthnDevices {
t.Run(actual.Description, func(t *testing.T) {
expected := have.WebAuthnDevices[i]
assert.Equal(t, expected.KID, actual.KID)
assert.Equal(t, expected.PublicKey, actual.PublicKey)
assert.Equal(t, expected.SignCount, actual.SignCount)
assert.Equal(t, expected.AttestationType, actual.AttestationType)
assert.Equal(t, expected.RPID, actual.RPID)
assert.Equal(t, expected.AAGUID.Valid, actual.AAGUID.Valid)
assert.Equal(t, expected.AAGUID.UUID, actual.AAGUID.UUID)
assert.WithinDuration(t, expected.CreatedAt, actual.CreatedAt, time.Second)
assert.WithinDuration(t, expected.LastUsedAt.Time, actual.LastUsedAt.Time, time.Second)
assert.Equal(t, expected.LastUsedAt.Valid, actual.LastUsedAt.Valid)
assert.Equal(t, expected.CloneWarning, actual.CloneWarning)
assert.Equal(t, expected.Description, actual.Description)
assert.Equal(t, expected.Username, actual.Username)
})
}
}
func MustRead(n int) []byte {
data := make([]byte, n)
if _, err := rand.Read(data); err != nil {
panic(err)
}
return data
}

View File

@ -1,8 +1,6 @@
package oidc
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 {

View File

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

View File

@ -112,6 +112,7 @@ const (
const (
FormParameterRequestURI = "request_uri"
FormParameterResponseMode = "response_mode"
FormParameterCodeChallenge = "code_challenge"
FormParameterCodeChallengeMethod = "code_challenge_method"
)

View File

@ -0,0 +1,12 @@
package oidc
import (
"strings"
"github.com/ory/fosite"
)
// IsPushedAuthorizedRequest returns true if the requester has a PushedAuthorizationRequest redirect_uri value.
func IsPushedAuthorizedRequest(r fosite.Requester, prefix string) bool {
return strings.HasPrefix(r.GetRequestForm().Get(FormParameterRequestURI), prefix)
}

View File

@ -38,12 +38,12 @@ type Provider interface {
LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error)
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)

View File

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

View File

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

View File

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

1
web/.gitignore vendored
View File

@ -19,6 +19,7 @@
.env.test.local
.env.production.local
.eslintcache
.vitest-preview
npm-debug.log*
yarn-debug.log*

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,61 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach } from "vitest";
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
vi.mock("react-i18next", () => ({
withTranslation: () => (Component: any) => {
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
return Component;
},
Trans: ({ children }: any) => children,
useTranslation: () => {
return {
t: (str) => str,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
};
},
}));
beforeEach(() => {
document.body.setAttribute("data-privacypolicyurl", "");
document.body.setAttribute("data-privacypolicyaccept", "false");
global.localStorage.clear();
});
it("renders privacy policy and accepts when Accept button is clicked", () => {
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
document.body.setAttribute("data-privacypolicyaccept", "true");
const { container } = render(<PrivacyPolicyDrawer />);
fireEvent.click(screen.getByText("Accept"));
expect(container).toBeEmptyDOMElement();
});
it("does not render when privacy policy is disabled", () => {
render(<PrivacyPolicyDrawer />);
expect(screen.queryByText("Privacy Policy")).toBeNull();
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
expect(screen.queryByText("Accept")).toBeNull();
});
it("does not render when acceptance is not required", () => {
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
render(<PrivacyPolicyDrawer />);
expect(screen.queryByText("Privacy Policy")).toBeNull();
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
expect(screen.queryByText("Accept")).toBeNull();
});
it("does not render when already accepted", () => {
global.localStorage.setItem("privacy-policy-accepted", "true");
const { container } = render(<PrivacyPolicyDrawer />);
expect(container).toBeEmptyDOMElement();
});

View File

@ -0,0 +1,30 @@
import React from "react";
import { render } from "@testing-library/react";
import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
vi.mock("react-i18next", () => ({
withTranslation: () => (Component: any) => {
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
return Component;
},
Trans: ({ children }: any) => children,
useTranslation: () => {
return {
t: (str) => str,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
};
},
}));
it("renders a link to the privacy policy with the correct text", () => {
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
const { getByRole } = render(<PrivacyPolicyLink />);
const link = getByRole("link");
expect(link).toHaveAttribute("href", "http://example.com/privacy-policy");
expect(link).toHaveTextContent("Privacy Policy");
});

View File

@ -1,9 +1,37 @@
import React from "react";
import { 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]));
});

View File

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

View File

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

View File

@ -1,10 +0,0 @@
import "@testing-library/jest-dom";
document.body.setAttribute("data-basepath", "");
document.body.setAttribute("data-duoselfenrollment", "true");
document.body.setAttribute("data-rememberme", "true");
document.body.setAttribute("data-resetpassword", "true");
document.body.setAttribute("data-resetpasswordcustomurl", "");
document.body.setAttribute("data-privacypolicyurl", "");
document.body.setAttribute("data-privacypolicyaccept", "false");
document.body.setAttribute("data-theme", "light");

View File

@ -0,0 +1,46 @@
import matchers, { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
declare global {
namespace Vi {
interface JestAssertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
}
}
expect.extend(matchers);
const localStorageMock = (function () {
let store = {};
return {
getItem(key) {
return store[key];
},
setItem(key, value) {
store[key] = value;
},
clear() {
store = {};
},
removeItem(key) {
delete store[key];
},
getAll() {
return store;
},
};
})();
Object.defineProperty(window, "localStorage", { value: localStorageMock });
document.body.setAttribute("data-basepath", "");
document.body.setAttribute("data-duoselfenrollment", "true");
document.body.setAttribute("data-rememberme", "true");
document.body.setAttribute("data-resetpassword", "true");
document.body.setAttribute("data-resetpasswordcustomurl", "");
document.body.setAttribute("data-privacypolicyurl", "");
document.body.setAttribute("data-privacypolicyaccept", "false");
document.body.setAttribute("data-theme", "light");

View File

@ -21,7 +21,7 @@
"dom.iterable",
"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,

View File

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