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

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>

# Conflicts:
#	internal/handlers/handler_register_webauthn.go
#	internal/handlers/webauthn.go
#	internal/handlers/webauthn_test.go
#	internal/mocks/storage.go
#	internal/model/webauthn.go
#	internal/storage/provider.go
#	internal/storage/sql_provider.go
#	web/package.json
#	web/pnpm-lock.yaml
#	web/src/layouts/LoginLayout.tsx
feat-otp-verification
James Elliott 2023-04-11 14:40:09 +10:00
commit 7fdcc351d4
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
87 changed files with 4280 additions and 6011 deletions

View File

@ -57,7 +57,7 @@ This is a list of the key features of Authelia:
* Several second factor methods: * Several second factor methods:
* **[Security Keys](https://www.authelia.com/overview/authentication/security-key/)** that support * **[Security Keys](https://www.authelia.com/overview/authentication/security-key/)** that support
[FIDO2]&nbsp;[Webauthn] with devices like a [YubiKey]. [FIDO2]&nbsp;[WebAuthn] with devices like a [YubiKey].
* **[Time-based One-Time password](https://www.authelia.com/overview/authentication/one-time-password/)** * **[Time-based One-Time password](https://www.authelia.com/overview/authentication/one-time-password/)**
with compatible authenticator applications. with compatible authenticator applications.
* **[Mobile Push Notifications](https://www.authelia.com/overview/authentication/push-notification/)** * **[Mobile Push Notifications](https://www.authelia.com/overview/authentication/push-notification/)**
@ -399,7 +399,7 @@ Companies contributing to Authelia via Open Collective will have a special menti
[TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm [TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
[FIDO2]: https://www.yubico.com/authentication-standards/fido2/ [FIDO2]: https://www.yubico.com/authentication-standards/fido2/
[YubiKey]: https://www.yubico.com/products/yubikey-5-overview/ [YubiKey]: https://www.yubico.com/products/yubikey-5-overview/
[Webauthn]: https://www.yubico.com/authentication-standards/webauthn/ [WebAuthn]: https://www.yubico.com/authentication-standards/webauthn/
[auth_request]: https://nginx.org/en/docs/http/ngx_http_auth_request_module.html [auth_request]: https://nginx.org/en/docs/http/ngx_http_auth_request_module.html
[config.template.yml]: ./config.template.yml [config.template.yml]: ./config.template.yml
[nginx]: https://www.authelia.com/integration/proxies/nginx/ [nginx]: https://www.authelia.com/integration/proxies/nginx/

View File

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

View File

@ -49,7 +49,7 @@ authelia configuration, and authelia database prior to attempting to do so._
Notable Missing Features from this build: Notable Missing Features from this build:
- OpenID Connect 1.0 PAR - OpenID Connect 1.0 PAR
- Multi-Device Webauthn - Multi-Device WebAuthn
- Device Registration OTP - Device Registration OTP
- Container Images: - Container Images:
@ -144,7 +144,7 @@ Please see the [roadmap](../../roadmap/active/openid-connect.md) for more inform
##### Initial Implementation ##### Initial Implementation
_**Important Note:** This feature at the time of this writing, will not work well with Webauthn. Steps are being taken _**Important Note:** This feature at the time of this writing, will not work well with WebAuthn. Steps are being taken
to address this however it will not specifically delay the release of this feature._ to address this however it will not specifically delay the release of this feature._
This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a
@ -160,14 +160,14 @@ NGINX/NGINX Proxy Manager/SWAG/HAProxy with the use of the new
[Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only [Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only
need to configure a single middleware or helper to perform automatic redirection. need to configure a single middleware or helper to perform automatic redirection.
## Webauthn ## WebAuthn
As part of our ongoing effort for comprehensive support for Webauthn we'll be introducing several important As part of our ongoing effort for comprehensive support for WebAuthn we'll be introducing several important
features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information. features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information.
##### Multiple Webauthn Credentials Per-User ##### Multiple WebAuthn Credentials Per-User
In this release we see full support for multiple Webauthn credentials. This is a fairly basic feature but getting the In this release we see full support for multiple WebAuthn credentials. This is a fairly basic feature but getting the
frontend experience right is important to us. This is going to be supported via the frontend experience right is important to us. This is going to be supported via the
[User Control Panel](#user-dashboard--control-panel). [User Control Panel](#user-dashboard--control-panel).

View File

@ -37,3 +37,5 @@ this instance if you wanted to downgrade to pre1 you would need to use an Authel
| 6 | 4.37.0 | Adjusted the OpenID Connect tables to allow pre-configured consent improvements | | 6 | 4.37.0 | Adjusted the OpenID Connect tables to allow pre-configured consent improvements |
| 7 | 4.37.3 | Fixed some schema inconsistencies most notably the MySQL/MariaDB Engine and Collation | | 7 | 4.37.3 | Fixed some schema inconsistencies most notably the MySQL/MariaDB Engine and Collation |
| 8 | 4.38.0 | OpenID Connect 1.0 Pushed Authorization Requests | | 8 | 4.38.0 | OpenID Connect 1.0 Pushed Authorization Requests |
| 9 | 4.38.0 | Fix a PostgreSQL NOT NULL constraint issue on the `aaguid` column of the `webauthn_devices` table |
| 10 | 4.38.0 | WebAuthn adjustments for multi-cookie domain changes |

View File

@ -2,7 +2,7 @@
title: "Testing" title: "Testing"
description: "Authelia Development Testing Guidelines" description: "Authelia Development Testing Guidelines"
lead: "This section covers the testing guidelines." lead: "This section covers the testing guidelines."
date: 2022-06-15T17:51:47+10:00 date: 2023-03-20T15:03:52+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -1,6 +1,6 @@
--- ---
title: "Amir Zarrinkafsh" title: "Amir Zarrinkafsh"
date: 2022-06-15T17:51:47+10:00 date: 2023-03-19T16:29:12+10:00
draft: false draft: false
images: [] images: []
--- ---

View File

@ -1,6 +1,6 @@
--- ---
title: "Clément Michaud" title: "Clément Michaud"
date: 2022-06-15T17:51:47+10:00 date: 2023-03-19T16:29:12+10:00
draft: false draft: false
images: [] images: []
--- ---

View File

@ -1,6 +1,6 @@
--- ---
title: "Manuel Nuñez" title: "Manuel Nuñez"
date: 2022-06-15T17:51:47+10:00 date: 2023-03-19T16:29:12+10:00
draft: false draft: false
images: [] images: []
--- ---

View File

@ -1,7 +1,7 @@
--- ---
title: "About" title: "About"
description: "About Authelia and the Authelia Team" description: "About Authelia and the Authelia Team"
date: 2022-06-15T17:51:47+10:00 date: 2023-03-19T16:29:12+10:00
draft: false draft: false
images: [] images: []
aliases: aliases:

View File

@ -2,7 +2,7 @@
title: "Firezone" title: "Firezone"
description: "Integrating Firezone with the Authelia OpenID Connect Provider." description: "Integrating Firezone with the Authelia OpenID Connect Provider."
lead: "" lead: ""
date: 2023-03-25T13:07:02+10:00 date: 2023-03-28T20:29:13+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "MinIO" title: "MinIO"
description: "Integrating MinIO with the Authelia OpenID Connect Provider." description: "Integrating MinIO with the Authelia OpenID Connect Provider."
lead: "" lead: ""
date: 2022-06-15T17:51:47+10:00 date: 2023-03-21T11:21:23+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "Misago" title: "Misago"
description: "Integrating Misago with the Authelia OpenID Connect Provider." description: "Integrating Misago with the Authelia OpenID Connect Provider."
lead: "" lead: ""
date: 2023-03-04T13:20:00+00:00 date: 2023-03-14T08:51:13+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -73,7 +73,7 @@ serving Authelia at `auth.example.com`.
```nginx ```nginx
## Set $authelia_backend to route requests to the current domain by default ## Set $authelia_backend to route requests to the current domain by default
set $authelia_backend $http_host; set $authelia_backend $http_host;
## In order for Webauthn to work with multiple domains authelia must operate on a separate subdomain ## In order for WebAuthn to work with multiple domains authelia must operate on a separate subdomain
## To use authelia on a separate subdomain: ## To use authelia on a separate subdomain:
## * comment the $authelia_backend line above ## * comment the $authelia_backend line above
## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf ## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf
@ -88,7 +88,7 @@ serving Authelia at `auth.example.com`.
```nginx ```nginx
## Set $authelia_backend to route requests to the current domain by default ## Set $authelia_backend to route requests to the current domain by default
# set $authelia_backend $http_host; # set $authelia_backend $http_host;
## In order for Webauthn to work with multiple domains authelia must operate on a separate subdomain ## In order for WebAuthn to work with multiple domains authelia must operate on a separate subdomain
## To use authelia on a separate subdomain: ## To use authelia on a separate subdomain:
## * comment the $authelia_backend line above ## * comment the $authelia_backend line above
## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf ## * rename /config/nginx/proxy-confs/authelia.conf.sample to /config/nginx/proxy-confs/authelia.conf

View File

@ -63,5 +63,5 @@ authelia storage user --help
* [authelia storage](authelia_storage.md) - Manage the Authelia storage * [authelia storage](authelia_storage.md) - Manage the Authelia storage
* [authelia storage user identifiers](authelia_storage_user_identifiers.md) - Manage user opaque identifiers * [authelia storage user identifiers](authelia_storage_user_identifiers.md) - Manage user opaque identifiers
* [authelia storage user totp](authelia_storage_user_totp.md) - Manage TOTP configurations * [authelia storage user totp](authelia_storage_user_totp.md) - Manage TOTP configurations
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices * [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices

View File

@ -14,13 +14,13 @@ toc: true
## authelia storage user webauthn ## authelia storage user webauthn
Manage Webauthn devices Manage WebAuthn devices
### Synopsis ### Synopsis
Manage Webauthn devices. Manage WebAuthn devices.
This subcommand allows interacting with Webauthn devices. This subcommand allows interacting with WebAuthn devices.
### Examples ### Examples
@ -61,8 +61,8 @@ authelia storage user webauthn --help
### SEE ALSO ### SEE ALSO
* [authelia storage user](authelia_storage_user.md) - Manages user settings * [authelia storage user](authelia_storage_user.md) - Manages user settings
* [authelia storage user webauthn delete](authelia_storage_user_webauthn_delete.md) - Delete a Webauthn device * [authelia storage user webauthn delete](authelia_storage_user_webauthn_delete.md) - Delete a WebAuthn device
* [authelia storage user webauthn export](authelia_storage_user_webauthn_export.md) - Perform exports of the Webauthn devices * [authelia storage user webauthn export](authelia_storage_user_webauthn_export.md) - Perform exports of the WebAuthn devices
* [authelia storage user webauthn import](authelia_storage_user_webauthn_import.md) - Perform imports of the Webauthn devices * [authelia storage user webauthn import](authelia_storage_user_webauthn_import.md) - Perform imports of the WebAuthn devices
* [authelia storage user webauthn list](authelia_storage_user_webauthn_list.md) - List Webauthn devices * [authelia storage user webauthn list](authelia_storage_user_webauthn_list.md) - List WebAuthn devices

View File

@ -14,13 +14,13 @@ toc: true
## authelia storage user webauthn delete ## authelia storage user webauthn delete
Delete a Webauthn device Delete a WebAuthn device
### Synopsis ### Synopsis
Delete a Webauthn device. Delete a WebAuthn device.
This subcommand allows deleting a Webauthn device directly from the database. This subcommand allows deleting a WebAuthn device directly from the database.
``` ```
authelia storage user webauthn delete [username] [flags] authelia storage user webauthn delete [username] [flags]
@ -75,5 +75,5 @@ authelia storage user webauthn delete --kid abc123 --encryption-key b3453fde-ecc
### SEE ALSO ### SEE ALSO
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices * [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices

View File

@ -14,13 +14,13 @@ toc: true
## authelia storage user webauthn export ## authelia storage user webauthn export
Perform exports of the Webauthn devices Perform exports of the WebAuthn devices
### Synopsis ### Synopsis
Perform exports of the Webauthn devices. Perform exports of the WebAuthn devices.
This subcommand allows exporting Webauthn devices to various formats. This subcommand allows exporting WebAuthn devices to various formats.
``` ```
authelia storage user webauthn export [flags] authelia storage user webauthn export [flags]
@ -68,5 +68,5 @@ authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-27
### SEE ALSO ### SEE ALSO
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices * [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices

View File

@ -14,13 +14,13 @@ toc: true
## authelia storage user webauthn import ## authelia storage user webauthn import
Perform imports of the Webauthn devices Perform imports of the WebAuthn devices
### Synopsis ### Synopsis
Perform imports of the Webauthn devices. Perform imports of the WebAuthn devices.
This subcommand allows importing Webauthn devices from various formats. This subcommand allows importing WebAuthn devices from various formats.
``` ```
authelia storage user webauthn import <filename> [flags] authelia storage user webauthn import <filename> [flags]
@ -67,5 +67,5 @@ authelia storage user webauthn import --file authelia.export.webauthn.yaml --enc
### SEE ALSO ### SEE ALSO
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices * [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices

View File

@ -14,13 +14,13 @@ toc: true
## authelia storage user webauthn list ## authelia storage user webauthn list
List Webauthn devices List WebAuthn devices
### Synopsis ### Synopsis
List Webauthn devices. List WebAuthn devices.
This subcommand allows listing Webauthn devices. This subcommand allows listing WebAuthn devices.
``` ```
authelia storage user webauthn list [username] [flags] authelia storage user webauthn list [username] [flags]
@ -69,5 +69,5 @@ authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-942
### SEE ALSO ### SEE ALSO
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices * [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices

View File

@ -39,27 +39,27 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.21.0", "@babel/cli": "7.21.0",
"@babel/core": "7.21.0", "@babel/core": "7.21.4",
"@babel/preset-env": "7.20.2", "@babel/preset-env": "7.21.4",
"@fullhuman/postcss-purgecss": "5.0.0", "@fullhuman/postcss-purgecss": "5.0.0",
"@hyas/images": "0.3.2", "@hyas/images": "0.3.2",
"@popperjs/core": "2.11.6", "@popperjs/core": "2.11.7",
"auto-changelog": "2.4.0", "auto-changelog": "2.4.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.14",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",
"bootstrap-icons": "1.10.3", "bootstrap-icons": "1.10.4",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"eslint": "8.35.0", "eslint": "8.38.0",
"exec-bin": "1.0.0", "exec-bin": "1.0.0",
"flexsearch": "0.7.31", "flexsearch": "0.7.31",
"highlight.js": "11.7.0", "highlight.js": "11.7.0",
"hugo-installer": "4.0.1", "hugo-installer": "4.0.1",
"instant.page": "5.1.1", "instant.page": "5.2.0",
"katex": "0.16.4", "katex": "0.16.4",
"lazysizes": "5.3.2", "lazysizes": "5.3.2",
"markdownlint-cli2": "0.6.0", "markdownlint-cli2": "0.6.0",
"netlify-plugin-submit-sitemap": "0.4.0", "netlify-plugin-submit-sitemap": "0.4.0",
"node-fetch": "3.3.0", "node-fetch": "3.3.1",
"postcss": "8.4.21", "postcss": "8.4.21",
"postcss-cli": "10.1.0", "postcss-cli": "10.1.0",
"purgecss-whitelister": "2.4.0", "purgecss-whitelister": "2.4.0",
@ -68,6 +68,6 @@
"stylelint-config-standard-scss": "6.1.0" "stylelint-config-standard-scss": "6.1.0"
}, },
"otherDependencies": { "otherDependencies": {
"hugo": "0.111.2" "hugo": "0.111.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -177,56 +177,56 @@ This subcommand allows manually adding an opaque identifier for a user to the da
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw` authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
cmdAutheliaStorageUserWebauthnShort = "Manage Webauthn devices" cmdAutheliaStorageUserWebAuthnShort = "Manage WebAuthn devices"
cmdAutheliaStorageUserWebauthnLong = `Manage Webauthn devices. cmdAutheliaStorageUserWebAuthnLong = `Manage WebAuthn devices.
This subcommand allows interacting with Webauthn devices.` This subcommand allows interacting with WebAuthn devices.`
cmdAutheliaStorageUserWebauthnExample = `authelia storage user webauthn --help` cmdAutheliaStorageUserWebAuthnExample = `authelia storage user webauthn --help`
cmdAutheliaStorageUserWebauthnImportShort = "Perform imports of the Webauthn devices" cmdAutheliaStorageUserWebAuthnImportShort = "Perform imports of the WebAuthn devices"
cmdAutheliaStorageUserWebauthnImportLong = `Perform imports of the Webauthn devices. cmdAutheliaStorageUserWebAuthnImportLong = `Perform imports of the WebAuthn devices.
This subcommand allows importing Webauthn devices from various formats.` This subcommand allows importing WebAuthn devices from various formats.`
cmdAutheliaStorageUserWebauthnImportExample = `authelia storage user webauthn export cmdAutheliaStorageUserWebAuthnImportExample = `authelia storage user webauthn export
authelia storage user webauthn import --file authelia.export.webauthn.yaml authelia storage user webauthn import --file authelia.export.webauthn.yaml
authelia storage user webauthn import --file authelia.export.webauthn.yaml --config config.yml authelia storage user webauthn import --file authelia.export.webauthn.yaml --config config.yml
authelia storage user webauthn import --file authelia.export.webauthn.yaml --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw` authelia storage user webauthn import --file authelia.export.webauthn.yaml --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
cmdAutheliaStorageUserWebauthnExportShort = "Perform exports of the Webauthn devices" cmdAutheliaStorageUserWebAuthnExportShort = "Perform exports of the WebAuthn devices"
cmdAutheliaStorageUserWebauthnExportLong = `Perform exports of the Webauthn devices. cmdAutheliaStorageUserWebAuthnExportLong = `Perform exports of the WebAuthn devices.
This subcommand allows exporting Webauthn devices to various formats.` This subcommand allows exporting WebAuthn devices to various formats.`
cmdAutheliaStorageUserWebauthnExportExample = `authelia storage user webauthn export cmdAutheliaStorageUserWebAuthnExportExample = `authelia storage user webauthn export
authelia storage user webauthn export --file authelia.export.webauthn.yaml authelia storage user webauthn export --file authelia.export.webauthn.yaml
authelia storage user webauthn export --config config.yml authelia storage user webauthn export --config config.yml
authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw` authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
cmdAutheliaStorageUserWebauthnListShort = "List Webauthn devices" cmdAutheliaStorageUserWebAuthnListShort = "List WebAuthn devices"
cmdAutheliaStorageUserWebauthnListLong = `List Webauthn devices. cmdAutheliaStorageUserWebAuthnListLong = `List WebAuthn devices.
This subcommand allows listing Webauthn devices.` This subcommand allows listing WebAuthn devices.`
cmdAutheliaStorageUserWebauthnListExample = `authelia storage user webauthn list cmdAutheliaStorageUserWebAuthnListExample = `authelia storage user webauthn list
authelia storage user webauthn list john authelia storage user webauthn list john
authelia storage user webauthn list --config config.yml authelia storage user webauthn list --config config.yml
authelia storage user webauthn list john --config config.yml authelia storage user webauthn list john --config config.yml
authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw` authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
cmdAutheliaStorageUserWebauthnDeleteShort = "Delete a Webauthn device" cmdAutheliaStorageUserWebAuthnDeleteShort = "Delete a WebAuthn device"
cmdAutheliaStorageUserWebauthnDeleteLong = `Delete a Webauthn device. cmdAutheliaStorageUserWebAuthnDeleteLong = `Delete a WebAuthn device.
This subcommand allows deleting a Webauthn device directly from the database.` This subcommand allows deleting a WebAuthn device directly from the database.`
cmdAutheliaStorageUserWebauthnDeleteExample = `authelia storage user webauthn delete john --all cmdAutheliaStorageUserWebAuthnDeleteExample = `authelia storage user webauthn delete john --all
authelia storage user webauthn delete john --all --config config.yml authelia storage user webauthn delete john --all --config config.yml
authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
authelia storage user webauthn delete john --description Primary authelia storage user webauthn delete john --description Primary

View File

@ -68,7 +68,7 @@ func storageTOTPGenerateRunEOptsFromFlags(flags *pflag.FlagSet) (force bool, fil
return force, filename, secret, nil return force, filename, secret, nil
} }
func storageWebauthnDeleteRunEOptsFromFlags(flags *pflag.FlagSet, args []string) (all, byKID bool, description, kid, user string, err error) { func storageWebAuthnDeleteRunEOptsFromFlags(flags *pflag.FlagSet, args []string) (all, byKID bool, description, kid, user string, err error) {
if len(args) != 0 { if len(args) != 0 {
user = args[0] user = args[0]
} }

View File

@ -124,7 +124,7 @@ func newStorageUserCmd(ctx *CmdCtx) (cmd *cobra.Command) {
cmd.AddCommand( cmd.AddCommand(
newStorageUserIdentifiersCmd(ctx), newStorageUserIdentifiersCmd(ctx),
newStorageUserTOTPCmd(ctx), newStorageUserTOTPCmd(ctx),
newStorageUserWebauthnCmd(ctx), newStorageUserWebAuthnCmd(ctx),
) )
return cmd return cmd
@ -221,34 +221,34 @@ func newStorageUserIdentifiersAddCmd(ctx *CmdCtx) (cmd *cobra.Command) {
return cmd return cmd
} }
func newStorageUserWebauthnCmd(ctx *CmdCtx) (cmd *cobra.Command) { func newStorageUserWebAuthnCmd(ctx *CmdCtx) (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: "webauthn", Use: "webauthn",
Short: cmdAutheliaStorageUserWebauthnShort, Short: cmdAutheliaStorageUserWebAuthnShort,
Long: cmdAutheliaStorageUserWebauthnLong, Long: cmdAutheliaStorageUserWebAuthnLong,
Example: cmdAutheliaStorageUserWebauthnExample, Example: cmdAutheliaStorageUserWebAuthnExample,
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
} }
cmd.AddCommand( cmd.AddCommand(
newStorageUserWebauthnListCmd(ctx), newStorageUserWebAuthnListCmd(ctx),
newStorageUserWebauthnDeleteCmd(ctx), newStorageUserWebAuthnDeleteCmd(ctx),
newStorageUserWebauthnExportCmd(ctx), newStorageUserWebAuthnExportCmd(ctx),
newStorageUserWebauthnImportCmd(ctx), newStorageUserWebAuthnImportCmd(ctx),
) )
return cmd return cmd
} }
func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) { func newStorageUserWebAuthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: cmdUseImportFileName, Use: cmdUseImportFileName,
Short: cmdAutheliaStorageUserWebauthnImportShort, Short: cmdAutheliaStorageUserWebAuthnImportShort,
Long: cmdAutheliaStorageUserWebauthnImportLong, Long: cmdAutheliaStorageUserWebAuthnImportLong,
Example: cmdAutheliaStorageUserWebauthnImportExample, Example: cmdAutheliaStorageUserWebAuthnImportExample,
RunE: ctx.StorageUserWebauthnImportRunE, RunE: ctx.StorageUserWebAuthnImportRunE,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
@ -257,13 +257,13 @@ func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
return cmd return cmd
} }
func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) { func newStorageUserWebAuthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: cmdUseExport, Use: cmdUseExport,
Short: cmdAutheliaStorageUserWebauthnExportShort, Short: cmdAutheliaStorageUserWebAuthnExportShort,
Long: cmdAutheliaStorageUserWebauthnExportLong, Long: cmdAutheliaStorageUserWebAuthnExportLong,
Example: cmdAutheliaStorageUserWebauthnExportExample, Example: cmdAutheliaStorageUserWebAuthnExportExample,
RunE: ctx.StorageUserWebauthnExportRunE, RunE: ctx.StorageUserWebAuthnExportRunE,
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
@ -274,13 +274,13 @@ func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
return cmd return cmd
} }
func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) { func newStorageUserWebAuthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: "list [username]", Use: "list [username]",
Short: cmdAutheliaStorageUserWebauthnListShort, Short: cmdAutheliaStorageUserWebAuthnListShort,
Long: cmdAutheliaStorageUserWebauthnListLong, Long: cmdAutheliaStorageUserWebAuthnListLong,
Example: cmdAutheliaStorageUserWebauthnListExample, Example: cmdAutheliaStorageUserWebAuthnListExample,
RunE: ctx.StorageUserWebauthnListRunE, RunE: ctx.StorageUserWebAuthnListRunE,
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
@ -289,13 +289,13 @@ func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
return cmd return cmd
} }
func newStorageUserWebauthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) { func newStorageUserWebAuthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: "delete [username]", Use: "delete [username]",
Short: cmdAutheliaStorageUserWebauthnDeleteShort, Short: cmdAutheliaStorageUserWebAuthnDeleteShort,
Long: cmdAutheliaStorageUserWebauthnDeleteLong, Long: cmdAutheliaStorageUserWebAuthnDeleteLong,
Example: cmdAutheliaStorageUserWebauthnDeleteExample, Example: cmdAutheliaStorageUserWebAuthnDeleteExample,
RunE: ctx.StorageUserWebauthnDeleteRunE, RunE: ctx.StorageUserWebAuthnDeleteRunE,
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,

View File

@ -415,7 +415,7 @@ func (ctx *CmdCtx) StorageSchemaInfoRunE(_ *cobra.Command, _ []string) (err erro
return nil return nil
} }
func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []string) (err error) { func (ctx *CmdCtx) StorageUserWebAuthnExportRunE(cmd *cobra.Command, args []string) (err error) {
defer func() { defer func() {
_ = ctx.providers.StorageProvider.Close() _ = ctx.providers.StorageProvider.Close()
}() }()
@ -443,19 +443,19 @@ func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []stri
count := 0 count := 0
var ( var (
devices []model.WebauthnDevice devices []model.WebAuthnDevice
) )
export := &model.WebauthnDeviceExport{ export := &model.WebAuthnDeviceExport{
WebauthnDevices: nil, WebAuthnDevices: nil,
} }
for page := 0; true; page++ { for page := 0; true; page++ {
if devices, err = ctx.providers.StorageProvider.LoadWebauthnDevices(ctx, limit, page); err != nil { if devices, err = ctx.providers.StorageProvider.LoadWebAuthnDevices(ctx, limit, page); err != nil {
return err return err
} }
export.WebauthnDevices = append(export.WebauthnDevices, devices...) export.WebAuthnDevices = append(export.WebAuthnDevices, devices...)
l := len(devices) l := len(devices)
@ -476,12 +476,12 @@ func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []stri
return fmt.Errorf("error occurred writing to file '%s': %w", filename, err) return fmt.Errorf("error occurred writing to file '%s': %w", filename, err)
} }
fmt.Printf(cliOutputFmtSuccessfulUserExportFile, count, "Webauthn devices", "YAML", filename) fmt.Printf(cliOutputFmtSuccessfulUserExportFile, count, "WebAuthn devices", "YAML", filename)
return nil return nil
} }
func (ctx *CmdCtx) StorageUserWebauthnImportRunE(cmd *cobra.Command, args []string) (err error) { func (ctx *CmdCtx) StorageUserWebAuthnImportRunE(cmd *cobra.Command, args []string) (err error) {
defer func() { defer func() {
_ = ctx.providers.StorageProvider.Close() _ = ctx.providers.StorageProvider.Close()
}() }()
@ -507,58 +507,58 @@ func (ctx *CmdCtx) StorageUserWebauthnImportRunE(cmd *cobra.Command, args []stri
return err return err
} }
export := &model.WebauthnDeviceExport{} export := &model.WebAuthnDeviceExport{}
if err = yaml.Unmarshal(data, export); err != nil { if err = yaml.Unmarshal(data, export); err != nil {
return err return err
} }
if len(export.WebauthnDevices) == 0 { if len(export.WebAuthnDevices) == 0 {
return fmt.Errorf("can't import a YAML file without Webauthn devices data") return fmt.Errorf("can't import a YAML file without WebAuthn devices data")
} }
if err = ctx.CheckSchema(); err != nil { if err = ctx.CheckSchema(); err != nil {
return storageWrapCheckSchemaErr(err) return storageWrapCheckSchemaErr(err)
} }
for _, device := range export.WebauthnDevices { for _, device := range export.WebAuthnDevices {
if err = ctx.providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil { if err = ctx.providers.StorageProvider.SaveWebAuthnDevice(ctx, device); err != nil {
return err return err
} }
} }
fmt.Printf(cliOutputFmtSuccessfulUserImportFile, len(export.WebauthnDevices), "Webauthn devices", "YAML", filename) fmt.Printf(cliOutputFmtSuccessfulUserImportFile, len(export.WebAuthnDevices), "WebAuthn devices", "YAML", filename)
return nil return nil
} }
// StorageUserWebauthnListRunE is the RunE for the authelia storage user webauthn list command. // StorageUserWebAuthnListRunE is the RunE for the authelia storage user webauthn list command.
func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string) (err error) { func (ctx *CmdCtx) StorageUserWebAuthnListRunE(cmd *cobra.Command, args []string) (err error) {
defer func() { defer func() {
_ = ctx.providers.StorageProvider.Close() _ = ctx.providers.StorageProvider.Close()
}() }()
if len(args) == 0 || args[0] == "" { if len(args) == 0 || args[0] == "" {
return ctx.StorageUserWebauthnListAllRunE(cmd, args) return ctx.StorageUserWebAuthnListAllRunE(cmd, args)
} }
if err = ctx.CheckSchema(); err != nil { if err = ctx.CheckSchema(); err != nil {
return storageWrapCheckSchemaErr(err) return storageWrapCheckSchemaErr(err)
} }
var devices []model.WebauthnDevice var devices []model.WebAuthnDevice
user := args[0] user := args[0]
devices, err = ctx.providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, "", user) devices, err = ctx.providers.StorageProvider.LoadWebAuthnDevicesByUsername(ctx, "", user)
switch { switch {
case len(devices) == 0 || (err != nil && errors.Is(err, storage.ErrNoWebauthnDevice)): case len(devices) == 0 || (err != nil && errors.Is(err, storage.ErrNoWebAuthnDevice)):
return fmt.Errorf("user '%s' has no webauthn devices", user) return fmt.Errorf("user '%s' has no webauthn devices", user)
case err != nil: case err != nil:
return fmt.Errorf("can't list devices for user '%s': %w", user, err) return fmt.Errorf("can't list devices for user '%s': %w", user, err)
default: default:
fmt.Printf("Webauthn Devices for user '%s':\n\n", user) fmt.Printf("WebAuthn Devices for user '%s':\n\n", user)
fmt.Printf("ID\tKID\tDescription\n") fmt.Printf("ID\tKID\tDescription\n")
for _, device := range devices { for _, device := range devices {
@ -569,8 +569,8 @@ func (ctx *CmdCtx) StorageUserWebauthnListRunE(cmd *cobra.Command, args []string
return nil return nil
} }
// StorageUserWebauthnListAllRunE is the RunE for the authelia storage user webauthn list command when no args are specified. // StorageUserWebAuthnListAllRunE is the RunE for the authelia storage user webauthn list command when no args are specified.
func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string) (err error) { func (ctx *CmdCtx) StorageUserWebAuthnListAllRunE(_ *cobra.Command, _ []string) (err error) {
defer func() { defer func() {
_ = ctx.providers.StorageProvider.Close() _ = ctx.providers.StorageProvider.Close()
}() }()
@ -579,14 +579,14 @@ func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string)
return storageWrapCheckSchemaErr(err) return storageWrapCheckSchemaErr(err)
} }
var devices []model.WebauthnDevice var devices []model.WebAuthnDevice
limit := 10 limit := 10
output := strings.Builder{} output := strings.Builder{}
for page := 0; true; page++ { for page := 0; true; page++ {
if devices, err = ctx.providers.StorageProvider.LoadWebauthnDevices(ctx, limit, page); err != nil { if devices, err = ctx.providers.StorageProvider.LoadWebAuthnDevices(ctx, limit, page); err != nil {
return fmt.Errorf("failed to list devices: %w", err) return fmt.Errorf("failed to list devices: %w", err)
} }
@ -603,14 +603,14 @@ func (ctx *CmdCtx) StorageUserWebauthnListAllRunE(_ *cobra.Command, _ []string)
} }
} }
fmt.Printf("Webauthn Devices:\n\nID\tKID\tDescription\tUsername\n") fmt.Printf("WebAuthn Devices:\n\nID\tKID\tDescription\tUsername\n")
fmt.Println(output.String()) fmt.Println(output.String())
return nil return nil
} }
// StorageUserWebauthnDeleteRunE is the RunE for the authelia storage user webauthn delete command. // StorageUserWebAuthnDeleteRunE is the RunE for the authelia storage user webauthn delete command.
func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []string) (err error) { func (ctx *CmdCtx) StorageUserWebAuthnDeleteRunE(cmd *cobra.Command, args []string) (err error) {
defer func() { defer func() {
_ = ctx.providers.StorageProvider.Close() _ = ctx.providers.StorageProvider.Close()
}() }()
@ -624,31 +624,31 @@ func (ctx *CmdCtx) StorageUserWebauthnDeleteRunE(cmd *cobra.Command, args []stri
description, kid, user string description, kid, user string
) )
if all, byKID, description, kid, user, err = storageWebauthnDeleteRunEOptsFromFlags(cmd.Flags(), args); err != nil { if all, byKID, description, kid, user, err = storageWebAuthnDeleteRunEOptsFromFlags(cmd.Flags(), args); err != nil {
return err return err
} }
if byKID { if byKID {
if err = ctx.providers.StorageProvider.DeleteWebauthnDevice(ctx, kid); err != nil { if err = ctx.providers.StorageProvider.DeleteWebAuthnDevice(ctx, kid); err != nil {
return fmt.Errorf("failed to delete webauthn device with kid '%s': %w", kid, err) return fmt.Errorf("failed to delete webauthn device with kid '%s': %w", kid, err)
} }
fmt.Printf("Successfully deleted Webauthn device with key id '%s'\n", kid) fmt.Printf("Successfully deleted WebAuthn device with key id '%s'\n", kid)
} else { } else {
err = ctx.providers.StorageProvider.DeleteWebauthnDeviceByUsername(ctx, user, description) err = ctx.providers.StorageProvider.DeleteWebAuthnDeviceByUsername(ctx, user, description)
if all { if all {
if err != nil { if err != nil {
return fmt.Errorf("failed to delete all webauthn devices with username '%s': %w", user, err) return fmt.Errorf("failed to delete all webauthn devices with username '%s': %w", user, err)
} }
fmt.Printf("Successfully deleted all Webauthn devices for user '%s'\n", user) fmt.Printf("Successfully deleted all WebAuthn devices for user '%s'\n", user)
} else { } else {
if err != nil { if err != nil {
return fmt.Errorf("failed to delete webauthn device with username '%s' and description '%s': %w", user, description, err) return fmt.Errorf("failed to delete webauthn device with username '%s' and description '%s': %w", user, description, err)
} }
fmt.Printf("Successfully deleted Webauthn device with description '%s' for user '%s'\n", description, user) fmt.Printf("Successfully deleted WebAuthn device with description '%s' for user '%s'\n", description, user)
} }
} }

View File

@ -21,7 +21,7 @@ type Configuration struct {
Notifier NotifierConfiguration `koanf:"notifier"` Notifier NotifierConfiguration `koanf:"notifier"`
Server ServerConfiguration `koanf:"server"` Server ServerConfiguration `koanf:"server"`
Telemetry TelemetryConfig `koanf:"telemetry"` Telemetry TelemetryConfig `koanf:"telemetry"`
Webauthn WebauthnConfiguration `koanf:"webauthn"` WebAuthn WebAuthnConfiguration `koanf:"webauthn"`
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"` PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"` PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"`
} }

View File

@ -6,8 +6,8 @@ import (
"github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/protocol"
) )
// WebauthnConfiguration represents the webauthn config. // WebAuthnConfiguration represents the webauthn config.
type WebauthnConfiguration struct { type WebAuthnConfiguration struct {
Disable bool `koanf:"disable"` Disable bool `koanf:"disable"`
DisplayName string `koanf:"display_name"` DisplayName string `koanf:"display_name"`
@ -17,8 +17,8 @@ type WebauthnConfiguration struct {
Timeout time.Duration `koanf:"timeout"` Timeout time.Duration `koanf:"timeout"`
} }
// DefaultWebauthnConfiguration describes the default values for the WebauthnConfiguration. // DefaultWebAuthnConfiguration describes the default values for the WebAuthnConfiguration.
var DefaultWebauthnConfiguration = WebauthnConfiguration{ var DefaultWebAuthnConfiguration = WebAuthnConfiguration{
DisplayName: "Authelia", DisplayName: "Authelia",
Timeout: time.Second * 60, Timeout: time.Second * 60,

View File

@ -43,7 +43,7 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
ValidateTOTP(config, validator) ValidateTOTP(config, validator)
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
ValidateAuthenticationBackend(&config.AuthenticationBackend, validator) ValidateAuthenticationBackend(&config.AuthenticationBackend, validator)
@ -89,7 +89,7 @@ func validateDefault2FAMethod(config *schema.Configuration, validator *schema.St
enabledMethods = append(enabledMethods, "totp") enabledMethods = append(enabledMethods, "totp")
} }
if !config.Webauthn.Disable { if !config.WebAuthn.Disable {
enabledMethods = append(enabledMethods, "webauthn") enabledMethods = append(enabledMethods, "webauthn")
} }

View File

@ -188,7 +188,7 @@ func TestValidateDefault2FAMethod(t *testing.T) {
}, },
}, },
{ {
desc: "ShouldAllowConfiguredMethodWebauthn", desc: "ShouldAllowConfiguredMethodWebAuthn",
have: &schema.Configuration{ have: &schema.Configuration{
Default2FAMethod: "webauthn", Default2FAMethod: "webauthn",
DuoAPI: schema.DuoAPIConfiguration{ DuoAPI: schema.DuoAPIConfiguration{
@ -225,7 +225,7 @@ func TestValidateDefault2FAMethod(t *testing.T) {
}, },
}, },
{ {
desc: "ShouldNotAllowDisabledMethodWebauthn", desc: "ShouldNotAllowDisabledMethodWebAuthn",
have: &schema.Configuration{ have: &schema.Configuration{
Default2FAMethod: "webauthn", Default2FAMethod: "webauthn",
DuoAPI: schema.DuoAPIConfiguration{ DuoAPI: schema.DuoAPIConfiguration{
@ -233,7 +233,7 @@ func TestValidateDefault2FAMethod(t *testing.T) {
IntegrationKey: "another key", IntegrationKey: "another key",
Hostname: "none", Hostname: "none",
}, },
Webauthn: schema.WebauthnConfiguration{Disable: true}, WebAuthn: schema.WebAuthnConfiguration{Disable: true},
}, },
expectedErrs: []string{ expectedErrs: []string{
"option 'default_2fa_method' is configured as 'webauthn' but must be one of the following enabled method values: 'totp', 'mobile_push'", "option 'default_2fa_method' is configured as 'webauthn' but must be one of the following enabled method values: 'totp', 'mobile_push'",

View File

@ -191,10 +191,10 @@ const (
"configured to an unsafe value, it should be above 8 but it's configured to %d" "configured to an unsafe value, it should be above 8 but it's configured to %d"
) )
// Webauthn Error constants. // WebAuthn Error constants.
const ( const (
errFmtWebauthnConveyancePreference = "webauthn: option 'attestation_conveyance_preference' must be one of '%s' but it is configured as '%s'" errFmtWebAuthnConveyancePreference = "webauthn: option 'attestation_conveyance_preference' must be one of '%s' but it is configured as '%s'"
errFmtWebauthnUserVerification = "webauthn: option 'user_verification' must be one of 'discouraged', 'preferred', 'required' but it is configured as '%s'" errFmtWebAuthnUserVerification = "webauthn: option 'user_verification' must be one of 'discouraged', 'preferred', 'required' but it is configured as '%s'"
) )
// Access Control error constants. // Access Control error constants.
@ -375,8 +375,8 @@ var (
validThemeNames = []string{"light", "dark", "grey", "auto"} validThemeNames = []string{"light", "dark", "grey", "auto"}
validSessionSameSiteValues = []string{"none", "lax", "strict"} validSessionSameSiteValues = []string{"none", "lax", "strict"}
validLogLevels = []string{"trace", "debug", "info", "warn", "error"} validLogLevels = []string{"trace", "debug", "info", "warn", "error"}
validWebauthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)} validWebAuthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)}
validWebauthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)} validWebAuthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)}
validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"} validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"}
) )
@ -401,7 +401,7 @@ var (
var ( var (
reKeyReplacer = regexp.MustCompile(`\[\d+]`) reKeyReplacer = regexp.MustCompile(`\[\d+]`)
reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`) reDomainCharacters = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)+[a-z0-9]$`)
reAuthzEndpointName = regexp.MustCompile(`^[a-zA-Z](([a-zA-Z0-9/\._-]*)([a-zA-Z]))?$`) reAuthzEndpointName = regexp.MustCompile(`^[a-zA-Z](([a-zA-Z0-9/._-]*)([a-zA-Z]))?$`)
) )
var replacedKeys = map[string]string{ var replacedKeys = map[string]string{

View File

@ -841,7 +841,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
// Assert Clients[0] DisplayName is set to the Clients[0] ID, and Clients[1]'s DisplayName is not overridden. // Assert Clients[0] DisplayName is set to the Clients[0] ID, and Clients[1]'s DisplayName is not overridden.
assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description) assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)
assert.Equal(t, "Normal DisplayName", config.OIDC.Clients[1].Description) assert.Equal(t, "Normal Description", config.OIDC.Clients[1].Description)
// Assert Clients[0] ends up configured with the default Scopes. // Assert Clients[0] ends up configured with the default Scopes.
require.Len(t, config.OIDC.Clients[0].Scopes, 4) require.Len(t, config.OIDC.Clients[0].Scopes, 4)

View File

@ -8,27 +8,27 @@ import (
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
// ValidateWebauthn validates and update Webauthn configuration. // ValidateWebAuthn validates and update WebAuthn configuration.
func ValidateWebauthn(config *schema.Configuration, validator *schema.StructValidator) { func ValidateWebAuthn(config *schema.Configuration, validator *schema.StructValidator) {
if config.Webauthn.DisplayName == "" { if config.WebAuthn.DisplayName == "" {
config.Webauthn.DisplayName = schema.DefaultWebauthnConfiguration.DisplayName config.WebAuthn.DisplayName = schema.DefaultWebAuthnConfiguration.DisplayName
} }
if config.Webauthn.Timeout <= 0 { if config.WebAuthn.Timeout <= 0 {
config.Webauthn.Timeout = schema.DefaultWebauthnConfiguration.Timeout config.WebAuthn.Timeout = schema.DefaultWebAuthnConfiguration.Timeout
} }
switch { switch {
case config.Webauthn.ConveyancePreference == "": case config.WebAuthn.ConveyancePreference == "":
config.Webauthn.ConveyancePreference = schema.DefaultWebauthnConfiguration.ConveyancePreference config.WebAuthn.ConveyancePreference = schema.DefaultWebAuthnConfiguration.ConveyancePreference
case !utils.IsStringInSlice(string(config.Webauthn.ConveyancePreference), validWebauthnConveyancePreferences): case !utils.IsStringInSlice(string(config.WebAuthn.ConveyancePreference), validWebAuthnConveyancePreferences):
validator.Push(fmt.Errorf(errFmtWebauthnConveyancePreference, strings.Join(validWebauthnConveyancePreferences, "', '"), config.Webauthn.ConveyancePreference)) validator.Push(fmt.Errorf(errFmtWebAuthnConveyancePreference, strings.Join(validWebAuthnConveyancePreferences, "', '"), config.WebAuthn.ConveyancePreference))
} }
switch { switch {
case config.Webauthn.UserVerification == "": case config.WebAuthn.UserVerification == "":
config.Webauthn.UserVerification = schema.DefaultWebauthnConfiguration.UserVerification config.WebAuthn.UserVerification = schema.DefaultWebAuthnConfiguration.UserVerification
case !utils.IsStringInSlice(string(config.Webauthn.UserVerification), validWebauthnUserVerificationRequirement): case !utils.IsStringInSlice(string(config.WebAuthn.UserVerification), validWebAuthnUserVerificationRequirement):
validator.Push(fmt.Errorf(errFmtWebauthnUserVerification, config.Webauthn.UserVerification)) validator.Push(fmt.Errorf(errFmtWebAuthnUserVerification, config.WebAuthn.UserVerification))
} }
} }

View File

@ -11,39 +11,39 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
func TestWebauthnShouldSetDefaultValues(t *testing.T) { func TestWebAuthnShouldSetDefaultValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{}, WebAuthn: schema.WebAuthnConfiguration{},
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultWebauthnConfiguration.DisplayName, config.Webauthn.DisplayName) assert.Equal(t, schema.DefaultWebAuthnConfiguration.DisplayName, config.WebAuthn.DisplayName)
assert.Equal(t, schema.DefaultWebauthnConfiguration.Timeout, config.Webauthn.Timeout) assert.Equal(t, schema.DefaultWebAuthnConfiguration.Timeout, config.WebAuthn.Timeout)
assert.Equal(t, schema.DefaultWebauthnConfiguration.ConveyancePreference, config.Webauthn.ConveyancePreference) assert.Equal(t, schema.DefaultWebAuthnConfiguration.ConveyancePreference, config.WebAuthn.ConveyancePreference)
assert.Equal(t, schema.DefaultWebauthnConfiguration.UserVerification, config.Webauthn.UserVerification) assert.Equal(t, schema.DefaultWebAuthnConfiguration.UserVerification, config.WebAuthn.UserVerification)
} }
func TestWebauthnShouldSetDefaultTimeoutWhenNegative(t *testing.T) { func TestWebAuthnShouldSetDefaultTimeoutWhenNegative(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Timeout: -1, Timeout: -1,
}, },
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultWebauthnConfiguration.Timeout, config.Webauthn.Timeout) assert.Equal(t, schema.DefaultWebAuthnConfiguration.Timeout, config.WebAuthn.Timeout)
} }
func TestWebauthnShouldNotSetDefaultValuesWhenConfigured(t *testing.T) { func TestWebAuthnShouldNotSetDefaultValuesWhenConfigured(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
DisplayName: "Test", DisplayName: "Test",
Timeout: time.Second * 50, Timeout: time.Second * 50,
ConveyancePreference: protocol.PreferNoAttestation, ConveyancePreference: protocol.PreferNoAttestation,
@ -51,37 +51,37 @@ func TestWebauthnShouldNotSetDefaultValuesWhenConfigured(t *testing.T) {
}, },
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, "Test", config.Webauthn.DisplayName) assert.Equal(t, "Test", config.WebAuthn.DisplayName)
assert.Equal(t, time.Second*50, config.Webauthn.Timeout) assert.Equal(t, time.Second*50, config.WebAuthn.Timeout)
assert.Equal(t, protocol.PreferNoAttestation, config.Webauthn.ConveyancePreference) assert.Equal(t, protocol.PreferNoAttestation, config.WebAuthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationDiscouraged, config.Webauthn.UserVerification) assert.Equal(t, protocol.VerificationDiscouraged, config.WebAuthn.UserVerification)
config.Webauthn.ConveyancePreference = protocol.PreferIndirectAttestation config.WebAuthn.ConveyancePreference = protocol.PreferIndirectAttestation
config.Webauthn.UserVerification = protocol.VerificationPreferred config.WebAuthn.UserVerification = protocol.VerificationPreferred
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, protocol.PreferIndirectAttestation, config.Webauthn.ConveyancePreference) assert.Equal(t, protocol.PreferIndirectAttestation, config.WebAuthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationPreferred, config.Webauthn.UserVerification) assert.Equal(t, protocol.VerificationPreferred, config.WebAuthn.UserVerification)
config.Webauthn.ConveyancePreference = protocol.PreferDirectAttestation config.WebAuthn.ConveyancePreference = protocol.PreferDirectAttestation
config.Webauthn.UserVerification = protocol.VerificationRequired config.WebAuthn.UserVerification = protocol.VerificationRequired
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, protocol.PreferDirectAttestation, config.Webauthn.ConveyancePreference) assert.Equal(t, protocol.PreferDirectAttestation, config.WebAuthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationRequired, config.Webauthn.UserVerification) assert.Equal(t, protocol.VerificationRequired, config.WebAuthn.UserVerification)
} }
func TestWebauthnShouldRaiseErrorsOnInvalidOptions(t *testing.T) { func TestWebAuthnShouldRaiseErrorsOnInvalidOptions(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
DisplayName: "Test", DisplayName: "Test",
Timeout: time.Second * 50, Timeout: time.Second * 50,
ConveyancePreference: "no", ConveyancePreference: "no",
@ -89,7 +89,7 @@ func TestWebauthnShouldRaiseErrorsOnInvalidOptions(t *testing.T) {
}, },
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 2) require.Len(t, validator.Errors(), 2)

View File

@ -8,8 +8,8 @@ const (
// ActionTOTPRegistration is the string representation of the action for which the token has been produced. // ActionTOTPRegistration is the string representation of the action for which the token has been produced.
ActionTOTPRegistration = "RegisterTOTPDevice" ActionTOTPRegistration = "RegisterTOTPDevice"
// ActionWebauthnRegistration is the string representation of the action for which the token has been produced. // ActionWebAuthnRegistration is the string representation of the action for which the token has been produced.
ActionWebauthnRegistration = "RegisterWebauthnDevice" ActionWebAuthnRegistration = "RegisterWebAuthnDevice"
// ActionResetPassword is the string representation of the action for which the token has been produced. // ActionResetPassword is the string representation of the action for which the token has been produced.
ActionResetPassword = "ResetPassword" ActionResetPassword = "ResetPassword"

View File

@ -36,7 +36,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldHaveAllConfiguredMethods
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -66,7 +66,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveTOTPFromAvailableM
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: true, Disable: true,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -88,7 +88,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveTOTPFromAvailableM
}) })
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveWebauthnFromAvailableMethodsWhenDisabled() { func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveWebAuthnFromAvailableMethodsWhenDisabled() {
s.mock.Ctx.Configuration = schema.Configuration{ s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: schema.DuoAPIConfiguration{ DuoAPI: schema.DuoAPIConfiguration{
Disable: false, Disable: false,
@ -96,7 +96,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveWebauthnFromAvaila
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: true, Disable: true,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -126,7 +126,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveDuoFromAvailableMe
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -156,7 +156,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveAllMethodsWhenNoTw
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -186,7 +186,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveAllMethodsWhenAllD
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: true, Disable: true,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: true, Disable: true,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{

View File

@ -16,26 +16,26 @@ import (
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
) )
// WebauthnRegistrationPUT returns the attestation challenge from the server. // WebAuthnRegistrationPUT returns the attestation challenge from the server.
func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) { func WebAuthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
var ( var (
w *webauthn.WebAuthn w *webauthn.WebAuthn
user *model.WebauthnUser user *model.WebAuthnUser
userSession session.UserSession userSession session.UserSession
bodyJSON bodyRegisterWebauthnPUTRequest bodyJSON bodyRegisterWebAuthnPUTRequest
err error err error
) )
if userSession, err = ctx.GetSession(); err != nil { if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s registration challenge", regulation.AuthTypeWebauthn) ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s registration challenge", regulation.AuthTypeWebAuthn)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to create provider to generate %s registration challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create provider to generate %s registration challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -43,7 +43,7 @@ func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
} }
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil { if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
ctx.Logger.Errorf("Unable to parse %s registration request PUT data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to parse %s registration request PUT data for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -51,16 +51,16 @@ func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
} }
if length := len(bodyJSON.Description); length == 0 || length > 64 { if length := len(bodyJSON.Description); length == 0 || length > 64 {
ctx.Logger.Errorf("Failed to validate the user chosen display name for during %s registration for user '%s': the value has a length of %d but must be between 1 and 64", regulation.AuthTypeWebauthn, userSession.Username, length) ctx.Logger.Errorf("Failed to validate the user chosen display name for during %s registration for user '%s': the value has a length of %d but must be between 1 and 64", regulation.AuthTypeWebAuthn, userSession.Username, length)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, w.Config.RPID, userSession.Username) devices, err := ctx.Providers.StorageProvider.LoadWebAuthnDevicesByUsername(ctx, w.Config.RPID, userSession.Username)
if err != nil && err != storage.ErrNoWebauthnDevice { if err != nil && err != storage.ErrNoWebAuthnDevice {
ctx.Logger.Errorf("Unable to load existing %s devices for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load existing %s devices for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -69,7 +69,7 @@ func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
for _, device := range devices { for _, device := range devices {
if strings.EqualFold(device.Description, bodyJSON.Description) { if strings.EqualFold(device.Description, bodyJSON.Description) {
ctx.Logger.Errorf("Unable to generate %s registration challenge: device for for user '%s' with display name '%s' already exists", regulation.AuthTypeWebauthn, userSession.Username, bodyJSON.Description) ctx.Logger.Errorf("Unable to generate %s registration challenge: device for for user '%s' with display name '%s' already exists", regulation.AuthTypeWebAuthn, userSession.Username, bodyJSON.Description)
ctx.SetStatusCode(fasthttp.StatusConflict) ctx.SetStatusCode(fasthttp.StatusConflict)
ctx.SetJSONError(messageSecurityKeyDuplicateName) ctx.SetJSONError(messageSecurityKeyDuplicateName)
@ -78,8 +78,8 @@ func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
} }
} }
if user, err = getWebauthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil { if user, err = getWebAuthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for registration challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s devices for registration challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -96,22 +96,22 @@ func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementDiscouraged), webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementDiscouraged),
} }
data := session.Webauthn{ data := session.WebAuthn{
Description: bodyJSON.Description, Description: bodyJSON.Description,
} }
if creation, data.SessionData, err = w.BeginRegistration(user, opts...); err != nil { if creation, data.SessionData, err = w.BeginRegistration(user, opts...); err != nil {
ctx.Logger.Errorf("Unable to create %s registration challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create %s registration challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
userSession.Webauthn = &data userSession.WebAuthn = &data
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "registration challenge", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "registration challenge", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -119,7 +119,7 @@ func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
} }
if err = ctx.SetJSONBody(creation); err != nil { if err = ctx.SetJSONBody(creation); err != nil {
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -127,12 +127,12 @@ func WebauthnRegistrationPUT(ctx *middlewares.AutheliaCtx) {
} }
} }
// WebauthnRegistrationPOST processes the attestation challenge response from the client. // WebAuthnRegistrationPOST processes the attestation challenge response from the client.
func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) { func WebAuthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
var ( var (
err error err error
w *webauthn.WebAuthn w *webauthn.WebAuthn
user *model.WebauthnUser user *model.WebAuthnUser
userSession session.UserSession userSession session.UserSession
@ -142,23 +142,23 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
) )
if userSession, err = ctx.GetSession(); err != nil { if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s registration response", regulation.AuthTypeWebauthn) ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s registration response", regulation.AuthTypeWebAuthn)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
if userSession.Webauthn == nil || userSession.Webauthn.SessionData == nil { if userSession.WebAuthn == nil || userSession.WebAuthn.SessionData == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle %s registration for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", regulation.AuthTypeWebauthn, userSession.Username) ctx.Logger.Errorf("WebAuthn session data is not present in order to handle %s registration for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", regulation.AuthTypeWebAuthn, userSession.Username)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to configure %s during registration for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -168,9 +168,9 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
if response, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(ctx.PostBody())); err != nil { if response, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
switch e := err.(type) { switch e := err.(type) {
case *protocol.Error: case *protocol.Error:
ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v (%s)", regulation.AuthTypeWebauthn, userSession.Username, err, e.DevInfo) ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v (%s)", regulation.AuthTypeWebAuthn, userSession.Username, err, e.DevInfo)
default: default:
ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to parse %s registration for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
} }
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -178,20 +178,20 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
if user, err = getWebauthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil { if user, err = getWebAuthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil {
ctx.Logger.Errorf("Unable to load %s user details for registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s user details for registration for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
if credential, err = w.CreateCredential(user, *userSession.Webauthn.SessionData, response); err != nil { if credential, err = w.CreateCredential(user, *userSession.WebAuthn.SessionData, response); err != nil {
switch e := err.(type) { switch e := err.(type) {
case *protocol.Error: case *protocol.Error:
ctx.Logger.Errorf("Unable to create %s credential for user '%s': %+v (%s)", regulation.AuthTypeWebauthn, userSession.Username, err, e.DevInfo) ctx.Logger.Errorf("Unable to create %s credential for user '%s': %+v (%s)", regulation.AuthTypeWebAuthn, userSession.Username, err, e.DevInfo)
default: default:
ctx.Logger.Errorf("Unable to create %s credential for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create %s credential for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
} }
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -199,26 +199,26 @@ func WebauthnRegistrationPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
device := model.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, userSession.Webauthn.Description, credential) device := model.NewWebAuthnDeviceFromCredential(w.Config.RPID, userSession.Username, userSession.WebAuthn.Description, credential)
device.Discoverable = webauthnCredentialCreationIsDiscoverable(ctx, response) device.Discoverable = webauthnCredentialCreationIsDiscoverable(ctx, response)
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil { if err = ctx.Providers.StorageProvider.SaveWebAuthnDevice(ctx, device); err != nil {
ctx.Logger.Errorf("Unable to save %s device registration for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to save %s device registration for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
userSession.Webauthn = nil userSession.WebAuthn = nil
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the registration challenge", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the registration challenge", regulation.AuthTypeWebAuthn, userSession.Username, err)
} }
ctx.ReplyOK() ctx.ReplyOK()
ctx.SetStatusCode(fasthttp.StatusCreated) ctx.SetStatusCode(fasthttp.StatusCreated)
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Credential Description": device.Description}) ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "WebAuthn Credential", "Credential Description": device.Description})
} }

View File

@ -12,11 +12,11 @@ import (
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
) )
// WebauthnAssertionGET handler starts the assertion ceremony. // WebAuthnAssertionGET handler starts the assertion ceremony.
func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) { func WebAuthnAssertionGET(ctx *middlewares.AutheliaCtx) {
var ( var (
w *webauthn.WebAuthn w *webauthn.WebAuthn
user *model.WebauthnUser user *model.WebAuthnUser
userSession session.UserSession userSession session.UserSession
err error err error
) )
@ -29,16 +29,16 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to configure %s during authentication challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if user, err = getWebauthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil { if user, err = getWebAuthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil {
ctx.Logger.Errorf("Unable to load %s user details during authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s user details during authentication challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -61,21 +61,21 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
var ( var (
assertion *protocol.CredentialAssertion assertion *protocol.CredentialAssertion
data session.Webauthn data session.WebAuthn
) )
if assertion, data.SessionData, err = w.BeginLogin(user, opts...); err != nil { if assertion, data.SessionData, err = w.BeginLogin(user, opts...); err != nil {
ctx.Logger.Errorf("Unable to create %s authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create %s authentication challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
userSession.Webauthn = &data userSession.WebAuthn = &data
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -83,7 +83,7 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
} }
if err = ctx.SetJSONBody(assertion); err != nil { if err = ctx.SetJSONBody(assertion); err != nil {
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -91,21 +91,21 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
} }
} }
// WebauthnAssertionPOST handler completes the assertion ceremony after verifying the challenge. // WebAuthnAssertionPOST handler completes the assertion ceremony after verifying the challenge.
// //
//nolint:gocyclo //nolint:gocyclo
func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) { func WebAuthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
var ( var (
userSession session.UserSession userSession session.UserSession
err error err error
w *webauthn.WebAuthn w *webauthn.WebAuthn
bodyJSON bodySignWebauthnRequest bodyJSON bodySignWebAuthnRequest
) )
if err = ctx.ParseBody(&bodyJSON); err != nil { if err = ctx.ParseBody(&bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err) ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebAuthn, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -120,16 +120,16 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
if userSession.Webauthn == nil || userSession.Webauthn.SessionData == nil { if userSession.WebAuthn == nil || userSession.WebAuthn.SessionData == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle authentication challenge for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username) ctx.Logger.Errorf("WebAuthn session data is not present in order to handle authentication challenge for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to configure %s during authentication challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -139,27 +139,27 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
var ( var (
assertionResponse *protocol.ParsedCredentialAssertionData assertionResponse *protocol.ParsedCredentialAssertionData
credential *webauthn.Credential credential *webauthn.Credential
user *model.WebauthnUser user *model.WebAuthnUser
) )
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil { if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil {
ctx.Logger.Errorf("Unable to parse %s authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to parse %s authentication challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if user, err = getWebauthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil { if user, err = getWebAuthnUserByRPID(ctx, userSession.Username, userSession.DisplayName, w.Config.RPID); err != nil {
ctx.Logger.Errorf("Unable to load %s credentials for authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s credentials for authentication challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if credential, err = w.ValidateLogin(user, *userSession.Webauthn.SessionData, assertionResponse); err != nil { if credential, err = w.ValidateLogin(user, *userSession.WebAuthn.SessionData, assertionResponse); err != nil {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebauthn, err) _ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebAuthn, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -174,8 +174,8 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
found = true found = true
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceSignIn(ctx, device); err != nil { if err = ctx.Providers.StorageProvider.UpdateWebAuthnDeviceSignIn(ctx, device); err != nil {
ctx.Logger.Errorf("Unable to save %s device signin count for authentication challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to save %s device signin count for authentication challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -187,7 +187,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
} }
if !found { if !found {
ctx.Logger.Errorf("Unable to save %s device signin count for authentication challenge for user '%s' device '%x' count '%d': unable to find device", regulation.AuthTypeWebauthn, userSession.Username, credential.ID, credential.Authenticator.SignCount) ctx.Logger.Errorf("Unable to save %s device signin count for authentication challenge for user '%s' device '%x' count '%d': unable to find device", regulation.AuthTypeWebAuthn, userSession.Username, credential.ID, credential.Authenticator.SignCount)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -195,25 +195,25 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
} }
if err = ctx.RegenerateSession(); err != nil { if err = ctx.RegenerateSession(); err != nil {
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeWebauthn, nil); err != nil { if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeWebAuthn, nil); err != nil {
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
userSession.SetTwoFactorWebauthn(ctx.Clock.Now(), userSession.SetTwoFactorWebAuthn(ctx.Clock.Now(),
assertionResponse.Response.AuthenticatorData.Flags.HasUserPresent(), assertionResponse.Response.AuthenticatorData.Flags.HasUserPresent(),
assertionResponse.Response.AuthenticatorData.Flags.HasUserVerified()) assertionResponse.Response.AuthenticatorData.Flags.HasUserVerified())
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the authentiation challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the authentiation challenge and authentication time", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)

View File

@ -62,7 +62,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
{ {
db: model.UserInfo{ db: model.UserInfo{
Method: "webauthn", Method: "webauthn",
HasWebauthn: true, HasWebAuthn: true,
HasTOTP: true, HasTOTP: true,
}, },
err: nil, err: nil,
@ -70,7 +70,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
{ {
db: model.UserInfo{ db: model.UserInfo{
Method: "webauthn", Method: "webauthn",
HasWebauthn: true, HasWebAuthn: true,
HasTOTP: false, HasTOTP: false,
}, },
err: nil, err: nil,
@ -78,7 +78,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
{ {
db: model.UserInfo{ db: model.UserInfo{
Method: "mobile_push", Method: "mobile_push",
HasWebauthn: false, HasWebAuthn: false,
HasTOTP: false, HasTOTP: false,
}, },
err: nil, err: nil,
@ -128,7 +128,7 @@ func TestUserInfoEndpoint_SetCorrectMethod(t *testing.T) {
}) })
t.Run("registered webauthn", func(t *testing.T) { t.Run("registered webauthn", func(t *testing.T) {
assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn) assert.Equal(t, resp.api.HasWebAuthn, actualPreferences.HasWebAuthn)
}) })
t.Run("registered totp", func(t *testing.T) { t.Run("registered totp", func(t *testing.T) {
@ -160,13 +160,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
db: model.UserInfo{ db: model.UserInfo{
Method: "", Method: "",
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
HasDuo: false, HasDuo: false,
}, },
api: &model.UserInfo{ api: &model.UserInfo{
Method: "totp", Method: "totp",
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
HasDuo: false, HasDuo: false,
}, },
config: &schema.Configuration{}, config: &schema.Configuration{},
@ -178,13 +178,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
db: model.UserInfo{ db: model.UserInfo{
Method: "", Method: "",
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
HasDuo: true, HasDuo: true,
}, },
api: &model.UserInfo{ api: &model.UserInfo{
Method: "mobile_push", Method: "mobile_push",
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
HasDuo: true, HasDuo: true,
}, },
config: &schema.Configuration{}, config: &schema.Configuration{},
@ -196,13 +196,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
db: model.UserInfo{ db: model.UserInfo{
Method: "", Method: "",
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
HasDuo: true, HasDuo: true,
}, },
api: &model.UserInfo{ api: &model.UserInfo{
Method: "totp", Method: "totp",
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
HasDuo: true, HasDuo: true,
}, },
config: &schema.Configuration{DuoAPI: schema.DuoAPIConfiguration{Disable: true}}, config: &schema.Configuration{DuoAPI: schema.DuoAPIConfiguration{Disable: true}},
@ -214,13 +214,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
db: model.UserInfo{ db: model.UserInfo{
Method: "", Method: "",
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
HasDuo: true, HasDuo: true,
}, },
api: &model.UserInfo{ api: &model.UserInfo{
Method: "webauthn", Method: "webauthn",
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
HasDuo: true, HasDuo: true,
}, },
config: &schema.Configuration{ config: &schema.Configuration{
@ -236,13 +236,13 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
db: model.UserInfo{ db: model.UserInfo{
Method: "", Method: "",
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
HasDuo: false, HasDuo: false,
}, },
api: &model.UserInfo{ api: &model.UserInfo{
Method: "totp", Method: "totp",
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
HasDuo: true, HasDuo: true,
}, },
config: &schema.Configuration{}, config: &schema.Configuration{},
@ -322,7 +322,7 @@ func TestUserInfoEndpoint_SetDefaultMethod(t *testing.T) {
}) })
t.Run("registered webauthn", func(t *testing.T) { t.Run("registered webauthn", func(t *testing.T) {
assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn) assert.Equal(t, resp.api.HasWebAuthn, actualPreferences.HasWebAuthn)
}) })
t.Run("registered totp", func(t *testing.T) { t.Run("registered totp", func(t *testing.T) {

View File

@ -16,7 +16,7 @@ import (
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
) )
func getWebauthnDeviceIDFromContext(ctx *middlewares.AutheliaCtx) (int, error) { func getWebAuthnDeviceIDFromContext(ctx *middlewares.AutheliaCtx) (int, error) {
deviceIDStr, ok := ctx.UserValue("deviceID").(string) deviceIDStr, ok := ctx.UserValue("deviceID").(string)
if !ok { if !ok {
ctx.SetStatusCode(fasthttp.StatusBadRequest) ctx.SetStatusCode(fasthttp.StatusBadRequest)
@ -34,8 +34,8 @@ func getWebauthnDeviceIDFromContext(ctx *middlewares.AutheliaCtx) (int, error) {
return deviceID, nil return deviceID, nil
} }
// WebauthnDevicesGET returns all devices registered for the current user. // WebAuthnDevicesGET returns all devices registered for the current user.
func WebauthnDevicesGET(ctx *middlewares.AutheliaCtx) { func WebAuthnDevicesGET(ctx *middlewares.AutheliaCtx) {
var ( var (
userSession session.UserSession userSession session.UserSession
origin *url.URL origin *url.URL
@ -58,9 +58,9 @@ func WebauthnDevicesGET(ctx *middlewares.AutheliaCtx) {
return return
} }
devices, err := ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, origin.Hostname(), userSession.Username) devices, err := ctx.Providers.StorageProvider.LoadWebAuthnDevicesByUsername(ctx, origin.Hostname(), userSession.Username)
if err != nil && err != storage.ErrNoWebauthnDevice { if err != nil && err != storage.ErrNoWebAuthnDevice {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
return return
} }
@ -71,13 +71,13 @@ func WebauthnDevicesGET(ctx *middlewares.AutheliaCtx) {
} }
} }
// WebauthnDevicePUT updates the description for a specific device for the current user. // WebAuthnDevicePUT updates the description for a specific device for the current user.
func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) { func WebAuthnDevicePUT(ctx *middlewares.AutheliaCtx) {
var ( var (
bodyJSON bodyEditWebauthnDeviceRequest bodyJSON bodyEditWebAuthnDeviceRequest
id int id int
device *model.WebauthnDevice device *model.WebAuthnDevice
userSession session.UserSession userSession session.UserSession
err error err error
@ -92,7 +92,7 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
} }
if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil { if err = json.Unmarshal(ctx.PostBody(), &bodyJSON); err != nil {
ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to parse %s update request data for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
ctx.SetStatusCode(fasthttp.StatusBadRequest) ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
@ -100,11 +100,11 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
return return
} }
if id, err = getWebauthnDeviceIDFromContext(ctx); err != nil { if id, err = getWebAuthnDeviceIDFromContext(ctx); err != nil {
return return
} }
if device, err = ctx.Providers.StorageProvider.LoadWebauthnDeviceByID(ctx, id); err != nil { if device, err = ctx.Providers.StorageProvider.LoadWebAuthnDeviceByID(ctx, id); err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
return return
} }
@ -114,26 +114,26 @@ func WebauthnDevicePUT(ctx *middlewares.AutheliaCtx) {
return return
} }
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceDescription(ctx, userSession.Username, id, bodyJSON.Description); err != nil { if err = ctx.Providers.StorageProvider.UpdateWebAuthnDeviceDescription(ctx, userSession.Username, id, bodyJSON.Description); err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
return return
} }
} }
// WebauthnDeviceDELETE deletes a specific device for the current user. // WebAuthnDeviceDELETE deletes a specific device for the current user.
func WebauthnDeviceDELETE(ctx *middlewares.AutheliaCtx) { func WebAuthnDeviceDELETE(ctx *middlewares.AutheliaCtx) {
var ( var (
id int id int
device *model.WebauthnDevice device *model.WebAuthnDevice
userSession session.UserSession userSession session.UserSession
err error err error
) )
if id, err = getWebauthnDeviceIDFromContext(ctx); err != nil { if id, err = getWebAuthnDeviceIDFromContext(ctx); err != nil {
return return
} }
if device, err = ctx.Providers.StorageProvider.LoadWebauthnDeviceByID(ctx, id); err != nil { if device, err = ctx.Providers.StorageProvider.LoadWebAuthnDeviceByID(ctx, id); err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
return return
} }
@ -151,7 +151,7 @@ func WebauthnDeviceDELETE(ctx *middlewares.AutheliaCtx) {
return return
} }
if err = ctx.Providers.StorageProvider.DeleteWebauthnDevice(ctx, device.KID.String()); err != nil { if err = ctx.Providers.StorageProvider.DeleteWebAuthnDevice(ctx, device.KID.String()); err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
return return
} }

View File

@ -31,8 +31,8 @@ type bodySignTOTPRequest struct {
WorkflowID string `json:"workflowID"` WorkflowID string `json:"workflowID"`
} }
// bodySignWebauthnRequest is the model of the request body of WebAuthn 2FA authentication endpoint. // bodySignWebAuthnRequest is the model of the request body of WebAuthn 2FA authentication endpoint.
type bodySignWebauthnRequest struct { type bodySignWebAuthnRequest struct {
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"` Workflow string `json:"workflow"`
WorkflowID string `json:"workflowID"` WorkflowID string `json:"workflowID"`
@ -40,11 +40,11 @@ type bodySignWebauthnRequest struct {
Response json.RawMessage `json:"response"` Response json.RawMessage `json:"response"`
} }
type bodyRegisterWebauthnPUTRequest struct { type bodyRegisterWebAuthnPUTRequest struct {
Description string `json:"description"` Description string `json:"description"`
} }
type bodyEditWebauthnDeviceRequest struct { type bodyEditWebAuthnDeviceRequest struct {
Description string `json:"description"` Description string `json:"description"`
} }

View File

@ -12,20 +12,20 @@ import (
"github.com/authelia/authelia/v4/internal/random" "github.com/authelia/authelia/v4/internal/random"
) )
func getWebauthnUserByRPID(ctx *middlewares.AutheliaCtx, username, displayname string, rpid string) (user *model.WebauthnUser, err error) { func getWebAuthnUserByRPID(ctx *middlewares.AutheliaCtx, username, displayname string, rpid string) (user *model.WebAuthnUser, err error) {
if user, err = ctx.Providers.StorageProvider.LoadWebauthnUser(ctx, rpid, username); err != nil { if user, err = ctx.Providers.StorageProvider.LoadWebAuthnUser(ctx, rpid, username); err != nil {
return nil, err return nil, err
} }
if user == nil { if user == nil {
user = &model.WebauthnUser{ user = &model.WebAuthnUser{
RPID: rpid, RPID: rpid,
Username: username, Username: username,
UserID: ctx.Providers.Random.StringCustom(64, random.CharSetASCII), UserID: ctx.Providers.Random.StringCustom(64, random.CharSetASCII),
DisplayName: displayname, DisplayName: displayname,
} }
if err = ctx.Providers.StorageProvider.SaveWebauthnUser(ctx, *user); err != nil { if err = ctx.Providers.StorageProvider.SaveWebAuthnUser(ctx, *user); err != nil {
return nil, err return nil, err
} }
} else { } else {
@ -36,14 +36,14 @@ func getWebauthnUserByRPID(ctx *middlewares.AutheliaCtx, username, displayname s
user.DisplayName = user.Username user.DisplayName = user.Username
} }
if user.Devices, err = ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, rpid, user.Username); err != nil { if user.Devices, err = ctx.Providers.StorageProvider.LoadWebAuthnDevicesByUsername(ctx, rpid, user.Username); err != nil {
return nil, err return nil, err
} }
return user, nil return user, nil
} }
func newWebauthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error) { func newWebAuthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error) {
var ( var (
origin *url.URL origin *url.URL
) )
@ -54,32 +54,32 @@ func newWebauthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error)
config := &webauthn.Config{ config := &webauthn.Config{
RPID: origin.Hostname(), RPID: origin.Hostname(),
RPDisplayName: ctx.Configuration.Webauthn.DisplayName, RPDisplayName: ctx.Configuration.WebAuthn.DisplayName,
RPOrigins: []string{origin.String()}, RPOrigins: []string{origin.String()},
AttestationPreference: ctx.Configuration.Webauthn.ConveyancePreference, AttestationPreference: ctx.Configuration.WebAuthn.ConveyancePreference,
AuthenticatorSelection: protocol.AuthenticatorSelection{ AuthenticatorSelection: protocol.AuthenticatorSelection{
AuthenticatorAttachment: protocol.CrossPlatform, AuthenticatorAttachment: protocol.CrossPlatform,
RequireResidentKey: protocol.ResidentKeyNotRequired(), RequireResidentKey: protocol.ResidentKeyNotRequired(),
ResidentKey: protocol.ResidentKeyRequirementDiscouraged, ResidentKey: protocol.ResidentKeyRequirementDiscouraged,
UserVerification: ctx.Configuration.Webauthn.UserVerification, UserVerification: ctx.Configuration.WebAuthn.UserVerification,
}, },
Debug: false, Debug: false,
EncodeUserIDAsString: true, EncodeUserIDAsString: true,
Timeouts: webauthn.TimeoutsConfig{ Timeouts: webauthn.TimeoutsConfig{
Login: webauthn.TimeoutConfig{ Login: webauthn.TimeoutConfig{
Enforce: true, Enforce: true,
Timeout: ctx.Configuration.Webauthn.Timeout, Timeout: ctx.Configuration.WebAuthn.Timeout,
TimeoutUVD: ctx.Configuration.Webauthn.Timeout, TimeoutUVD: ctx.Configuration.WebAuthn.Timeout,
}, },
Registration: webauthn.TimeoutConfig{ Registration: webauthn.TimeoutConfig{
Enforce: true, Enforce: true,
Timeout: ctx.Configuration.Webauthn.Timeout, Timeout: ctx.Configuration.WebAuthn.Timeout,
TimeoutUVD: ctx.Configuration.Webauthn.Timeout, TimeoutUVD: ctx.Configuration.WebAuthn.Timeout,
}, },
}, },
} }
ctx.Logger.Tracef("Creating new Webauthn RP instance with ID %s and Origins %s", config.RPID, strings.Join(config.RPOrigins, ", ")) ctx.Logger.Tracef("Creating new WebAuthn RP instance with ID %s and Origins %s", config.RPID, strings.Join(config.RPOrigins, ", "))
return webauthn.New(config) return webauthn.New(config)
} }

View File

@ -15,7 +15,7 @@ import (
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
) )
func TestWebauthnGetUser(t *testing.T) { func TestWebAuthnGetUser(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{ userSession := session.UserSession{
@ -24,10 +24,10 @@ func TestWebauthnGetUser(t *testing.T) {
} }
ctx.StorageMock.EXPECT(). ctx.StorageMock.EXPECT().
LoadWebauthnUser(ctx.Ctx, "example.com", "john"). LoadWebAuthnUser(ctx.Ctx, "example.com", "john").
Return(&model.WebauthnUser{ID: 1, RPID: "example.com", Username: "john", UserID: "john123"}, nil) Return(&model.WebAuthnUser{ID: 1, RPID: "example.com", Username: "john", UserID: "john123"}, nil)
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return([]model.WebauthnDevice{ ctx.StorageMock.EXPECT().LoadWebAuthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return([]model.WebAuthnDevice{
{ {
ID: 1, ID: 1,
RPID: "example.com", RPID: "example.com",
@ -53,7 +53,7 @@ func TestWebauthnGetUser(t *testing.T) {
}, },
}, nil) }, nil)
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com") user, err := getWebAuthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, user) require.NotNil(t, user)
@ -105,7 +105,7 @@ func TestWebauthnGetUser(t *testing.T) {
assert.Equal(t, protocol.AuthenticatorTransport("nfc"), descriptors[1].Transport[1]) assert.Equal(t, protocol.AuthenticatorTransport("nfc"), descriptors[1].Transport[1])
} }
func TestWebauthnGetNewUser(t *testing.T) { func TestWebAuthnGetNewUser(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
// Use the random mock. // Use the random mock.
@ -118,15 +118,15 @@ func TestWebauthnGetNewUser(t *testing.T) {
gomock.InOrder( gomock.InOrder(
ctx.StorageMock.EXPECT(). ctx.StorageMock.EXPECT().
LoadWebauthnUser(ctx.Ctx, "example.com", "john"). LoadWebAuthnUser(ctx.Ctx, "example.com", "john").
Return(nil, nil), Return(nil, nil),
ctx.RandomMock.EXPECT(). ctx.RandomMock.EXPECT().
StringCustom(64, random.CharSetASCII). StringCustom(64, random.CharSetASCII).
Return("=ckBRe.%fp{w#K[qw4)AWMZrAP)(z3NUt5n3g?;>'^Rp>+eE4z>[^.<3?&n;LM#w"), Return("=ckBRe.%fp{w#K[qw4)AWMZrAP)(z3NUt5n3g?;>'^Rp>+eE4z>[^.<3?&n;LM#w"),
ctx.StorageMock.EXPECT(). ctx.StorageMock.EXPECT().
SaveWebauthnUser(ctx.Ctx, model.WebauthnUser{RPID: "example.com", Username: "john", DisplayName: "John Smith", UserID: "=ckBRe.%fp{w#K[qw4)AWMZrAP)(z3NUt5n3g?;>'^Rp>+eE4z>[^.<3?&n;LM#w"}). SaveWebAuthnUser(ctx.Ctx, model.WebAuthnUser{RPID: "example.com", Username: "john", DisplayName: "John Smith", UserID: "=ckBRe.%fp{w#K[qw4)AWMZrAP)(z3NUt5n3g?;>'^Rp>+eE4z>[^.<3?&n;LM#w"}).
Return(nil), Return(nil),
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return([]model.WebauthnDevice{ ctx.StorageMock.EXPECT().LoadWebAuthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return([]model.WebAuthnDevice{
{ {
ID: 1, ID: 1,
RPID: "example.com", RPID: "example.com",
@ -153,7 +153,7 @@ func TestWebauthnGetNewUser(t *testing.T) {
}, nil), }, nil),
) )
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com") user, err := getWebAuthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, user) require.NotNil(t, user)
@ -205,7 +205,7 @@ func TestWebauthnGetNewUser(t *testing.T) {
assert.Equal(t, protocol.AuthenticatorTransport("nfc"), descriptors[1].Transport[1]) assert.Equal(t, protocol.AuthenticatorTransport("nfc"), descriptors[1].Transport[1])
} }
func TestWebauthnGetUserWithoutDisplayName(t *testing.T) { func TestWebAuthnGetUserWithoutDisplayName(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{ userSession := session.UserSession{
@ -213,10 +213,10 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
} }
ctx.StorageMock.EXPECT(). ctx.StorageMock.EXPECT().
LoadWebauthnUser(ctx.Ctx, "example.com", "john"). LoadWebAuthnUser(ctx.Ctx, "example.com", "john").
Return(&model.WebauthnUser{ID: 1, RPID: "example.com", Username: "john", UserID: "john123"}, nil) Return(&model.WebAuthnUser{ID: 1, RPID: "example.com", Username: "john", UserID: "john123"}, nil)
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return([]model.WebauthnDevice{ ctx.StorageMock.EXPECT().LoadWebAuthnDevicesByUsername(ctx.Ctx, "example.com", "john").Return([]model.WebAuthnDevice{
{ {
ID: 1, ID: 1,
RPID: "example.com", RPID: "example.com",
@ -230,7 +230,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
}, },
}, nil) }, nil)
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com") user, err := getWebAuthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, user) require.NotNil(t, user)
@ -239,7 +239,7 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
assert.Equal(t, "john", user.DisplayName) assert.Equal(t, "john", user.DisplayName)
} }
func TestWebauthnGetUserWithErr(t *testing.T) { func TestWebAuthnGetUserWithErr(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{ userSession := session.UserSession{
@ -247,37 +247,37 @@ func TestWebauthnGetUserWithErr(t *testing.T) {
} }
ctx.StorageMock.EXPECT(). ctx.StorageMock.EXPECT().
LoadWebauthnUser(ctx.Ctx, "example.com", "john"). LoadWebAuthnUser(ctx.Ctx, "example.com", "john").
Return(&model.WebauthnUser{ID: 1, RPID: "example.com", Username: "john", UserID: "john123"}, nil) Return(&model.WebAuthnUser{ID: 1, RPID: "example.com", Username: "john", UserID: "john123"}, nil)
ctx.StorageMock.EXPECT(). ctx.StorageMock.EXPECT().
LoadWebauthnDevicesByUsername(ctx.Ctx, "example.com", "john"). LoadWebAuthnDevicesByUsername(ctx.Ctx, "example.com", "john").
Return(nil, errors.New("not found")) Return(nil, errors.New("not found"))
user, err := getWebauthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com") user, err := getWebAuthnUserByRPID(ctx.Ctx, userSession.Username, userSession.DisplayName, "example.com")
assert.EqualError(t, err, "not found") assert.EqualError(t, err, "not found")
assert.Nil(t, user) assert.Nil(t, user)
} }
func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) { func TestWebAuthnNewWebAuthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
ctx.Ctx.Request.Header.Del("X-Forwarded-Host") ctx.Ctx.Request.Header.Del("X-Forwarded-Host")
w, err := newWebauthn(ctx.Ctx) w, err := newWebAuthn(ctx.Ctx)
assert.Nil(t, w) assert.Nil(t, w)
assert.EqualError(t, err, "missing required X-Forwarded-Host header") assert.EqualError(t, err, "missing required X-Forwarded-Host header")
} }
func TestWebauthnNewWebauthnShouldReturnErrWhenWebauthnNotConfigured(t *testing.T) { func TestWebAuthnNewWebAuthnShouldReturnErrWhenWebAuthnNotConfigured(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
ctx.Ctx.Request.Header.Set("X-Forwarded-Host", "example.com") ctx.Ctx.Request.Header.Set("X-Forwarded-Host", "example.com")
ctx.Ctx.Request.Header.Set("X-Forwarded-URI", "/") ctx.Ctx.Request.Header.Set("X-Forwarded-URI", "/")
ctx.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") ctx.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
w, err := newWebauthn(ctx.Ctx) w, err := newWebAuthn(ctx.Ctx)
assert.Nil(t, w) assert.Nil(t, w)
assert.EqualError(t, err, "error occurred validating the configuration: the field 'RPDisplayName' must be configured but it is empty") assert.EqualError(t, err, "error occurred validating the configuration: the field 'RPDisplayName' must be configured but it is empty")

View File

@ -49,8 +49,8 @@ func (ctx *AutheliaCtx) AvailableSecondFactorMethods() (methods []string) {
methods = append(methods, model.SecondFactorMethodTOTP) methods = append(methods, model.SecondFactorMethodTOTP)
} }
if !ctx.Configuration.Webauthn.Disable { if !ctx.Configuration.WebAuthn.Disable {
methods = append(methods, model.SecondFactorMethodWebauthn) methods = append(methods, model.SecondFactorMethodWebAuthn)
} }
if !ctx.Configuration.DuoAPI.Disable { if !ctx.Configuration.DuoAPI.Disable {

View File

@ -235,17 +235,17 @@ func TestShouldReturnCorrectSecondFactorMethods(t *testing.T) {
mock.Ctx.Configuration.DuoAPI.Disable = true mock.Ctx.Configuration.DuoAPI.Disable = true
assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebauthn}, mock.Ctx.AvailableSecondFactorMethods()) assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebAuthn}, mock.Ctx.AvailableSecondFactorMethods())
mock.Ctx.Configuration.DuoAPI.Disable = false mock.Ctx.Configuration.DuoAPI.Disable = false
assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebauthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods()) assert.Equal(t, []string{model.SecondFactorMethodTOTP, model.SecondFactorMethodWebAuthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())
mock.Ctx.Configuration.TOTP.Disable = true mock.Ctx.Configuration.TOTP.Disable = true
assert.Equal(t, []string{model.SecondFactorMethodWebauthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods()) assert.Equal(t, []string{model.SecondFactorMethodWebAuthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())
mock.Ctx.Configuration.Webauthn.Disable = true mock.Ctx.Configuration.WebAuthn.Disable = true
assert.Equal(t, []string{model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods()) assert.Equal(t, []string{model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())

View File

@ -39,7 +39,6 @@ func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
return m.recorder return m.recorder
} }
// AppendAuthenticationLog mocks base method. // AppendAuthenticationLog mocks base method.
func (m *MockStorage) AppendAuthenticationLog(arg0 context.Context, arg1 model.AuthenticationAttempt) error { func (m *MockStorage) AppendAuthenticationLog(arg0 context.Context, arg1 model.AuthenticationAttempt) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -167,32 +166,32 @@ func (mr *MockStorageMockRecorder) DeleteTOTPConfiguration(arg0, arg1 interface{
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).DeleteTOTPConfiguration), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).DeleteTOTPConfiguration), arg0, arg1)
} }
// DeleteWebauthnDevice mocks base method. // DeleteWebAuthnDevice mocks base method.
func (m *MockStorage) DeleteWebauthnDevice(arg0 context.Context, arg1 string) error { func (m *MockStorage) DeleteWebAuthnDevice(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWebauthnDevice", arg0, arg1) ret := m.ctrl.Call(m, "DeleteWebAuthnDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteWebauthnDevice indicates an expected call of DeleteWebauthnDevice. // DeleteWebAuthnDevice indicates an expected call of DeleteWebAuthnDevice.
func (mr *MockStorageMockRecorder) DeleteWebauthnDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) DeleteWebAuthnDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDevice", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebAuthnDevice", reflect.TypeOf((*MockStorage)(nil).DeleteWebAuthnDevice), arg0, arg1)
} }
// DeleteWebauthnDeviceByUsername mocks base method. // DeleteWebAuthnDeviceByUsername mocks base method.
func (m *MockStorage) DeleteWebauthnDeviceByUsername(arg0 context.Context, arg1, arg2 string) error { func (m *MockStorage) DeleteWebAuthnDeviceByUsername(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWebauthnDeviceByUsername", arg0, arg1, arg2) ret := m.ctrl.Call(m, "DeleteWebAuthnDeviceByUsername", arg0, arg1, arg2)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteWebauthnDeviceByUsername indicates an expected call of DeleteWebauthnDeviceByUsername. // DeleteWebAuthnDeviceByUsername indicates an expected call of DeleteWebAuthnDeviceByUsername.
func (mr *MockStorageMockRecorder) DeleteWebauthnDeviceByUsername(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) DeleteWebAuthnDeviceByUsername(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDeviceByUsername", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDeviceByUsername), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebAuthnDeviceByUsername", reflect.TypeOf((*MockStorage)(nil).DeleteWebAuthnDeviceByUsername), arg0, arg1, arg2)
} }
// FindIdentityVerification mocks base method. // FindIdentityVerification mocks base method.
@ -420,64 +419,64 @@ func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifiers(arg0 interface{}) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifiers", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifiers), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifiers", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifiers), arg0)
} }
// LoadWebauthnDeviceByID mocks base method. // LoadWebAuthnDeviceByID mocks base method.
func (m *MockStorage) LoadWebauthnDeviceByID(arg0 context.Context, arg1 int) (*model.WebauthnDevice, error) { func (m *MockStorage) LoadWebAuthnDeviceByID(arg0 context.Context, arg1 int) (*model.WebAuthnDevice, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDeviceByID", arg0, arg1) ret := m.ctrl.Call(m, "LoadWebAuthnDeviceByID", arg0, arg1)
ret0, _ := ret[0].(*model.WebauthnDevice) ret0, _ := ret[0].(*model.WebAuthnDevice)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// LoadWebauthnDeviceByID indicates an expected call of LoadWebauthnDeviceByID. // LoadWebAuthnDeviceByID indicates an expected call of LoadWebAuthnDeviceByID.
func (mr *MockStorageMockRecorder) LoadWebauthnDeviceByID(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadWebAuthnDeviceByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDeviceByID", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDeviceByID), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebAuthnDeviceByID", reflect.TypeOf((*MockStorage)(nil).LoadWebAuthnDeviceByID), arg0, arg1)
} }
// LoadWebauthnDevices mocks base method. // LoadWebAuthnDevices mocks base method.
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebauthnDevice, error) { func (m *MockStorage) LoadWebAuthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebAuthnDevice, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDevices", arg0, arg1, arg2) ret := m.ctrl.Call(m, "LoadWebAuthnDevices", arg0, arg1, arg2)
ret0, _ := ret[0].([]model.WebauthnDevice) ret0, _ := ret[0].([]model.WebAuthnDevice)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// LoadWebauthnDevices indicates an expected call of LoadWebauthnDevices. // LoadWebAuthnDevices indicates an expected call of LoadWebAuthnDevices.
func (mr *MockStorageMockRecorder) LoadWebauthnDevices(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadWebAuthnDevices(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevices", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevices), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebAuthnDevices", reflect.TypeOf((*MockStorage)(nil).LoadWebAuthnDevices), arg0, arg1, arg2)
} }
// LoadWebauthnDevicesByUsername mocks base method. // LoadWebAuthnDevicesByUsername mocks base method.
func (m *MockStorage) LoadWebauthnDevicesByUsername(arg0 context.Context, arg1, arg2 string) ([]model.WebauthnDevice, error) { func (m *MockStorage) LoadWebAuthnDevicesByUsername(arg0 context.Context, arg1, arg2 string) ([]model.WebAuthnDevice, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDevicesByUsername", arg0, arg1, arg2) ret := m.ctrl.Call(m, "LoadWebAuthnDevicesByUsername", arg0, arg1, arg2)
ret0, _ := ret[0].([]model.WebauthnDevice) ret0, _ := ret[0].([]model.WebAuthnDevice)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// LoadWebauthnDevicesByUsername indicates an expected call of LoadWebauthnDevicesByUsername. // LoadWebAuthnDevicesByUsername indicates an expected call of LoadWebAuthnDevicesByUsername.
func (mr *MockStorageMockRecorder) LoadWebauthnDevicesByUsername(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadWebAuthnDevicesByUsername(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevicesByUsername", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevicesByUsername), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebAuthnDevicesByUsername", reflect.TypeOf((*MockStorage)(nil).LoadWebAuthnDevicesByUsername), arg0, arg1, arg2)
} }
// LoadWebauthnUser mocks base method. // LoadWebAuthnUser mocks base method.
func (m *MockStorage) LoadWebauthnUser(arg0 context.Context, arg1, arg2 string) (*model.WebauthnUser, error) { func (m *MockStorage) LoadWebAuthnUser(arg0 context.Context, arg1, arg2 string) (*model.WebAuthnUser, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnUser", arg0, arg1, arg2) ret := m.ctrl.Call(m, "LoadWebAuthnUser", arg0, arg1, arg2)
ret0, _ := ret[0].(*model.WebauthnUser) ret0, _ := ret[0].(*model.WebAuthnUser)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// LoadWebauthnUser indicates an expected call of LoadWebauthnUser. // LoadWebAuthnUser indicates an expected call of LoadWebAuthnUser.
func (mr *MockStorageMockRecorder) LoadWebauthnUser(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadWebAuthnUser(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnUser", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnUser), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebAuthnUser", reflect.TypeOf((*MockStorage)(nil).LoadWebAuthnUser), arg0, arg1, arg2)
} }
// RevokeOAuth2PARContext mocks base method. // RevokeOAuth2PARContext mocks base method.
@ -719,32 +718,32 @@ func (mr *MockStorageMockRecorder) SaveUserOpaqueIdentifier(arg0, arg1 interface
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserOpaqueIdentifier", reflect.TypeOf((*MockStorage)(nil).SaveUserOpaqueIdentifier), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserOpaqueIdentifier", reflect.TypeOf((*MockStorage)(nil).SaveUserOpaqueIdentifier), arg0, arg1)
} }
// SaveWebauthnDevice mocks base method. // SaveWebAuthnDevice mocks base method.
func (m *MockStorage) SaveWebauthnDevice(arg0 context.Context, arg1 model.WebauthnDevice) error { func (m *MockStorage) SaveWebAuthnDevice(arg0 context.Context, arg1 model.WebAuthnDevice) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveWebauthnDevice", arg0, arg1) ret := m.ctrl.Call(m, "SaveWebAuthnDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// SaveWebauthnDevice indicates an expected call of SaveWebauthnDevice. // SaveWebAuthnDevice indicates an expected call of SaveWebAuthnDevice.
func (mr *MockStorageMockRecorder) SaveWebauthnDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SaveWebAuthnDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveWebauthnDevice", reflect.TypeOf((*MockStorage)(nil).SaveWebauthnDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveWebAuthnDevice", reflect.TypeOf((*MockStorage)(nil).SaveWebAuthnDevice), arg0, arg1)
} }
// SaveWebauthnUser mocks base method. // SaveWebAuthnUser mocks base method.
func (m *MockStorage) SaveWebauthnUser(arg0 context.Context, arg1 model.WebauthnUser) error { func (m *MockStorage) SaveWebAuthnUser(arg0 context.Context, arg1 model.WebAuthnUser) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveWebauthnUser", arg0, arg1) ret := m.ctrl.Call(m, "SaveWebAuthnUser", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// SaveWebauthnUser indicates an expected call of SaveWebauthnUser. // SaveWebAuthnUser indicates an expected call of SaveWebAuthnUser.
func (mr *MockStorageMockRecorder) SaveWebauthnUser(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SaveWebAuthnUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveWebauthnUser", reflect.TypeOf((*MockStorage)(nil).SaveWebauthnUser), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveWebAuthnUser", reflect.TypeOf((*MockStorage)(nil).SaveWebAuthnUser), arg0, arg1)
} }
// SchemaEncryptionChangeKey mocks base method. // SchemaEncryptionChangeKey mocks base method.
@ -908,30 +907,30 @@ func (mr *MockStorageMockRecorder) UpdateTOTPConfigurationSignIn(arg0, arg1, arg
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateTOTPConfigurationSignIn), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateTOTPConfigurationSignIn), arg0, arg1, arg2)
} }
// UpdateWebauthnDeviceDescription mocks base method. // UpdateWebAuthnDeviceDescription mocks base method.
func (m *MockStorage) UpdateWebauthnDeviceDescription(arg0 context.Context, arg1 string, arg2 int, arg3 string) error { func (m *MockStorage) UpdateWebAuthnDeviceDescription(arg0 context.Context, arg1 string, arg2 int, arg3 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceDescription", arg0, arg1, arg2, arg3) ret := m.ctrl.Call(m, "UpdateWebAuthnDeviceDescription", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// UpdateWebauthnDeviceDescription indicates an expected call of UpdateWebauthnDeviceDescription. // UpdateWebAuthnDeviceDescription indicates an expected call of UpdateWebAuthnDeviceDescription.
func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceDescription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) UpdateWebAuthnDeviceDescription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebauthnDeviceDescription", reflect.TypeOf((*MockStorage)(nil).UpdateWebauthnDeviceDescription), arg0, arg1, arg2, arg3) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebAuthnDeviceDescription", reflect.TypeOf((*MockStorage)(nil).UpdateWebAuthnDeviceDescription), arg0, arg1, arg2, arg3)
} }
// UpdateWebauthnDeviceSignIn mocks base method. // UpdateWebAuthnDeviceSignIn mocks base method.
func (m *MockStorage) UpdateWebauthnDeviceSignIn(arg0 context.Context, arg1 model.WebauthnDevice) error { func (m *MockStorage) UpdateWebAuthnDeviceSignIn(arg0 context.Context, arg1 model.WebAuthnDevice) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1) ret := m.ctrl.Call(m, "UpdateWebAuthnDeviceSignIn", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// UpdateWebauthnDeviceSignIn indicates an expected call of UpdateWebauthnDeviceSignIn. // UpdateWebAuthnDeviceSignIn indicates an expected call of UpdateWebAuthnDeviceSignIn.
func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceSignIn(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) UpdateWebAuthnDeviceSignIn(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebauthnDeviceSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateWebauthnDeviceSignIn), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebAuthnDeviceSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateWebAuthnDeviceSignIn), arg0, arg1)
} }

View File

@ -15,8 +15,8 @@ const (
// SecondFactorMethodTOTP method using Time-Based One-Time Password applications like Google Authenticator. // SecondFactorMethodTOTP method using Time-Based One-Time Password applications like Google Authenticator.
SecondFactorMethodTOTP = "totp" SecondFactorMethodTOTP = "totp"
// SecondFactorMethodWebauthn method using Webauthn devices like YubiKey's. // SecondFactorMethodWebAuthn method using WebAuthn devices like YubiKey's.
SecondFactorMethodWebauthn = "webauthn" SecondFactorMethodWebAuthn = "webauthn"
// SecondFactorMethodDuo method using Duo application to receive push notifications. // SecondFactorMethodDuo method using Duo application to receive push notifications.
SecondFactorMethodDuo = "mobile_push" SecondFactorMethodDuo = "mobile_push"

View File

@ -15,8 +15,8 @@ type UserInfo struct {
// True if a TOTP device has been registered. // True if a TOTP device has been registered.
HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"` HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"`
// True if a Webauthn device has been registered. // True if a WebAuthn device has been registered.
HasWebauthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"` HasWebAuthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
// True if a duo device has been configured as the preferred. // True if a duo device has been configured as the preferred.
HasDuo bool `db:"has_duo" json:"has_duo" valid:"required"` HasDuo bool `db:"has_duo" json:"has_duo" valid:"required"`
@ -31,7 +31,7 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string, fallback strin
before := i.Method before := i.Method
totp, webauthn, duo := utils.IsStringInSlice(SecondFactorMethodTOTP, methods), utils.IsStringInSlice(SecondFactorMethodWebauthn, methods), utils.IsStringInSlice(SecondFactorMethodDuo, methods) totp, webauthn, duo := utils.IsStringInSlice(SecondFactorMethodTOTP, methods), utils.IsStringInSlice(SecondFactorMethodWebAuthn, methods), utils.IsStringInSlice(SecondFactorMethodDuo, methods)
if i.Method == "" && utils.IsStringInSlice(fallback, methods) { if i.Method == "" && utils.IsStringInSlice(fallback, methods) {
i.Method = fallback i.Method = fallback
@ -50,8 +50,8 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
switch { switch {
case i.HasTOTP && totp: case i.HasTOTP && totp:
i.Method = SecondFactorMethodTOTP i.Method = SecondFactorMethodTOTP
case i.HasWebauthn && webauthn: case i.HasWebAuthn && webauthn:
i.Method = SecondFactorMethodWebauthn i.Method = SecondFactorMethodWebAuthn
case i.HasDuo && duo: case i.HasDuo && duo:
i.Method = SecondFactorMethodDuo i.Method = SecondFactorMethodDuo
case fallback != "" && utils.IsStringInSlice(fallback, methods): case fallback != "" && utils.IsStringInSlice(fallback, methods):
@ -59,7 +59,7 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
case totp: case totp:
i.Method = SecondFactorMethodTOTP i.Method = SecondFactorMethodTOTP
case webauthn: case webauthn:
i.Method = SecondFactorMethodWebauthn i.Method = SecondFactorMethodWebAuthn
case duo: case duo:
i.Method = SecondFactorMethodDuo i.Method = SecondFactorMethodDuo
} }

View File

@ -20,7 +20,7 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
has := "" has := ""
if have.HasTOTP || have.HasDuo || have.HasWebauthn { if have.HasTOTP || have.HasDuo || have.HasWebAuthn {
has += " has" has += " has"
if have.HasTOTP { if have.HasTOTP {
@ -31,8 +31,8 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
has += " " + SecondFactorMethodDuo has += " " + SecondFactorMethodDuo
} }
if have.HasWebauthn { if have.HasWebAuthn {
has += " " + SecondFactorMethodWebauthn has += " " + SecondFactorMethodWebAuthn
} }
} }
@ -62,60 +62,60 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: true, HasDuo: true,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: true, HasDuo: true,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
changed: true, changed: true,
}, },
{ {
have: UserInfo{ have: UserInfo{
HasDuo: true, HasDuo: true,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: true, HasDuo: true,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
changed: true, changed: true,
}, },
{ {
have: UserInfo{ have: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: true, HasDuo: true,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: true, HasDuo: true,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodTOTP}, methods: []string{SecondFactorMethodTOTP},
changed: true, changed: true,
}, },
{ {
have: UserInfo{ have: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodTOTP}, methods: []string{SecondFactorMethodTOTP},
changed: true, changed: true,
@ -125,15 +125,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodWebauthn}, methods: []string{SecondFactorMethodWebAuthn},
changed: true, changed: true,
}, },
{ {
@ -141,31 +141,31 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodDuo, Method: SecondFactorMethodDuo,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodDuo}, methods: []string{SecondFactorMethodDuo},
changed: true, changed: true,
}, },
{ {
have: UserInfo{ have: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
changed: false, changed: false,
}, },
{ {
@ -173,15 +173,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: "", Method: "",
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
changed: true, changed: true,
}, },
{ {
@ -189,13 +189,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: "", Method: "",
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodDuo, Method: SecondFactorMethodDuo,
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
methods: []string{SecondFactorMethodDuo}, methods: []string{SecondFactorMethodDuo},
changed: true, changed: true,
@ -205,13 +205,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: "", Method: "",
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
want: UserInfo{ want: UserInfo{
Method: "", Method: "",
HasDuo: false, HasDuo: false,
HasTOTP: true, HasTOTP: true,
HasWebauthn: true, HasWebAuthn: true,
}, },
methods: nil, methods: nil,
changed: false, changed: false,
@ -221,15 +221,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: "", Method: "",
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodDuo, Method: SecondFactorMethodDuo,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
fallback: SecondFactorMethodDuo, fallback: SecondFactorMethodDuo,
changed: true, changed: true,
}, },
@ -238,15 +238,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: "", Method: "",
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn}, methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn},
fallback: SecondFactorMethodDuo, fallback: SecondFactorMethodDuo,
changed: true, changed: true,
}, },
@ -255,15 +255,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: true, HasDuo: true,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodDuo, Method: SecondFactorMethodDuo,
HasDuo: true, HasDuo: true,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
changed: true, changed: true,
}, },
{ {
@ -271,30 +271,30 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
Method: SecondFactorMethodTOTP, Method: SecondFactorMethodTOTP,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
fallback: SecondFactorMethodWebauthn, fallback: SecondFactorMethodWebAuthn,
changed: true, changed: true,
}, },
{ {
have: UserInfo{ have: UserInfo{
Method: SecondFactorMethodWebauthn, Method: SecondFactorMethodWebAuthn,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
want: UserInfo{ want: UserInfo{
Method: SecondFactorMethodDuo, Method: SecondFactorMethodDuo,
HasDuo: false, HasDuo: false,
HasTOTP: false, HasTOTP: false,
HasWebauthn: false, HasWebAuthn: false,
}, },
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo}, methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo},
fallback: SecondFactorMethodDuo, fallback: SecondFactorMethodDuo,

View File

@ -18,19 +18,19 @@ const (
attestationTypeFIDOU2F = "fido-u2f" attestationTypeFIDOU2F = "fido-u2f"
) )
// WebauthnUser is an object to represent a user for the Webauthn lib. // WebAuthnUser is an object to represent a user for the WebAuthn lib.
type WebauthnUser struct { type WebAuthnUser struct {
ID int `db:"id"` ID int `db:"id"`
RPID string `db:"rpid"` RPID string `db:"rpid"`
Username string `db:"username"` Username string `db:"username"`
UserID string `db:"userid"` UserID string `db:"userid"`
DisplayName string `db:"-"` DisplayName string `db:"-"`
Devices []WebauthnDevice `db:"-"` Devices []WebAuthnDevice `db:"-"`
} }
// HasFIDOU2F returns true if the user has any attestation type `fido-u2f` devices. // HasFIDOU2F returns true if the user has any attestation type `fido-u2f` devices.
func (w WebauthnUser) HasFIDOU2F() bool { func (w WebAuthnUser) HasFIDOU2F() bool {
for _, c := range w.Devices { for _, c := range w.Devices {
if c.AttestationType == attestationTypeFIDOU2F { if c.AttestationType == attestationTypeFIDOU2F {
return true return true
@ -41,27 +41,27 @@ func (w WebauthnUser) HasFIDOU2F() bool {
} }
// WebAuthnID implements the webauthn.User interface. // WebAuthnID implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnID() []byte { func (w WebAuthnUser) WebAuthnID() []byte {
return []byte(w.UserID) return []byte(w.UserID)
} }
// WebAuthnName implements the webauthn.User interface. // WebAuthnName implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnName() string { func (w WebAuthnUser) WebAuthnName() string {
return w.Username return w.Username
} }
// WebAuthnDisplayName implements the webauthn.User interface. // WebAuthnDisplayName implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnDisplayName() string { func (w WebAuthnUser) WebAuthnDisplayName() string {
return w.DisplayName return w.DisplayName
} }
// WebAuthnIcon implements the webauthn.User interface. // WebAuthnIcon implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnIcon() string { func (w WebAuthnUser) WebAuthnIcon() string {
return "" return ""
} }
// WebAuthnCredentials implements the webauthn.User interface. // WebAuthnCredentials implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnCredentials() (credentials []webauthn.Credential) { func (w WebAuthnUser) WebAuthnCredentials() (credentials []webauthn.Credential) {
credentials = make([]webauthn.Credential, len(w.Devices)) credentials = make([]webauthn.Credential, len(w.Devices))
var credential webauthn.Credential var credential webauthn.Credential
@ -108,7 +108,7 @@ func (w WebauthnUser) WebAuthnCredentials() (credentials []webauthn.Credential)
} }
// WebAuthnCredentialDescriptors decodes the users credentials into protocol.CredentialDescriptor's. // WebAuthnCredentialDescriptors decodes the users credentials into protocol.CredentialDescriptor's.
func (w WebauthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) { func (w WebAuthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) {
credentials := w.WebAuthnCredentials() credentials := w.WebAuthnCredentials()
descriptors = make([]protocol.CredentialDescriptor, len(credentials)) descriptors = make([]protocol.CredentialDescriptor, len(credentials))
@ -120,15 +120,15 @@ func (w WebauthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.Cr
return descriptors return descriptors
} }
// NewWebauthnDeviceFromCredential creates a WebauthnDevice from a webauthn.Credential. // NewWebAuthnDeviceFromCredential creates a WebAuthnDevice from a webauthn.Credential.
func NewWebauthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebauthnDevice) { func NewWebAuthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebAuthnDevice) {
transport := make([]string, len(credential.Transport)) transport := make([]string, len(credential.Transport))
for i, t := range credential.Transport { for i, t := range credential.Transport {
transport[i] = string(t) transport[i] = string(t)
} }
device = WebauthnDevice{ device = WebAuthnDevice{
RPID: rpid, RPID: rpid,
Username: username, Username: username,
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -155,30 +155,8 @@ func NewWebauthnDeviceFromCredential(rpid, username, description string, credent
return device return device
} }
// WebauthnDeviceJSON represents a Webauthn Device in the JSON format. // WebAuthnDevice represents a WebAuthn Device in the database storage.
type WebauthnDeviceJSON struct { type WebAuthnDevice struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
RPID string `json:"rpid"`
Description string `json:"description"`
KID []byte `json:"kid"`
AAGUID string `json:"aaguid,omitempty"`
Attachment string `json:"attachment"`
AttestationType string `json:"attestation_type"`
Transports []string `json:"transports"`
SignCount uint32 `json:"sign_count"`
CloneWarning bool `json:"clone_warning"`
Discoverable bool `json:"discoverable"`
Present bool `json:"present"`
Verified bool `json:"verified"`
BackupEligible bool `json:"backup_eligible"`
BackupState bool `json:"backup_state"`
PublicKey []byte `json:"public_key"`
}
// WebauthnDevice represents a Webauthn Device in the database storage.
type WebauthnDevice struct {
ID int `db:"id"` ID int `db:"id"`
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
LastUsedAt sql.NullTime `db:"last_used_at"` LastUsedAt sql.NullTime `db:"last_used_at"`
@ -200,44 +178,8 @@ type WebauthnDevice struct {
PublicKey []byte `db:"public_key"` PublicKey []byte `db:"public_key"`
} }
// MarshalJSON returns the WebauthnDevice in a JSON friendly manner. // UpdateSignInInfo adjusts the values of the WebAuthnDevice after a sign in.
func (w *WebauthnDevice) MarshalJSON() (data []byte, err error) { func (d *WebAuthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
o := WebauthnDeviceJSON{
ID: w.ID,
CreatedAt: w.CreatedAt,
RPID: w.RPID,
Description: w.Description,
KID: w.KID.data,
AttestationType: w.AttestationType,
Attachment: w.Attachment,
Transports: []string{},
SignCount: w.SignCount,
CloneWarning: w.CloneWarning,
Discoverable: w.Discoverable,
Present: w.Present,
Verified: w.Verified,
BackupEligible: w.BackupEligible,
BackupState: w.BackupState,
PublicKey: w.PublicKey,
}
if w.AAGUID.Valid {
o.AAGUID = w.AAGUID.UUID.String()
}
if w.Transport != "" {
o.Transports = strings.Split(w.Transport, ",")
}
if w.LastUsedAt.Valid {
o.LastUsedAt = &w.LastUsedAt.Time
}
return json.Marshal(o)
}
// 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.LastUsedAt = sql.NullTime{Time: now, Valid: true}
d.SignCount = signCount d.SignCount = signCount
@ -254,7 +196,7 @@ func (d *WebauthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time
} }
} }
func (d *WebauthnDevice) LastUsed() *time.Time { func (d *WebAuthnDevice) DataValueLastUsedAt() *time.Time {
if d.LastUsedAt.Valid { if d.LastUsedAt.Valid {
return &d.LastUsedAt.Time return &d.LastUsedAt.Time
} }
@ -262,19 +204,28 @@ func (d *WebauthnDevice) LastUsed() *time.Time {
return nil return nil
} }
// MarshalYAML marshals this model into YAML. func (d *WebAuthnDevice) DataValueAAGUID() *string {
func (d *WebauthnDevice) MarshalYAML() (any, error) { if d.AAGUID.Valid {
o := WebauthnDeviceData{ value := d.AAGUID.UUID.String()
return &value
}
return nil
}
func (d *WebAuthnDevice) ToData() WebAuthnDeviceData {
o := WebAuthnDeviceData{
ID: d.ID,
CreatedAt: d.CreatedAt, CreatedAt: d.CreatedAt,
LastUsedAt: d.LastUsed(), LastUsedAt: d.DataValueLastUsedAt(),
RPID: d.RPID, RPID: d.RPID,
Username: d.Username, Username: d.Username,
Description: d.Description, Description: d.Description,
KID: d.KID.String(), KID: d.KID.String(),
AAGUID: d.AAGUID.UUID.String(), AAGUID: d.DataValueAAGUID(),
AttestationType: d.AttestationType, AttestationType: d.AttestationType,
Attachment: d.Attachment, Attachment: d.Attachment,
Transport: d.Transport,
SignCount: d.SignCount, SignCount: d.SignCount,
CloneWarning: d.CloneWarning, CloneWarning: d.CloneWarning,
Present: d.Present, Present: d.Present,
@ -284,12 +235,26 @@ func (d *WebauthnDevice) MarshalYAML() (any, error) {
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey), PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
} }
return yaml.Marshal(o) if d.Transport != "" {
o.Transports = strings.Split(d.Transport, ",")
}
return o
}
// MarshalJSON returns the WebAuthnDevice in a JSON friendly manner.
func (d *WebAuthnDevice) MarshalJSON() (data []byte, err error) {
return json.Marshal(d.ToData())
}
// MarshalYAML marshals this model into YAML.
func (d *WebAuthnDevice) MarshalYAML() (any, error) {
return d.ToData(), nil
} }
// UnmarshalYAML unmarshalls YAML into this model. // UnmarshalYAML unmarshalls YAML into this model.
func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) { func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
o := &WebauthnDeviceData{} o := &WebAuthnDeviceData{}
if err = value.Decode(o); err != nil { if err = value.Decode(o); err != nil {
return err return err
@ -301,12 +266,14 @@ func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
var aaguid uuid.UUID var aaguid uuid.UUID
if aaguid, err = uuid.Parse(o.AAGUID); err != nil { if o.AAGUID != nil {
return err if aaguid, err = uuid.Parse(*o.AAGUID); err != nil {
} return err
}
if aaguid.ID() != 0 { if aaguid.ID() != 0 {
d.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid} d.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
}
} }
var kid []byte var kid []byte
@ -323,7 +290,7 @@ func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
d.Description = o.Description d.Description = o.Description
d.AttestationType = o.AttestationType d.AttestationType = o.AttestationType
d.Attachment = o.Attachment d.Attachment = o.Attachment
d.Transport = o.Transport d.Transport = strings.Join(o.Transports, ",")
d.SignCount = o.SignCount d.SignCount = o.SignCount
d.CloneWarning = o.CloneWarning d.CloneWarning = o.CloneWarning
d.Discoverable = o.Discoverable d.Discoverable = o.Discoverable
@ -339,29 +306,79 @@ func (d *WebauthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
return nil return nil
} }
// WebauthnDeviceData represents a Webauthn Device in the database storage. // WebAuthnDeviceData represents a WebAuthn Device in the database storage.
type WebauthnDeviceData struct { type WebAuthnDeviceData struct {
CreatedAt time.Time `yaml:"created_at"` ID int `json:"id" yaml:"-"`
LastUsedAt *time.Time `yaml:"last_used_at"` CreatedAt time.Time `json:"created_at" yaml:"created_at"`
RPID string `yaml:"rpid"` LastUsedAt *time.Time `json:"last_used_at,omitempty" yaml:"last_used_at,omitempty"`
Username string `yaml:"username"` RPID string `json:"rpid" yaml:"rpid"`
Description string `yaml:"description"` Username string `json:"-" yaml:"username"`
KID string `yaml:"kid"` Description string `json:"description" yaml:"description"`
AAGUID string `yaml:"aaguid"` KID string `json:"kid" yaml:"kid"`
AttestationType string `yaml:"attestation_type"` AAGUID *string `json:"aaguid,omitempty" yaml:"aaguid,omitempty"`
Attachment string `yaml:"attachment"` AttestationType string `json:"attestation_type" yaml:"attestation_type"`
Transport string `yaml:"transport"` Attachment string `json:"attachment" yaml:"attachment"`
SignCount uint32 `yaml:"sign_count"` Transports []string `json:"transports" yaml:"transports"`
CloneWarning bool `yaml:"clone_warning"` SignCount uint32 `json:"sign_count" yaml:"sign_count"`
Discoverable bool `yaml:"discoverable"` CloneWarning bool `json:"clone_warning" yaml:"clone_warning"`
Present bool `yaml:"present"` Discoverable bool `json:"discoverable" yaml:"discoverable"`
Verified bool `yaml:"verified"` Present bool `json:"present" yaml:"present"`
BackupEligible bool `yaml:"backup_eligible"` Verified bool `json:"verified" yaml:"verified"`
BackupState bool `yaml:"backup_state"` BackupEligible bool `json:"backup_eligible" yaml:"backup_eligible"`
PublicKey string `yaml:"public_key"` BackupState bool `json:"backup_state" yaml:"backup_state"`
PublicKey string `json:"public_key" yaml:"public_key"`
} }
// WebauthnDeviceExport represents a WebauthnDevice export file. func (d *WebAuthnDeviceData) ToDevice() (device *WebAuthnDevice, err error) {
type WebauthnDeviceExport struct { device = &WebAuthnDevice{
WebauthnDevices []WebauthnDevice `yaml:"webauthn_devices"` CreatedAt: d.CreatedAt,
RPID: d.RPID,
Username: d.Username,
Description: d.Description,
AttestationType: d.AttestationType,
Attachment: d.Attachment,
Transport: strings.Join(d.Transports, ","),
SignCount: d.SignCount,
CloneWarning: d.CloneWarning,
Discoverable: d.Discoverable,
Present: d.Present,
Verified: d.Verified,
BackupEligible: d.BackupEligible,
BackupState: d.BackupState,
}
if device.PublicKey, err = base64.StdEncoding.DecodeString(d.PublicKey); err != nil {
return nil, err
}
var aaguid uuid.UUID
if d.AAGUID != nil {
if aaguid, err = uuid.Parse(*d.AAGUID); err != nil {
return nil, err
}
if aaguid.ID() != 0 {
device.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
}
}
var kid []byte
if kid, err = base64.StdEncoding.DecodeString(d.KID); err != nil {
return nil, err
}
device.KID = NewBase64(kid)
if d.LastUsedAt != nil {
device.LastUsedAt = sql.NullTime{Valid: true, Time: *d.LastUsedAt}
}
return device, nil
}
// WebAuthnDeviceExport represents a WebAuthnDevice export file.
type WebAuthnDeviceExport struct {
WebAuthnDevices []WebAuthnDevice `yaml:"webauthn_devices"`
} }

View File

@ -5,9 +5,9 @@ type AuthenticationMethodsReferences struct {
UsernameAndPassword bool UsernameAndPassword bool
TOTP bool TOTP bool
Duo bool Duo bool
Webauthn bool WebAuthn bool
WebauthnUserPresence bool WebAuthnUserPresence bool
WebauthnUserVerified bool WebAuthnUserVerified bool
} }
// FactorKnowledge returns true if a "something you know" factor of authentication was used. // FactorKnowledge returns true if a "something you know" factor of authentication was used.
@ -17,7 +17,7 @@ func (r AuthenticationMethodsReferences) FactorKnowledge() bool {
// FactorPossession returns true if a "something you have" factor of authentication was used. // FactorPossession returns true if a "something you have" factor of authentication was used.
func (r AuthenticationMethodsReferences) FactorPossession() bool { func (r AuthenticationMethodsReferences) FactorPossession() bool {
return r.TOTP || r.Webauthn || r.Duo return r.TOTP || r.WebAuthn || r.Duo
} }
// MultiFactorAuthentication returns true if multiple factors were used. // MultiFactorAuthentication returns true if multiple factors were used.
@ -27,7 +27,7 @@ func (r AuthenticationMethodsReferences) MultiFactorAuthentication() bool {
// ChannelBrowser returns true if a browser was used to authenticate. // ChannelBrowser returns true if a browser was used to authenticate.
func (r AuthenticationMethodsReferences) ChannelBrowser() bool { func (r AuthenticationMethodsReferences) ChannelBrowser() bool {
return r.UsernameAndPassword || r.TOTP || r.Webauthn return r.UsernameAndPassword || r.TOTP || r.WebAuthn
} }
// ChannelService returns true if a non-browser service was used to authenticate. // ChannelService returns true if a non-browser service was used to authenticate.
@ -57,15 +57,15 @@ func (r AuthenticationMethodsReferences) MarshalRFC8176() []string {
amr = append(amr, AMRShortMessageService) amr = append(amr, AMRShortMessageService)
} }
if r.Webauthn { if r.WebAuthn {
amr = append(amr, AMRHardwareSecuredKey) amr = append(amr, AMRHardwareSecuredKey)
} }
if r.WebauthnUserPresence { if r.WebAuthnUserPresence {
amr = append(amr, AMRUserPresence) amr = append(amr, AMRUserPresence)
} }
if r.WebauthnUserVerified { if r.WebAuthnUserVerified {
amr = append(amr, AMRPersonalIdentificationNumber) amr = append(amr, AMRPersonalIdentificationNumber)
} }

View File

@ -49,9 +49,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn", desc: "WebAuthn",
is: AuthenticationMethodsReferences{Webauthn: true}, is: AuthenticationMethodsReferences{WebAuthn: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: true, FactorPossession: true,
@ -63,9 +63,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn User Presence", desc: "WebAuthn User Presence",
is: AuthenticationMethodsReferences{WebauthnUserPresence: true}, is: AuthenticationMethodsReferences{WebAuthnUserPresence: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: false, FactorPossession: false,
@ -77,9 +77,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn User Verified", desc: "WebAuthn User Verified",
is: AuthenticationMethodsReferences{WebauthnUserVerified: true}, is: AuthenticationMethodsReferences{WebAuthnUserVerified: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: false, FactorPossession: false,
@ -91,9 +91,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn with User Presence and Verified", desc: "WebAuthn with User Presence and Verified",
is: AuthenticationMethodsReferences{Webauthn: true, WebauthnUserVerified: true, WebauthnUserPresence: true}, is: AuthenticationMethodsReferences{WebAuthn: true, WebAuthnUserVerified: true, WebAuthnUserPresence: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: true, FactorPossession: true,
@ -119,9 +119,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Duo Webauthn TOTP", desc: "Duo WebAuthn TOTP",
is: AuthenticationMethodsReferences{Duo: true, Webauthn: true, TOTP: true}, is: AuthenticationMethodsReferences{Duo: true, WebAuthn: true, TOTP: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: true, FactorPossession: true,

View File

@ -190,7 +190,7 @@ const (
// a user presence test. Evidence that the end user is present and interacting with the device. This is sometimes // a user presence test. Evidence that the end user is present and interacting with the device. This is sometimes
// also referred to as "test of user presence" as per W3C.WD-webauthn-20170216. // also referred to as "test of user presence" as per W3C.WD-webauthn-20170216.
// //
// Authelia utilizes this when a user has used Webauthn to authenticate and the user presence flag was set. // Authelia utilizes this when a user has used WebAuthn to authenticate and the user presence flag was set.
// Factor: Meta, Channel: Meta. // Factor: Meta, Channel: Meta.
// //
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176 // RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
@ -203,7 +203,7 @@ const (
// containing only numbers) that a user enters to unlock a key on the device. This mechanism should have a way to // containing only numbers) that a user enters to unlock a key on the device. This mechanism should have a way to
// deter an attacker from obtaining the PIN by trying repeated guesses. // deter an attacker from obtaining the PIN by trying repeated guesses.
// //
// Authelia utilizes this when a user has used Webauthn to authenticate and the user verified flag was set. // Authelia utilizes this when a user has used WebAuthn to authenticate and the user verified flag was set.
// Factor: Meta, Channel: Meta. // Factor: Meta, Channel: Meta.
// //
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176 // RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
@ -239,7 +239,7 @@ const (
// AMRHardwareSecuredKey is an RFC8176 Authentication Method Reference Value that // AMRHardwareSecuredKey is an RFC8176 Authentication Method Reference Value that
// represents authentication via a proof-of-Possession (PoP) of a hardware-secured key. // represents authentication via a proof-of-Possession (PoP) of a hardware-secured key.
// //
// Authelia utilizes this when a user has used Webauthn to authenticate. Factor: Have, Channel: Browser. // Authelia utilizes this when a user has used WebAuthn to authenticate. Factor: Have, Channel: Browser.
// //
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176 // RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
AMRHardwareSecuredKey = "hwk" AMRHardwareSecuredKey = "hwk"

View File

@ -12,8 +12,8 @@ const (
// AuthTypeTOTP is the string representing an auth log for second-factor authentication via TOTP. // AuthTypeTOTP is the string representing an auth log for second-factor authentication via TOTP.
AuthTypeTOTP = "TOTP" AuthTypeTOTP = "TOTP"
// AuthTypeWebauthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn. // AuthTypeWebAuthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn.
AuthTypeWebauthn = "Webauthn" AuthTypeWebAuthn = "WebAuthn"
// AuthTypeDuo is the string representing an auth log for second-factor authentication via DUO. // AuthTypeDuo is the string representing an auth log for second-factor authentication via DUO.
AuthTypeDuo = "Duo" AuthTypeDuo = "Duo"

View File

@ -260,16 +260,16 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
r.POST("/api/secondfactor/totp", middleware1FA(handlers.TimeBasedOneTimePasswordPOST)) r.POST("/api/secondfactor/totp", middleware1FA(handlers.TimeBasedOneTimePasswordPOST))
} }
if !config.Webauthn.Disable { if !config.WebAuthn.Disable {
r.GET("/api/secondfactor/webauthn", middleware1FA(handlers.WebauthnAssertionGET)) r.GET("/api/secondfactor/webauthn", middleware1FA(handlers.WebAuthnAssertionGET))
r.POST("/api/secondfactor/webauthn", middleware1FA(handlers.WebauthnAssertionPOST)) r.POST("/api/secondfactor/webauthn", middleware1FA(handlers.WebAuthnAssertionPOST))
// Management of the webauthn devices. // Management of the webauthn devices.
r.GET("/api/secondfactor/webauthn/credentials", middleware1FA(handlers.WebauthnDevicesGET)) r.GET("/api/secondfactor/webauthn/credentials", middleware1FA(handlers.WebAuthnDevicesGET))
r.PUT("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebauthnRegistrationPUT)) r.PUT("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebAuthnRegistrationPUT))
r.POST("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebauthnRegistrationPOST)) r.POST("/api/secondfactor/webauthn/credential/register", middleware1FA(handlers.WebAuthnRegistrationPOST))
r.PUT("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDevicePUT)) r.PUT("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebAuthnDevicePUT))
r.DELETE("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebauthnDeviceDELETE)) r.DELETE("/api/secondfactor/webauthn/credential/{deviceID}", middleware2FA(handlers.WebAuthnDeviceDELETE))
} }
// Configure DUO api endpoint only if configuration exists. // Configure DUO api endpoint only if configuration exists.

View File

@ -272,7 +272,7 @@ func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileO
Theme: config.Theme, Theme: config.Theme,
EndpointsPasswordReset: !(config.AuthenticationBackend.PasswordReset.Disable || config.AuthenticationBackend.PasswordReset.CustomURL.String() != ""), EndpointsPasswordReset: !(config.AuthenticationBackend.PasswordReset.Disable || config.AuthenticationBackend.PasswordReset.CustomURL.String() != ""),
EndpointsWebauthn: !config.Webauthn.Disable, EndpointsWebauthn: !config.WebAuthn.Disable,
EndpointsTOTP: !config.TOTP.Disable, EndpointsTOTP: !config.TOTP.Disable,
EndpointsDuo: !config.DuoAPI.Disable, EndpointsDuo: !config.DuoAPI.Disable,
EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil), EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil),

View File

@ -179,7 +179,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true}, AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true},
}, session) }, session)
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebAuthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -187,7 +187,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, session.AuthenticationMethodRefs) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true}, session.AuthenticationMethodRefs)
assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication()) assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication())
authAt, err = session.AuthenticatedTime(authorization.OneFactor) authAt, err = session.AuthenticatedTime(authorization.OneFactor)
@ -202,7 +202,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.EqualError(t, err, "invalid authorization level") assert.EqualError(t, err, "invalid authorization level")
assert.Equal(t, timeZeroFactor, authAt) assert.Equal(t, timeZeroFactor, authAt)
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebAuthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -211,10 +211,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebAuthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -223,10 +223,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, true, false) session.SetTwoFactorWebAuthn(timeTwoFactor, true, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -235,10 +235,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserPresence: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, true, false) session.SetTwoFactorWebAuthn(timeTwoFactor, true, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -247,10 +247,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserPresence: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, false, true) session.SetTwoFactorWebAuthn(timeTwoFactor, false, true)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -259,10 +259,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, false, true) session.SetTwoFactorWebAuthn(timeTwoFactor, false, true)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -271,7 +271,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorTOTP(timeTwoFactor) session.SetTwoFactorTOTP(timeTwoFactor)
@ -283,7 +283,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorTOTP(timeTwoFactor) session.SetTwoFactorTOTP(timeTwoFactor)
@ -295,7 +295,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
} }

View File

@ -35,8 +35,8 @@ type UserSession struct {
AuthenticationMethodRefs oidc.AuthenticationMethodsReferences AuthenticationMethodRefs oidc.AuthenticationMethodsReferences
// Webauthn holds the session registration data for this session. // WebAuthn holds the session registration data for this session.
Webauthn *Webauthn WebAuthn *WebAuthn
// This boolean is set to true after identity verification and checked // This boolean is set to true after identity verification and checked
// while doing the query actually updating the password. // while doing the query actually updating the password.
@ -45,8 +45,8 @@ type UserSession struct {
RefreshTTL time.Time RefreshTTL time.Time
} }
// Webauthn holds the standard webauthn session data plus some extra. // WebAuthn holds the standard webauthn session data plus some extra.
type Webauthn struct { type WebAuthn struct {
*webauthn.SessionData *webauthn.SessionData
Description string Description string
} }

View File

@ -56,13 +56,13 @@ func (s *UserSession) SetTwoFactorDuo(now time.Time) {
s.AuthenticationMethodRefs.Duo = true s.AuthenticationMethodRefs.Duo = true
} }
// SetTwoFactorWebauthn sets the relevant Webauthn AMR's and sets the factor to 2FA. // SetTwoFactorWebAuthn sets the relevant WebAuthn AMR's and sets the factor to 2FA.
func (s *UserSession) SetTwoFactorWebauthn(now time.Time, userPresence, userVerified bool) { func (s *UserSession) SetTwoFactorWebAuthn(now time.Time, userPresence, userVerified bool) {
s.setTwoFactor(now) s.setTwoFactor(now)
s.AuthenticationMethodRefs.Webauthn = true s.AuthenticationMethodRefs.WebAuthn = true
s.AuthenticationMethodRefs.WebauthnUserPresence, s.AuthenticationMethodRefs.WebauthnUserVerified = userPresence, userVerified s.AuthenticationMethodRefs.WebAuthnUserPresence, s.AuthenticationMethodRefs.WebAuthnUserVerified = userPresence, userVerified
s.Webauthn = nil s.WebAuthn = nil
} }
// AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level. // AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level.

View File

@ -11,8 +11,8 @@ const (
tableTOTPConfigurations = "totp_configurations" tableTOTPConfigurations = "totp_configurations"
tableUserOpaqueIdentifier = "user_opaque_identifier" tableUserOpaqueIdentifier = "user_opaque_identifier"
tableUserPreferences = "user_preferences" tableUserPreferences = "user_preferences"
tableWebauthnDevices = "webauthn_devices" tableWebAuthnDevices = "webauthn_devices"
tableWebauthnUsers = "webauthn_users" tableWebAuthnUsers = "webauthn_users"
tableOAuth2BlacklistedJTI = "oauth2_blacklisted_jti" tableOAuth2BlacklistedJTI = "oauth2_blacklisted_jti"
tableOAuth2ConsentSession = "oauth2_consent_session" tableOAuth2ConsentSession = "oauth2_consent_session"

View File

@ -11,8 +11,8 @@ var (
// ErrNoTOTPConfiguration error thrown when no TOTP configuration has been found in DB. // ErrNoTOTPConfiguration error thrown when no TOTP configuration has been found in DB.
ErrNoTOTPConfiguration = errors.New("no TOTP configuration for user") ErrNoTOTPConfiguration = errors.New("no TOTP configuration for user")
// ErrNoWebauthnDevice error thrown when no Webauthn device handle has been found in DB. // ErrNoWebAuthnDevice error thrown when no WebAuthn device handle has been found in DB.
ErrNoWebauthnDevice = errors.New("no Webauthn device found") ErrNoWebAuthnDevice = errors.New("no WebAuthn device found")
// ErrNoDuoDevice error thrown when no Duo device and method has been found in DB. // ErrNoDuoDevice error thrown when no Duo device and method has been found in DB.
ErrNoDuoDevice = errors.New("no Duo device and method saved") ErrNoDuoDevice = errors.New("no Duo device and method saved")

View File

@ -9,7 +9,7 @@ import (
const ( const (
// This is the latest schema version for the purpose of tests. // This is the latest schema version for the purpose of tests.
LatestVersion = 9 LatestVersion = 10
) )
func TestShouldObtainCorrectUpMigrations(t *testing.T) { func TestShouldObtainCorrectUpMigrations(t *testing.T) {

View File

@ -38,17 +38,17 @@ type Provider interface {
LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error) LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error)
LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []model.TOTPConfiguration, err error) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []model.TOTPConfiguration, err error)
SaveWebauthnUser(ctx context.Context, user model.WebauthnUser) (err error) SaveWebAuthnUser(ctx context.Context, user model.WebAuthnUser) (err error)
LoadWebauthnUser(ctx context.Context, rpid, username string) (user *model.WebauthnUser, err error) LoadWebAuthnUser(ctx context.Context, rpid, username string) (user *model.WebAuthnUser, err error)
SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error) SaveWebAuthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error)
UpdateWebauthnDeviceDescription(ctx context.Context, username string, deviceID int, description string) (err error) UpdateWebAuthnDeviceDescription(ctx context.Context, username string, deviceID int, description string) (err error)
UpdateWebauthnDeviceSignIn(ctx context.Context, device model.WebauthnDevice) (err error) UpdateWebAuthnDeviceSignIn(ctx context.Context, device model.WebAuthnDevice) (err error)
DeleteWebauthnDevice(ctx context.Context, kid string) (err error) DeleteWebAuthnDevice(ctx context.Context, kid string) (err error)
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error) DeleteWebAuthnDeviceByUsername(ctx context.Context, username, description string) (err error)
LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error) LoadWebAuthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error)
LoadWebauthnDevicesByUsername(ctx context.Context, rpid, username string) (devices []model.WebauthnDevice, err error) LoadWebAuthnDevicesByUsername(ctx context.Context, rpid, username string) (devices []model.WebAuthnDevice, err error)
LoadWebauthnDeviceByID(ctx context.Context, id int) (device *model.WebauthnDevice, err error) LoadWebAuthnDeviceByID(ctx context.Context, id int) (device *model.WebAuthnDevice, err error)
SavePreferredDuoDevice(ctx context.Context, device model.DuoDevice) (err error) SavePreferredDuoDevice(ctx context.Context, device model.DuoDevice) (err error)
DeletePreferredDuoDevice(ctx context.Context, username string) (err error) DeletePreferredDuoDevice(ctx context.Context, username string) (err error)

View File

@ -46,19 +46,19 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlUpdateTOTPConfigRecordSignIn: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignIn, tableTOTPConfigurations), sqlUpdateTOTPConfigRecordSignIn: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignIn, tableTOTPConfigurations),
sqlUpdateTOTPConfigRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignInByUsername, tableTOTPConfigurations), sqlUpdateTOTPConfigRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignInByUsername, tableTOTPConfigurations),
sqlInsertWebauthnUser: fmt.Sprintf(queryFmtInsertWebauthnUser, tableWebauthnUsers), sqlInsertWebAuthnUser: fmt.Sprintf(queryFmtInsertWebAuthnUser, tableWebAuthnUsers),
sqlSelectWebauthnUser: fmt.Sprintf(queryFmtSelectWebauthnUser, tableWebauthnUsers), sqlSelectWebAuthnUser: fmt.Sprintf(queryFmtSelectWebAuthnUser, tableWebAuthnUsers),
sqlInsertWebauthnDevice: fmt.Sprintf(queryFmtInsertWebauthnDevice, tableWebauthnDevices), sqlInsertWebAuthnDevice: fmt.Sprintf(queryFmtInsertWebAuthnDevice, tableWebAuthnDevices),
sqlSelectWebauthnDevices: fmt.Sprintf(queryFmtSelectWebauthnDevices, tableWebauthnDevices), sqlSelectWebAuthnDevices: fmt.Sprintf(queryFmtSelectWebAuthnDevices, tableWebAuthnDevices),
sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices), sqlSelectWebAuthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebAuthnDevicesByUsername, tableWebAuthnDevices),
sqlSelectWebauthnDevicesByRPIDByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByRPIDByUsername, tableWebauthnDevices), sqlSelectWebAuthnDevicesByRPIDByUsername: fmt.Sprintf(queryFmtSelectWebAuthnDevicesByRPIDByUsername, tableWebAuthnDevices),
sqlSelectWebauthnDeviceByID: fmt.Sprintf(queryFmtSelectWebauthnDeviceByID, tableWebauthnDevices), sqlSelectWebAuthnDeviceByID: fmt.Sprintf(queryFmtSelectWebAuthnDeviceByID, tableWebAuthnDevices),
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID, tableWebauthnDevices), sqlUpdateWebAuthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebAuthnDeviceDescriptionByUsernameAndID, tableWebAuthnDevices),
sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices), sqlUpdateWebAuthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebAuthnDeviceRecordSignIn, tableWebAuthnDevices),
sqlDeleteWebauthnDevice: fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices), sqlDeleteWebAuthnDevice: fmt.Sprintf(queryFmtDeleteWebAuthnDevice, tableWebAuthnDevices),
sqlDeleteWebauthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices), sqlDeleteWebAuthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebAuthnDeviceByUsername, tableWebAuthnDevices),
sqlDeleteWebauthnDeviceByUsernameAndDisplayName: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDescription, tableWebauthnDevices), sqlDeleteWebAuthnDeviceByUsernameAndDisplayName: fmt.Sprintf(queryFmtDeleteWebAuthnDeviceByUsernameAndDescription, tableWebAuthnDevices),
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices), sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices), sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),
@ -66,7 +66,7 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlUpsertPreferred2FAMethod: fmt.Sprintf(queryFmtUpsertPreferred2FAMethod, tableUserPreferences), sqlUpsertPreferred2FAMethod: fmt.Sprintf(queryFmtUpsertPreferred2FAMethod, tableUserPreferences),
sqlSelectPreferred2FAMethod: fmt.Sprintf(queryFmtSelectPreferred2FAMethod, tableUserPreferences), sqlSelectPreferred2FAMethod: fmt.Sprintf(queryFmtSelectPreferred2FAMethod, tableUserPreferences),
sqlSelectUserInfo: fmt.Sprintf(queryFmtSelectUserInfo, tableTOTPConfigurations, tableWebauthnDevices, tableDuoDevices, tableUserPreferences), sqlSelectUserInfo: fmt.Sprintf(queryFmtSelectUserInfo, tableTOTPConfigurations, tableWebAuthnDevices, tableDuoDevices, tableUserPreferences),
sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier), sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier),
sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier), sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier),
@ -168,22 +168,22 @@ type SQLProvider struct {
sqlUpdateTOTPConfigRecordSignInByUsername string sqlUpdateTOTPConfigRecordSignInByUsername string
// Table: webauthn_users. // Table: webauthn_users.
sqlInsertWebauthnUser string sqlInsertWebAuthnUser string
sqlSelectWebauthnUser string sqlSelectWebAuthnUser string
// Table: webauthn_devices. // Table: webauthn_devices.
sqlInsertWebauthnDevice string sqlInsertWebAuthnDevice string
sqlSelectWebauthnDevices string sqlSelectWebAuthnDevices string
sqlSelectWebauthnDevicesByUsername string sqlSelectWebAuthnDevicesByUsername string
sqlSelectWebauthnDevicesByRPIDByUsername string sqlSelectWebAuthnDevicesByRPIDByUsername string
sqlSelectWebauthnDeviceByID string sqlSelectWebAuthnDeviceByID string
sqlUpdateWebauthnDeviceDescriptionByUsernameAndID string sqlUpdateWebAuthnDeviceDescriptionByUsernameAndID string
sqlUpdateWebauthnDeviceRecordSignIn string sqlUpdateWebAuthnDeviceRecordSignIn string
sqlDeleteWebauthnDevice string sqlDeleteWebAuthnDevice string
sqlDeleteWebauthnDeviceByUsername string sqlDeleteWebAuthnDeviceByUsername string
sqlDeleteWebauthnDeviceByUsernameAndDisplayName string sqlDeleteWebAuthnDeviceByUsernameAndDisplayName string
// Table: duo_devices. // Table: duo_devices.
sqlUpsertDuoDevice string sqlUpsertDuoDevice string
@ -832,7 +832,7 @@ func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config model.TO
return nil return nil
} }
// UpdateTOTPConfigurationSignIn updates a registered Webauthn devices sign in information. // UpdateTOTPConfigurationSignIn updates a registered WebAuthn devices sign in information.
func (p *SQLProvider) UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt sql.NullTime) (err error) { func (p *SQLProvider) UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt sql.NullTime) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigRecordSignIn, lastUsedAt, id); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigRecordSignIn, lastUsedAt, id); err != nil {
return fmt.Errorf("error updating TOTP configuration id %d: %w", id, err) return fmt.Errorf("error updating TOTP configuration id %d: %w", id, err)
@ -890,154 +890,154 @@ func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page in
return configs, nil return configs, nil
} }
// SaveWebauthnUser saves a registered Webauthn user. // SaveWebAuthnUser saves a registered WebAuthn user.
func (p *SQLProvider) SaveWebauthnUser(ctx context.Context, user model.WebauthnUser) (err error) { func (p *SQLProvider) SaveWebAuthnUser(ctx context.Context, user model.WebAuthnUser) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlInsertWebauthnUser, user.RPID, user.Username, user.UserID); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlInsertWebAuthnUser, user.RPID, user.Username, user.UserID); err != nil {
return fmt.Errorf("error inserting Webauthn user '%s' with relying party id '%s': %w", user.Username, user.RPID, err) return fmt.Errorf("error inserting WebAuthn user '%s' with relying party id '%s': %w", user.Username, user.RPID, err)
} }
return nil return nil
} }
// LoadWebauthnUser loads a registered Webauthn user. // LoadWebAuthnUser loads a registered WebAuthn user.
func (p *SQLProvider) LoadWebauthnUser(ctx context.Context, rpid, username string) (user *model.WebauthnUser, err error) { func (p *SQLProvider) LoadWebAuthnUser(ctx context.Context, rpid, username string) (user *model.WebAuthnUser, err error) {
user = &model.WebauthnUser{} user = &model.WebAuthnUser{}
if err = p.db.GetContext(ctx, user, p.sqlSelectWebauthnUser, rpid, username); err != nil { if err = p.db.GetContext(ctx, user, p.sqlSelectWebAuthnUser, rpid, username); err != nil {
switch { switch {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
return nil, nil return nil, nil
default: default:
return nil, fmt.Errorf("error selecting Webauthn user '%s' with relying party id '%s': %w", user.Username, user.RPID, err) return nil, fmt.Errorf("error selecting WebAuthn user '%s' with relying party id '%s': %w", user.Username, user.RPID, err)
} }
} }
return user, nil return user, nil
} }
// SaveWebauthnDevice saves a registered Webauthn device. // SaveWebAuthnDevice saves a registered WebAuthn device.
func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error) { func (p *SQLProvider) SaveWebAuthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error) {
if device.PublicKey, err = p.encrypt(device.PublicKey); err != nil { if device.PublicKey, err = p.encrypt(device.PublicKey); err != nil {
return fmt.Errorf("error encrypting Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err) return fmt.Errorf("error encrypting WebAuthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err)
} }
if _, err = p.db.ExecContext(ctx, p.sqlInsertWebauthnDevice, if _, err = p.db.ExecContext(ctx, p.sqlInsertWebAuthnDevice,
device.CreatedAt, device.LastUsedAt, device.RPID, device.Username, device.Description, device.CreatedAt, device.LastUsedAt, device.RPID, device.Username, device.Description,
device.KID, device.AAGUID, device.AttestationType, device.Attachment, device.Transport, device.KID, device.AAGUID, device.AttestationType, device.Attachment, device.Transport,
device.SignCount, device.CloneWarning, device.Discoverable, device.Present, device.Verified, device.SignCount, device.CloneWarning, device.Discoverable, device.Present, device.Verified,
device.BackupEligible, device.BackupState, device.PublicKey, device.BackupEligible, device.BackupState, device.PublicKey,
); err != nil { ); err != nil {
return fmt.Errorf("error inserting Webauthn device for user '%s' kid '%x': %w", device.Username, device.KID, err) return fmt.Errorf("error inserting WebAuthn device for user '%s' kid '%x': %w", device.Username, device.KID, err)
} }
return nil return nil
} }
// UpdateWebauthnDeviceDescription updates a registered Webauthn device's description. // UpdateWebAuthnDeviceDescription updates a registered WebAuthn device's description.
func (p *SQLProvider) UpdateWebauthnDeviceDescription(ctx context.Context, username string, deviceID int, description string) (err error) { func (p *SQLProvider) UpdateWebAuthnDeviceDescription(ctx context.Context, username string, deviceID int, description string) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceDescriptionByUsernameAndID, description, username, deviceID); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebAuthnDeviceDescriptionByUsernameAndID, description, username, deviceID); err != nil {
return fmt.Errorf("error updating Webauthn device description to '%s' for device id '%d': %w", description, deviceID, err) return fmt.Errorf("error updating WebAuthn device description to '%s' for device id '%d': %w", description, deviceID, err)
} }
return nil return nil
} }
// UpdateWebauthnDeviceSignIn updates a registered Webauthn devices sign in information. // UpdateWebAuthnDeviceSignIn updates a registered WebAuthn devices sign in information.
func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, device model.WebauthnDevice) (err error) { func (p *SQLProvider) UpdateWebAuthnDeviceSignIn(ctx context.Context, device model.WebAuthnDevice) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn, if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebAuthnDeviceRecordSignIn,
device.RPID, device.LastUsedAt, device.SignCount, device.Discoverable, device.Present, device.Verified, device.RPID, device.LastUsedAt, device.SignCount, device.Discoverable, device.Present, device.Verified,
device.BackupEligible, device.BackupState, device.CloneWarning, device.ID, device.BackupEligible, device.BackupState, device.CloneWarning, device.ID,
); err != nil { ); err != nil {
return fmt.Errorf("error updating Webauthn authentication metadata for id '%x': %w", device.ID, err) return fmt.Errorf("error updating WebAuthn authentication metadata for id '%x': %w", device.ID, err)
} }
return nil return nil
} }
// DeleteWebauthnDevice deletes a registered Webauthn device. // DeleteWebAuthnDevice deletes a registered WebAuthn device.
func (p *SQLProvider) DeleteWebauthnDevice(ctx context.Context, kid string) (err error) { func (p *SQLProvider) DeleteWebAuthnDevice(ctx context.Context, kid string) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDevice, kid); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebAuthnDevice, kid); err != nil {
return fmt.Errorf("error deleting webauthn device with kid '%s': %w", kid, err) return fmt.Errorf("error deleting WebAuthn device with kid '%s': %w", kid, err)
} }
return nil return nil
} }
// DeleteWebauthnDeviceByUsername deletes registered Webauthn devices by username or username and description. // DeleteWebAuthnDeviceByUsername deletes registered WebAuthn devices by username or username and description.
func (p *SQLProvider) DeleteWebauthnDeviceByUsername(ctx context.Context, username, displayname string) (err error) { func (p *SQLProvider) DeleteWebAuthnDeviceByUsername(ctx context.Context, username, displayname string) (err error) {
if len(username) == 0 { if len(username) == 0 {
return fmt.Errorf("error deleting webauthn device with username '%s' and displayname '%s': username must not be empty", username, displayname) return fmt.Errorf("error deleting WebAuthn device with username '%s' and displayname '%s': username must not be empty", username, displayname)
} }
if len(displayname) == 0 { if len(displayname) == 0 {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsername, username); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebAuthnDeviceByUsername, username); err != nil {
return fmt.Errorf("error deleting webauthn devices for username '%s': %w", username, err) return fmt.Errorf("error deleting WebAuthn devices for username '%s': %w", username, err)
} }
} else { } else {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsernameAndDisplayName, username, displayname); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebAuthnDeviceByUsernameAndDisplayName, username, displayname); err != nil {
return fmt.Errorf("error deleting webauthn device with username '%s' and displayname '%s': %w", username, displayname, err) return fmt.Errorf("error deleting WebAuthn device with username '%s' and displayname '%s': %w", username, displayname, err)
} }
} }
return nil return nil
} }
// LoadWebauthnDevices loads Webauthn device registrations. // LoadWebAuthnDevices loads WebAuthn device registrations.
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error) { func (p *SQLProvider) LoadWebAuthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error) {
devices = make([]model.WebauthnDevice, 0, limit) devices = make([]model.WebAuthnDevice, 0, limit)
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevices, limit, limit*page); err != nil { if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebAuthnDevices, limit, limit*page); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
return nil, fmt.Errorf("error selecting Webauthn devices: %w", err) return nil, fmt.Errorf("error selecting WebAuthn devices: %w", err)
} }
for i, device := range devices { for i, device := range devices {
if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil { if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil {
return nil, fmt.Errorf("error decrypting Webauthn public key for user '%s': %w", device.Username, err) return nil, fmt.Errorf("error decrypting WebAuthn public key for user '%s': %w", device.Username, err)
} }
} }
return devices, nil return devices, nil
} }
// LoadWebauthnDeviceByID loads a webauthn device registration for a given id. // LoadWebAuthnDeviceByID loads a WebAuthn device registration for a given id.
func (p *SQLProvider) LoadWebauthnDeviceByID(ctx context.Context, id int) (device *model.WebauthnDevice, err error) { func (p *SQLProvider) LoadWebAuthnDeviceByID(ctx context.Context, id int) (device *model.WebAuthnDevice, err error) {
device = &model.WebauthnDevice{} device = &model.WebAuthnDevice{}
if err = p.db.GetContext(ctx, device, p.sqlSelectWebauthnDeviceByID, id); err != nil { if err = p.db.GetContext(ctx, device, p.sqlSelectWebAuthnDeviceByID, id); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, sql.ErrNoRows return nil, sql.ErrNoRows
} }
return nil, fmt.Errorf("error selecting Webauthn device with id '%d': %w", id, err) return nil, fmt.Errorf("error selecting WebAuthn device with id '%d': %w", id, err)
} }
return device, nil return device, nil
} }
// LoadWebauthnDevicesByUsername loads all webauthn devices registration for a given username. // LoadWebAuthnDevicesByUsername loads all WebAuthn devices registration for a given username.
func (p *SQLProvider) LoadWebauthnDevicesByUsername(ctx context.Context, rpid, username string) (devices []model.WebauthnDevice, err error) { func (p *SQLProvider) LoadWebAuthnDevicesByUsername(ctx context.Context, rpid, username string) (devices []model.WebAuthnDevice, err error) {
switch len(rpid) { switch len(rpid) {
case 0: case 0:
err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevicesByUsername, username) err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebAuthnDevicesByUsername, username)
default: default:
err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevicesByRPIDByUsername, rpid, username) err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebAuthnDevicesByRPIDByUsername, rpid, username)
} }
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return devices, ErrNoWebauthnDevice return devices, ErrNoWebAuthnDevice
} }
return nil, fmt.Errorf("error selecting Webauthn devices for user '%s': %w", username, err) return nil, fmt.Errorf("error selecting WebAuthn devices for user '%s': %w", username, err)
} }
for i, device := range devices { for i, device := range devices {
if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil { if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil {
return nil, fmt.Errorf("error decrypting Webauthn public key for user '%s': %w", username, err) return nil, fmt.Errorf("error decrypting WebAuthn public key for user '%s': %w", username, err)
} }
} }

View File

@ -58,19 +58,19 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo
provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig) provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig)
provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs) provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
provider.sqlInsertWebauthnUser = provider.db.Rebind(provider.sqlInsertWebauthnUser) provider.sqlInsertWebAuthnUser = provider.db.Rebind(provider.sqlInsertWebAuthnUser)
provider.sqlSelectWebauthnUser = provider.db.Rebind(provider.sqlSelectWebauthnUser) provider.sqlSelectWebAuthnUser = provider.db.Rebind(provider.sqlSelectWebAuthnUser)
provider.sqlInsertWebauthnDevice = provider.db.Rebind(provider.sqlInsertWebauthnDevice) provider.sqlInsertWebAuthnDevice = provider.db.Rebind(provider.sqlInsertWebAuthnDevice)
provider.sqlSelectWebauthnDevices = provider.db.Rebind(provider.sqlSelectWebauthnDevices) provider.sqlSelectWebAuthnDevices = provider.db.Rebind(provider.sqlSelectWebAuthnDevices)
provider.sqlSelectWebauthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByUsername) provider.sqlSelectWebAuthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebAuthnDevicesByUsername)
provider.sqlSelectWebauthnDevicesByRPIDByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByRPIDByUsername) provider.sqlSelectWebAuthnDevicesByRPIDByUsername = provider.db.Rebind(provider.sqlSelectWebAuthnDevicesByRPIDByUsername)
provider.sqlSelectWebauthnDeviceByID = provider.db.Rebind(provider.sqlSelectWebauthnDeviceByID) provider.sqlSelectWebAuthnDeviceByID = provider.db.Rebind(provider.sqlSelectWebAuthnDeviceByID)
provider.sqlUpdateWebauthnDeviceDescriptionByUsernameAndID = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceDescriptionByUsernameAndID) provider.sqlUpdateWebAuthnDeviceDescriptionByUsernameAndID = provider.db.Rebind(provider.sqlUpdateWebAuthnDeviceDescriptionByUsernameAndID)
provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn) provider.sqlUpdateWebAuthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebAuthnDeviceRecordSignIn)
provider.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice) provider.sqlDeleteWebAuthnDevice = provider.db.Rebind(provider.sqlDeleteWebAuthnDevice)
provider.sqlDeleteWebauthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsername) provider.sqlDeleteWebAuthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebAuthnDeviceByUsername)
provider.sqlDeleteWebauthnDeviceByUsernameAndDisplayName = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndDisplayName) provider.sqlDeleteWebAuthnDeviceByUsernameAndDisplayName = provider.db.Rebind(provider.sqlDeleteWebAuthnDeviceByUsernameAndDisplayName)
provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice) provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)
provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice) provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice)

View File

@ -34,7 +34,7 @@ func (p *SQLProvider) SchemaEncryptionChangeKey(ctx context.Context, key string)
encChangeFuncs := []EncryptionChangeKeyFunc{ encChangeFuncs := []EncryptionChangeKeyFunc{
schemaEncryptionChangeKeyTOTP, schemaEncryptionChangeKeyTOTP,
schemaEncryptionChangeKeyWebauthn, schemaEncryptionChangeKeyWebAuthn,
} }
for i := 0; true; i++ { for i := 0; true; i++ {
@ -90,7 +90,7 @@ func (p *SQLProvider) SchemaEncryptionCheckKey(ctx context.Context, verbose bool
if verbose { if verbose {
encCheckFuncs := []EncryptionCheckKeyFunc{ encCheckFuncs := []EncryptionCheckKeyFunc{
schemaEncryptionCheckKeyTOTP, schemaEncryptionCheckKeyTOTP,
schemaEncryptionCheckKeyWebauthn, schemaEncryptionCheckKeyWebAuthn,
} }
for i := 0; true; i++ { for i := 0; true; i++ {
@ -153,10 +153,10 @@ func schemaEncryptionChangeKeyTOTP(ctx context.Context, provider *SQLProvider, t
return nil return nil
} }
func schemaEncryptionChangeKeyWebauthn(ctx context.Context, provider *SQLProvider, tx *sqlx.Tx, key [32]byte) (err error) { func schemaEncryptionChangeKeyWebAuthn(ctx context.Context, provider *SQLProvider, tx *sqlx.Tx, key [32]byte) (err error) {
var count int var count int
if err = tx.GetContext(ctx, &count, fmt.Sprintf(queryFmtSelectRowCount, tableWebauthnDevices)); err != nil { if err = tx.GetContext(ctx, &count, fmt.Sprintf(queryFmtSelectRowCount, tableWebAuthnDevices)); err != nil {
return err return err
} }
@ -164,29 +164,29 @@ func schemaEncryptionChangeKeyWebauthn(ctx context.Context, provider *SQLProvide
return nil return nil
} }
devices := make([]encWebauthnDevice, 0, count) devices := make([]encWebAuthnDevice, 0, count)
if err = tx.SelectContext(ctx, &devices, fmt.Sprintf(queryFmtSelectWebauthnDevicesEncryptedData, tableWebauthnDevices)); err != nil { if err = tx.SelectContext(ctx, &devices, fmt.Sprintf(queryFmtSelectWebAuthnDevicesEncryptedData, tableWebAuthnDevices)); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil return nil
} }
return fmt.Errorf("error selecting Webauthn devices: %w", err) return fmt.Errorf("error selecting WebAuthn devices: %w", err)
} }
query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebauthnDevicesEncryptedData, tableWebauthnDevices)) query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebAuthnDevicesEncryptedData, tableWebAuthnDevices))
for _, d := range devices { for _, d := range devices {
if d.PublicKey, err = provider.decrypt(d.PublicKey); err != nil { if d.PublicKey, err = provider.decrypt(d.PublicKey); err != nil {
return fmt.Errorf("error decrypting Webauthn device public key with id '%d': %w", d.ID, err) return fmt.Errorf("error decrypting WebAuthn device public key with id '%d': %w", d.ID, err)
} }
if d.PublicKey, err = utils.Encrypt(d.PublicKey, &key); err != nil { if d.PublicKey, err = utils.Encrypt(d.PublicKey, &key); err != nil {
return fmt.Errorf("error encrypting Webauthn device public key with id '%d': %w", d.ID, err) return fmt.Errorf("error encrypting WebAuthn device public key with id '%d': %w", d.ID, err)
} }
if _, err = tx.ExecContext(ctx, query, d.PublicKey, d.ID); err != nil { if _, err = tx.ExecContext(ctx, query, d.PublicKey, d.ID); err != nil {
return fmt.Errorf("error updating Webauthn device public key with id '%d': %w", d.ID, err) return fmt.Errorf("error updating WebAuthn device public key with id '%d': %w", d.ID, err)
} }
} }
@ -262,17 +262,17 @@ func schemaEncryptionCheckKeyTOTP(ctx context.Context, provider *SQLProvider) (t
return tableTOTPConfigurations, result return tableTOTPConfigurations, result
} }
func schemaEncryptionCheckKeyWebauthn(ctx context.Context, provider *SQLProvider) (table string, result EncryptionValidationTableResult) { func schemaEncryptionCheckKeyWebAuthn(ctx context.Context, provider *SQLProvider) (table string, result EncryptionValidationTableResult) {
var ( var (
rows *sqlx.Rows rows *sqlx.Rows
err error err error
) )
if rows, err = provider.db.QueryxContext(ctx, fmt.Sprintf(queryFmtSelectWebauthnDevicesEncryptedData, tableWebauthnDevices)); err != nil { if rows, err = provider.db.QueryxContext(ctx, fmt.Sprintf(queryFmtSelectWebAuthnDevicesEncryptedData, tableWebAuthnDevices)); err != nil {
return tableWebauthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error selecting Webauthn devices: %w", err)} return tableWebAuthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error selecting WebAuthn devices: %w", err)}
} }
var device encWebauthnDevice var device encWebAuthnDevice
for rows.Next() { for rows.Next() {
result.Total++ result.Total++
@ -280,7 +280,7 @@ func schemaEncryptionCheckKeyWebauthn(ctx context.Context, provider *SQLProvider
if err = rows.StructScan(&device); err != nil { if err = rows.StructScan(&device); err != nil {
_ = rows.Close() _ = rows.Close()
return tableWebauthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error scanning Webauthn device to struct: %w", err)} return tableWebAuthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error scanning WebAuthn device to struct: %w", err)}
} }
if _, err = provider.decrypt(device.PublicKey); err != nil { if _, err = provider.decrypt(device.PublicKey); err != nil {
@ -290,7 +290,7 @@ func schemaEncryptionCheckKeyWebauthn(ctx context.Context, provider *SQLProvider
_ = rows.Close() _ = rows.Close()
return tableWebauthnDevices, result return tableWebAuthnDevices, result
} }
func schemaEncryptionCheckKeyOpenIDConnect(typeOAuth2Session OAuth2SessionType) EncryptionCheckKeyFunc { func schemaEncryptionCheckKeyOpenIDConnect(typeOAuth2Session OAuth2SessionType) EncryptionCheckKeyFunc {

View File

@ -119,71 +119,71 @@ const (
) )
const ( const (
queryFmtSelectWebauthnDevices = ` queryFmtSelectWebAuthnDevices = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
FROM %s FROM %s
LIMIT ? LIMIT ?
OFFSET ?;` OFFSET ?;`
queryFmtSelectWebauthnDevicesByUsername = ` queryFmtSelectWebAuthnDevicesByUsername = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
FROM %s FROM %s
WHERE username = ?;` WHERE username = ?;`
queryFmtSelectWebauthnDevicesByRPIDByUsername = ` queryFmtSelectWebAuthnDevicesByRPIDByUsername = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
FROM %s FROM %s
WHERE rpid = ? AND username = ?;` WHERE rpid = ? AND username = ?;`
queryFmtSelectWebauthnDeviceByID = ` queryFmtSelectWebAuthnDeviceByID = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key SELECT id, created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key
FROM %s FROM %s
WHERE id = ?;` WHERE id = ?;`
queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID = ` queryFmtUpdateUpdateWebAuthnDeviceDescriptionByUsernameAndID = `
UPDATE %s UPDATE %s
SET description = ? SET description = ?
WHERE username = ? AND id = ?;` WHERE username = ? AND id = ?;`
queryFmtUpdateWebauthnDeviceRecordSignIn = ` queryFmtUpdateWebAuthnDeviceRecordSignIn = `
UPDATE %s UPDATE %s
SET SET
rpid = ?, last_used_at = ?, sign_count = ?, discoverable = ?, present = ?, verified = ?, backup_eligible = ?, backup_state = ?, rpid = ?, last_used_at = ?, sign_count = ?, discoverable = ?, present = ?, verified = ?, backup_eligible = ?, backup_state = ?,
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
WHERE id = ?;` WHERE id = ?;`
queryFmtInsertWebauthnDevice = ` queryFmtInsertWebAuthnDevice = `
INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key) INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, aaguid, attestation_type, attachment, transport, sign_count, clone_warning, discoverable, present, verified, backup_eligible, backup_state, public_key)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
queryFmtDeleteWebauthnDevice = ` queryFmtDeleteWebAuthnDevice = `
DELETE FROM %s DELETE FROM %s
WHERE kid = ?;` WHERE kid = ?;`
queryFmtDeleteWebauthnDeviceByUsername = ` queryFmtDeleteWebAuthnDeviceByUsername = `
DELETE FROM %s DELETE FROM %s
WHERE username = ?;` WHERE username = ?;`
queryFmtDeleteWebauthnDeviceByUsernameAndDescription = ` queryFmtDeleteWebAuthnDeviceByUsernameAndDescription = `
DELETE FROM %s DELETE FROM %s
WHERE username = ? AND description = ?;` WHERE username = ? AND description = ?;`
queryFmtSelectWebauthnDevicesEncryptedData = ` queryFmtSelectWebAuthnDevicesEncryptedData = `
SELECT id, public_key SELECT id, public_key
FROM %s;` FROM %s;`
queryFmtUpdateWebauthnDevicesEncryptedData = ` queryFmtUpdateWebAuthnDevicesEncryptedData = `
UPDATE %s UPDATE %s
SET public_key = ? SET public_key = ?
WHERE id = ?;` WHERE id = ?;`
) )
const ( const (
queryFmtInsertWebauthnUser = ` queryFmtInsertWebAuthnUser = `
INSERT INTO %s (rpid, username, userid) INSERT INTO %s (rpid, username, userid)
VALUES (?, ?, ?);` VALUES (?, ?, ?);`
queryFmtSelectWebauthnUser = ` queryFmtSelectWebAuthnUser = `
SELECT id, rpid, username, userid SELECT id, rpid, username, userid
FROM %s FROM %s
WHERE rpid = ? AND username = ?;` WHERE rpid = ? AND username = ?;`

View File

@ -32,7 +32,7 @@ type encOAuth2Session struct {
Session []byte `db:"session_data"` Session []byte `db:"session_data"`
} }
type encWebauthnDevice struct { type encWebAuthnDevice struct {
ID int `db:"id"` ID int `db:"id"`
PublicKey []byte `db:"public_key"` PublicKey []byte `db:"public_key"`
} }

1
web/.gitignore vendored
View File

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

View File

@ -10,15 +10,8 @@
"peerDependencyRules": { "peerDependencyRules": {
"allowedVersions": { "allowedVersions": {
"@types/react": "18", "@types/react": "18",
"react": "18", "react": "18"
"react-dom": "18" }
},
"ignoreMissing": [
"@babel/core",
"@babel/plugin-syntax-flow",
"@babel/plugin-transform-react-jsx",
"prop-types"
]
} }
}, },
"dependencies": { "dependencies": {
@ -55,83 +48,14 @@
"build": "vite build", "build": "vite build",
"coverage": "VITE_COVERAGE=true vite build", "coverage": "VITE_COVERAGE=true vite build",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"test": "jest --coverage --no-cache", "test": "vitest run --coverage",
"test:watch": "vitest --coverage",
"test:preview": "vitest-preview",
"report": "nyc report -r clover -r json -r lcov -r text" "report": "nyc report -r clover -r json -r lcov -r text"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
}, },
"jest": {
"roots": [
"<rootDir>/src"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
],
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],
"testMatch": [
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
],
"testEnvironment": "jsdom",
"transform": {
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": [
"esbuild-jest",
{
"sourcemap": true
}
],
"^.+\\.(css|png|svg)$": "jest-transform-stub"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\](?!(\\.pnpm[/\\\\])?(@simplewebauthn[+/\\\\]browser)).+\\.(js|jsx|cjs|ts|tsx)$"
],
"moduleNameMapper": {
"^@root/(.*)$": [
"<rootDir>/src/$1"
],
"^@assets/(.*)$": [
"<rootDir>/src/assets/$1"
],
"^@components/(.*)$": [
"<rootDir>/src/components/$1"
],
"^@constants/(.*)$": [
"<rootDir>/src/constants/$1"
],
"^@hooks/(.*)$": [
"<rootDir>/src/hooks/$1"
],
"^@i18n/(.*)$": [
"<rootDir>/src/i18n/$1"
],
"^@layouts/(.*)$": [
"<rootDir>/src/layouts/$1"
],
"^@models/(.*)$": [
"<rootDir>/src/models/$1"
],
"^@services/(.*)$": [
"<rootDir>/src/services/$1"
],
"^@themes/(.*)$": [
"<rootDir>/src/themes/$1"
],
"^@utils/(.*)$": [
"<rootDir>/src/utils/$1"
],
"^@views/(.*)$": [
"<rootDir>/src/views/$1"
]
},
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
],
"resetMocks": true
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
@ -153,16 +77,16 @@
"@limegrass/eslint-plugin-import-alias": "1.0.6", "@limegrass/eslint-plugin-import-alias": "1.0.6",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0", "@testing-library/react": "14.0.0",
"@types/jest": "29.5.0",
"@types/node": "18.15.11", "@types/node": "18.15.11",
"@types/react": "18.0.33", "@types/react": "18.0.34",
"@types/react-dom": "18.0.11", "@types/react-dom": "18.0.11",
"@types/testing-library__jest-dom": "5.14.5",
"@types/zxcvbn": "4.4.1", "@types/zxcvbn": "4.4.1",
"@typescript-eslint/eslint-plugin": "5.57.1", "@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.57.1", "@typescript-eslint/parser": "5.58.0",
"@vitejs/plugin-react": "3.1.0", "@vitejs/plugin-react": "3.1.0",
"esbuild": "0.17.15", "@vitest/coverage-istanbul": "0.30.0",
"esbuild-jest": "0.5.0", "esbuild": "0.17.16",
"eslint": "8.38.0", "eslint": "8.38.0",
"eslint-config-prettier": "8.8.0", "eslint-config-prettier": "8.8.0",
"eslint-config-react-app": "7.0.1", "eslint-config-react-app": "7.0.1",
@ -173,11 +97,8 @@
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2", "eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"happy-dom": "9.1.9",
"husky": "8.0.3", "husky": "8.0.3",
"jest": "29.5.0",
"jest-environment-jsdom": "29.5.0",
"jest-transform-stub": "2.0.0",
"jest-watch-typeahead": "2.2.2",
"prettier": "2.8.7", "prettier": "2.8.7",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"typescript": "5.0.4", "typescript": "5.0.4",
@ -185,6 +106,8 @@
"vite-plugin-eslint": "1.8.1", "vite-plugin-eslint": "1.8.1",
"vite-plugin-istanbul": "4.0.1", "vite-plugin-istanbul": "4.0.1",
"vite-plugin-svgr": "2.4.0", "vite-plugin-svgr": "2.4.0",
"vite-tsconfig-paths": "4.0.8" "vite-tsconfig-paths": "4.1.0",
"vitest": "0.30.0",
"vitest-preview": "0.0.1"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,34 @@
import React from "react"; import React from "react";
import { render } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import NotificationBar from "@components/NotificationBar"; import NotificationBar from "@components/NotificationBar";
import NotificationsContext from "@hooks/NotificationsContext";
import { Notification } from "@models/Notifications";
const testNotification: Notification = {
message: "Test notification",
level: "success",
timeout: 3,
};
it("renders without crashing", () => { it("renders without crashing", () => {
render(<NotificationBar onClose={() => {}} />); render(<NotificationBar onClose={() => {}} />);
}); });
it("displays notification message and level correctly", async () => {
render(
<NotificationsContext.Provider value={{ notification: testNotification, setNotification: () => {} }}>
<NotificationBar onClose={() => {}} />
</NotificationsContext.Provider>,
);
const alert = await screen.getByRole("alert");
const message = await screen.findByText(testNotification.message);
expect(alert).toHaveClass(
`MuiAlert-filled${testNotification.level.charAt(0).toUpperCase() + testNotification.level.substring(1)}`,
{ exact: false },
);
expect(message).toHaveTextContent(testNotification.message);
});

View File

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

View File

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

View File

@ -1,9 +1,37 @@
import React from "react"; import React from "react";
import { render } from "@testing-library/react"; import { act, render } from "@testing-library/react";
import TimerIcon from "@components/TimerIcon"; import TimerIcon from "@components/TimerIcon";
beforeEach(() => {
vi.useFakeTimers().setSystemTime(new Date(2023, 1, 1, 8));
});
afterEach(() => {
vi.useRealTimers();
});
it("renders without crashing", () => { it("renders without crashing", () => {
render(<TimerIcon width={32} height={32} period={30} />); render(<TimerIcon width={32} height={32} period={30} />);
}); });
it("renders a timer icon with updating progress for a given period", async () => {
const { container } = render(<TimerIcon width={32} height={32} period={30} />);
const initialProgress =
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
"stroke-dasharray",
);
expect(initialProgress).toBe("0 31.6");
act(() => {
vi.advanceTimersByTime(3000);
});
const updatedProgress =
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
"stroke-dasharray",
);
expect(updatedProgress).toBe("3.16 31.6");
expect(Number(updatedProgress!.split(/\s(.+)/)[0])).toBeGreaterThan(Number(initialProgress!.split(/\s(.+)/)[0]));
});

View File

@ -2,12 +2,41 @@ import React from "react";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import TypographyWithTooltip from "@components/TypographyWithTootip"; import TypographyWithTooltip, { Props } from "@components/TypographyWithTooltip";
const defaultProps: Props = {
variant: "h5",
value: "Example",
};
it("renders without crashing", () => { it("renders without crashing", () => {
render(<TypographyWithTooltip value={"Example"} variant={"h5"} />); render(<TypographyWithTooltip {...defaultProps} />);
}); });
it("renders with tooltip without crashing", () => { it("renders with tooltip without crashing", () => {
render(<TypographyWithTooltip value={"Example"} tooltip={"A tooltip"} variant={"h5"} />); const props: Props = {
...defaultProps,
tooltip: "A tooltip",
};
render(<TypographyWithTooltip {...props} />);
});
it("renders the text correctly", () => {
const props: Props = {
...defaultProps,
value: "Test text",
};
const { getByText } = render(<TypographyWithTooltip {...props} />);
const element = getByText(props.value!);
expect(element).toBeInTheDocument();
});
it("renders the tooltip correctly", () => {
const props: Props = {
...defaultProps,
tooltip: "Test tooltip",
};
const { getByText } = render(<TypographyWithTooltip {...props} />);
const element = getByText(props.value!);
expect(element).toHaveAttribute("aria-label", props.tooltip);
}); });

View File

@ -9,7 +9,7 @@ import { useNavigate } from "react-router-dom";
import { ReactComponent as UserSvg } from "@assets/images/user.svg"; import { ReactComponent as UserSvg } from "@assets/images/user.svg";
import Brand from "@components/Brand"; import Brand from "@components/Brand";
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer"; import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
import TypographyWithTooltip from "@components/TypographyWithTootip"; import TypographyWithTooltip from "@components/TypographyWithTooltip";
import { SettingsRoute } from "@constants/Routes"; import { SettingsRoute } from "@constants/Routes";
import { getLogoOverride } from "@utils/Configuration"; import { getLogoOverride } from "@utils/Configuration";

View File

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

View File

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

View File

@ -21,7 +21,7 @@
"dom.iterable", "dom.iterable",
"esnext" "esnext"
], ],
"types": ["@types/jest", "vite/client", "vite-plugin-svgr/client"], "types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,

View File

@ -5,18 +5,17 @@ import istanbul from "vite-plugin-istanbul";
import svgr from "vite-plugin-svgr"; import svgr from "vite-plugin-svgr";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
// @ts-ignore
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const isCoverage = process.env.VITE_COVERAGE === "true"; const isCoverage = process.env.VITE_COVERAGE === "true";
const sourcemap = isCoverage ? "inline" : undefined; const sourcemap = isCoverage ? "inline" : undefined;
const istanbulPlugin = isCoverage const istanbulPlugin = isCoverage
? istanbul({ ? istanbul({
include: "src/*", checkProd: false,
exclude: ["node_modules"], exclude: ["node_modules"],
extension: [".js", ".jsx", ".ts", ".tsx"], extension: [".js", ".jsx", ".ts", ".tsx"],
checkProd: false,
forceBuildInstrument: true, forceBuildInstrument: true,
include: "src/*",
requireEnv: true, requireEnv: true,
}) })
: undefined; : undefined;
@ -24,14 +23,11 @@ export default defineConfig(({ mode }) => {
return { return {
base: "./", base: "./",
build: { build: {
sourcemap,
outDir: "../internal/server/public_html",
emptyOutDir: true,
assetsDir: "static", assetsDir: "static",
emptyOutDir: true,
outDir: "../internal/server/public_html",
rollupOptions: { rollupOptions: {
output: { output: {
entryFileNames: `static/js/[name].[hash].js`,
chunkFileNames: `static/js/[name].[hash].js`,
assetFileNames: ({ name }) => { assetFileNames: ({ name }) => {
if (name && name.endsWith(".css")) { if (name && name.endsWith(".css")) {
return "static/css/[name].[hash].[ext]"; return "static/css/[name].[hash].[ext]";
@ -39,12 +35,26 @@ export default defineConfig(({ mode }) => {
return "static/media/[name].[hash].[ext]"; return "static/media/[name].[hash].[ext]";
}, },
chunkFileNames: `static/js/[name].[hash].js`,
entryFileNames: `static/js/[name].[hash].js`,
}, },
}, },
sourcemap,
}, },
server: { server: {
port: 3000,
open: false, open: false,
port: 3000,
},
test: {
coverage: {
provider: "istanbul",
},
environment: "happy-dom",
globals: true,
onConsoleLog(log) {
if (log.includes('No routes matched location "blank"')) return false;
},
setupFiles: ["src/setupTests.ts"],
}, },
plugins: [eslintPlugin({ cache: false }), istanbulPlugin, react(), svgr(), tsconfigPaths()], plugins: [eslintPlugin({ cache: false }), istanbulPlugin, react(), svgr(), tsconfigPaths()],
}; };