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.tsxfeat-otp-verification
commit
7fdcc351d4
|
@ -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] [Webauthn] with devices like a [YubiKey].
|
[FIDO2] [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/
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
const (
|
const (
|
||||||
versionSwaggerUI = "4.18.1"
|
versionSwaggerUI = "4.18.2"
|
||||||
)
|
)
|
||||||
|
|
|
@ -49,7 +49,7 @@ authelia configuration, and authelia database prior to attempting to do so._
|
||||||
Notable Missing Features from this build:
|
Notable Missing Features from this build:
|
||||||
|
|
||||||
- OpenID Connect 1.0 PAR
|
- OpenID Connect 1.0 PAR
|
||||||
- Multi-Device Webauthn
|
- Multi-Device WebAuthn
|
||||||
- Device Registration OTP
|
- Device Registration OTP
|
||||||
|
|
||||||
- Container Images:
|
- Container Images:
|
||||||
|
@ -144,7 +144,7 @@ Please see the [roadmap](../../roadmap/active/openid-connect.md) for more inform
|
||||||
|
|
||||||
##### Initial Implementation
|
##### Initial Implementation
|
||||||
|
|
||||||
_**Important Note:** This feature at the time of this writing, will not work well with Webauthn. Steps are being taken
|
_**Important Note:** This feature at the time of this writing, will not work well with WebAuthn. Steps are being taken
|
||||||
to address this however it will not specifically delay the release of this feature._
|
to address this however it will not specifically delay the release of this feature._
|
||||||
|
|
||||||
This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a
|
This release see's the initial implementation of multi-domain protection. Users will be able to configure more than a
|
||||||
|
@ -160,14 +160,14 @@ NGINX/NGINX Proxy Manager/SWAG/HAProxy with the use of the new
|
||||||
[Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only
|
[Customizable Authorization Endpoints](#customizable-authorization-endpoints). This is important as it means you only
|
||||||
need to configure a single middleware or helper to perform automatic redirection.
|
need to configure a single middleware or helper to perform automatic redirection.
|
||||||
|
|
||||||
## Webauthn
|
## WebAuthn
|
||||||
|
|
||||||
As part of our ongoing effort for comprehensive support for Webauthn we'll be introducing several important
|
As part of our ongoing effort for comprehensive support for WebAuthn we'll be introducing several important
|
||||||
features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information.
|
features. Please see the [roadmap](../../roadmap/active/webauthn.md) for more information.
|
||||||
|
|
||||||
##### Multiple Webauthn Credentials Per-User
|
##### Multiple WebAuthn Credentials Per-User
|
||||||
|
|
||||||
In this release we see full support for multiple Webauthn credentials. This is a fairly basic feature but getting the
|
In this release we see full support for multiple WebAuthn credentials. This is a fairly basic feature but getting the
|
||||||
frontend experience right is important to us. This is going to be supported via the
|
frontend experience right is important to us. This is going to be supported via the
|
||||||
[User Control Panel](#user-dashboard--control-panel).
|
[User Control Panel](#user-dashboard--control-panel).
|
||||||
|
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Testing"
|
title: "Testing"
|
||||||
description: "Authelia Development Testing Guidelines"
|
description: "Authelia Development Testing Guidelines"
|
||||||
lead: "This section covers the testing guidelines."
|
lead: "This section covers the testing guidelines."
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-20T15:03:52+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Amir Zarrinkafsh"
|
title: "Amir Zarrinkafsh"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Clément Michaud"
|
title: "Clément Michaud"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Manuel Nuñez"
|
title: "Manuel Nuñez"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
title: "About"
|
title: "About"
|
||||||
description: "About Authelia and the Authelia Team"
|
description: "About Authelia and the Authelia Team"
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-19T16:29:12+10:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
aliases:
|
aliases:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Firezone"
|
title: "Firezone"
|
||||||
description: "Integrating Firezone with the Authelia OpenID Connect Provider."
|
description: "Integrating Firezone with the Authelia OpenID Connect Provider."
|
||||||
lead: ""
|
lead: ""
|
||||||
date: 2023-03-25T13:07:02+10:00
|
date: 2023-03-28T20:29:13+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "MinIO"
|
title: "MinIO"
|
||||||
description: "Integrating MinIO with the Authelia OpenID Connect Provider."
|
description: "Integrating MinIO with the Authelia OpenID Connect Provider."
|
||||||
lead: ""
|
lead: ""
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2023-03-21T11:21:23+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Misago"
|
title: "Misago"
|
||||||
description: "Integrating Misago with the Authelia OpenID Connect Provider."
|
description: "Integrating Misago with the Authelia OpenID Connect Provider."
|
||||||
lead: ""
|
lead: ""
|
||||||
date: 2023-03-04T13:20:00+00:00
|
date: 2023-03-14T08:51:13+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -63,5 +63,5 @@ authelia storage user --help
|
||||||
* [authelia storage](authelia_storage.md) - Manage the Authelia storage
|
* [authelia storage](authelia_storage.md) - Manage the Authelia storage
|
||||||
* [authelia storage user identifiers](authelia_storage_user_identifiers.md) - Manage user opaque identifiers
|
* [authelia storage user identifiers](authelia_storage_user_identifiers.md) - Manage user opaque identifiers
|
||||||
* [authelia storage user totp](authelia_storage_user_totp.md) - Manage TOTP configurations
|
* [authelia storage user totp](authelia_storage_user_totp.md) - Manage TOTP configurations
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn
|
## authelia storage user webauthn
|
||||||
|
|
||||||
Manage Webauthn devices
|
Manage WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Manage Webauthn devices.
|
Manage WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows interacting with Webauthn devices.
|
This subcommand allows interacting with WebAuthn devices.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ authelia storage user webauthn --help
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user](authelia_storage_user.md) - Manages user settings
|
* [authelia storage user](authelia_storage_user.md) - Manages user settings
|
||||||
* [authelia storage user webauthn delete](authelia_storage_user_webauthn_delete.md) - Delete a Webauthn device
|
* [authelia storage user webauthn delete](authelia_storage_user_webauthn_delete.md) - Delete a WebAuthn device
|
||||||
* [authelia storage user webauthn export](authelia_storage_user_webauthn_export.md) - Perform exports of the Webauthn devices
|
* [authelia storage user webauthn export](authelia_storage_user_webauthn_export.md) - Perform exports of the WebAuthn devices
|
||||||
* [authelia storage user webauthn import](authelia_storage_user_webauthn_import.md) - Perform imports of the Webauthn devices
|
* [authelia storage user webauthn import](authelia_storage_user_webauthn_import.md) - Perform imports of the WebAuthn devices
|
||||||
* [authelia storage user webauthn list](authelia_storage_user_webauthn_list.md) - List Webauthn devices
|
* [authelia storage user webauthn list](authelia_storage_user_webauthn_list.md) - List WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn delete
|
## authelia storage user webauthn delete
|
||||||
|
|
||||||
Delete a Webauthn device
|
Delete a WebAuthn device
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Delete a Webauthn device.
|
Delete a WebAuthn device.
|
||||||
|
|
||||||
This subcommand allows deleting a Webauthn device directly from the database.
|
This subcommand allows deleting a WebAuthn device directly from the database.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn delete [username] [flags]
|
authelia storage user webauthn delete [username] [flags]
|
||||||
|
@ -75,5 +75,5 @@ authelia storage user webauthn delete --kid abc123 --encryption-key b3453fde-ecc
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn export
|
## authelia storage user webauthn export
|
||||||
|
|
||||||
Perform exports of the Webauthn devices
|
Perform exports of the WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Perform exports of the Webauthn devices.
|
Perform exports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows exporting Webauthn devices to various formats.
|
This subcommand allows exporting WebAuthn devices to various formats.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn export [flags]
|
authelia storage user webauthn export [flags]
|
||||||
|
@ -68,5 +68,5 @@ authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-27
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn import
|
## authelia storage user webauthn import
|
||||||
|
|
||||||
Perform imports of the Webauthn devices
|
Perform imports of the WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
Perform imports of the Webauthn devices.
|
Perform imports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows importing Webauthn devices from various formats.
|
This subcommand allows importing WebAuthn devices from various formats.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn import <filename> [flags]
|
authelia storage user webauthn import <filename> [flags]
|
||||||
|
@ -67,5 +67,5 @@ authelia storage user webauthn import --file authelia.export.webauthn.yaml --enc
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@ toc: true
|
||||||
|
|
||||||
## authelia storage user webauthn list
|
## authelia storage user webauthn list
|
||||||
|
|
||||||
List Webauthn devices
|
List WebAuthn devices
|
||||||
|
|
||||||
### Synopsis
|
### Synopsis
|
||||||
|
|
||||||
List Webauthn devices.
|
List WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows listing Webauthn devices.
|
This subcommand allows listing WebAuthn devices.
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia storage user webauthn list [username] [flags]
|
authelia storage user webauthn list [username] [flags]
|
||||||
|
@ -69,5 +69,5 @@ authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-942
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage Webauthn devices
|
* [authelia storage user webauthn](authelia_storage_user_webauthn.md) - Manage WebAuthn devices
|
||||||
|
|
||||||
|
|
|
@ -39,27 +39,27 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "7.21.0",
|
"@babel/cli": "7.21.0",
|
||||||
"@babel/core": "7.21.0",
|
"@babel/core": "7.21.4",
|
||||||
"@babel/preset-env": "7.20.2",
|
"@babel/preset-env": "7.21.4",
|
||||||
"@fullhuman/postcss-purgecss": "5.0.0",
|
"@fullhuman/postcss-purgecss": "5.0.0",
|
||||||
"@hyas/images": "0.3.2",
|
"@hyas/images": "0.3.2",
|
||||||
"@popperjs/core": "2.11.6",
|
"@popperjs/core": "2.11.7",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.14",
|
||||||
"bootstrap": "5.2.3",
|
"bootstrap": "5.2.3",
|
||||||
"bootstrap-icons": "1.10.3",
|
"bootstrap-icons": "1.10.4",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"eslint": "8.35.0",
|
"eslint": "8.38.0",
|
||||||
"exec-bin": "1.0.0",
|
"exec-bin": "1.0.0",
|
||||||
"flexsearch": "0.7.31",
|
"flexsearch": "0.7.31",
|
||||||
"highlight.js": "11.7.0",
|
"highlight.js": "11.7.0",
|
||||||
"hugo-installer": "4.0.1",
|
"hugo-installer": "4.0.1",
|
||||||
"instant.page": "5.1.1",
|
"instant.page": "5.2.0",
|
||||||
"katex": "0.16.4",
|
"katex": "0.16.4",
|
||||||
"lazysizes": "5.3.2",
|
"lazysizes": "5.3.2",
|
||||||
"markdownlint-cli2": "0.6.0",
|
"markdownlint-cli2": "0.6.0",
|
||||||
"netlify-plugin-submit-sitemap": "0.4.0",
|
"netlify-plugin-submit-sitemap": "0.4.0",
|
||||||
"node-fetch": "3.3.0",
|
"node-fetch": "3.3.1",
|
||||||
"postcss": "8.4.21",
|
"postcss": "8.4.21",
|
||||||
"postcss-cli": "10.1.0",
|
"postcss-cli": "10.1.0",
|
||||||
"purgecss-whitelister": "2.4.0",
|
"purgecss-whitelister": "2.4.0",
|
||||||
|
@ -68,6 +68,6 @@
|
||||||
"stylelint-config-standard-scss": "6.1.0"
|
"stylelint-config-standard-scss": "6.1.0"
|
||||||
},
|
},
|
||||||
"otherDependencies": {
|
"otherDependencies": {
|
||||||
"hugo": "0.111.2"
|
"hugo": "0.111.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2197
docs/pnpm-lock.yaml
2197
docs/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -177,56 +177,56 @@ This subcommand allows manually adding an opaque identifier for a user to the da
|
||||||
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml
|
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml
|
||||||
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnShort = "Manage Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnShort = "Manage WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnLong = `Manage Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnLong = `Manage WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows interacting with Webauthn devices.`
|
This subcommand allows interacting with WebAuthn devices.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExample = `authelia storage user webauthn --help`
|
cmdAutheliaStorageUserWebAuthnExample = `authelia storage user webauthn --help`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnImportShort = "Perform imports of the Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnImportShort = "Perform imports of the WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnImportLong = `Perform imports of the Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnImportLong = `Perform imports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows importing Webauthn devices from various formats.`
|
This subcommand allows importing WebAuthn devices from various formats.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnImportExample = `authelia storage user webauthn export
|
cmdAutheliaStorageUserWebAuthnImportExample = `authelia storage user webauthn export
|
||||||
authelia storage user webauthn import --file authelia.export.webauthn.yaml
|
authelia storage user webauthn import --file authelia.export.webauthn.yaml
|
||||||
authelia storage user webauthn import --file authelia.export.webauthn.yaml --config config.yml
|
authelia storage user webauthn import --file authelia.export.webauthn.yaml --config config.yml
|
||||||
authelia storage user webauthn import --file authelia.export.webauthn.yaml --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user webauthn import --file authelia.export.webauthn.yaml --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExportShort = "Perform exports of the Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnExportShort = "Perform exports of the WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExportLong = `Perform exports of the Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnExportLong = `Perform exports of the WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows exporting Webauthn devices to various formats.`
|
This subcommand allows exporting WebAuthn devices to various formats.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnExportExample = `authelia storage user webauthn export
|
cmdAutheliaStorageUserWebAuthnExportExample = `authelia storage user webauthn export
|
||||||
authelia storage user webauthn export --file authelia.export.webauthn.yaml
|
authelia storage user webauthn export --file authelia.export.webauthn.yaml
|
||||||
authelia storage user webauthn export --config config.yml
|
authelia storage user webauthn export --config config.yml
|
||||||
authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user webauthn export--encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnListShort = "List Webauthn devices"
|
cmdAutheliaStorageUserWebAuthnListShort = "List WebAuthn devices"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnListLong = `List Webauthn devices.
|
cmdAutheliaStorageUserWebAuthnListLong = `List WebAuthn devices.
|
||||||
|
|
||||||
This subcommand allows listing Webauthn devices.`
|
This subcommand allows listing WebAuthn devices.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnListExample = `authelia storage user webauthn list
|
cmdAutheliaStorageUserWebAuthnListExample = `authelia storage user webauthn list
|
||||||
authelia storage user webauthn list john
|
authelia storage user webauthn list john
|
||||||
authelia storage user webauthn list --config config.yml
|
authelia storage user webauthn list --config config.yml
|
||||||
authelia storage user webauthn list john --config config.yml
|
authelia storage user webauthn list john --config config.yml
|
||||||
authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
||||||
authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnDeleteShort = "Delete a Webauthn device"
|
cmdAutheliaStorageUserWebAuthnDeleteShort = "Delete a WebAuthn device"
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnDeleteLong = `Delete a Webauthn device.
|
cmdAutheliaStorageUserWebAuthnDeleteLong = `Delete a WebAuthn device.
|
||||||
|
|
||||||
This subcommand allows deleting a Webauthn device directly from the database.`
|
This subcommand allows deleting a WebAuthn device directly from the database.`
|
||||||
|
|
||||||
cmdAutheliaStorageUserWebauthnDeleteExample = `authelia storage user webauthn delete john --all
|
cmdAutheliaStorageUserWebAuthnDeleteExample = `authelia storage user webauthn delete john --all
|
||||||
authelia storage user webauthn delete john --all --config config.yml
|
authelia storage user webauthn delete john --all --config config.yml
|
||||||
authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw
|
||||||
authelia storage user webauthn delete john --description Primary
|
authelia storage user webauthn delete john --description Primary
|
||||||
|
|
|
@ -68,7 +68,7 @@ func storageTOTPGenerateRunEOptsFromFlags(flags *pflag.FlagSet) (force bool, fil
|
||||||
return force, filename, secret, nil
|
return force, filename, secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func storageWebauthnDeleteRunEOptsFromFlags(flags *pflag.FlagSet, args []string) (all, byKID bool, description, kid, user string, err error) {
|
func storageWebAuthnDeleteRunEOptsFromFlags(flags *pflag.FlagSet, args []string) (all, byKID bool, description, kid, user string, err error) {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
user = args[0]
|
user = args[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ func newStorageUserCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newStorageUserIdentifiersCmd(ctx),
|
newStorageUserIdentifiersCmd(ctx),
|
||||||
newStorageUserTOTPCmd(ctx),
|
newStorageUserTOTPCmd(ctx),
|
||||||
newStorageUserWebauthnCmd(ctx),
|
newStorageUserWebAuthnCmd(ctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -221,34 +221,34 @@ func newStorageUserIdentifiersAddCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "webauthn",
|
Use: "webauthn",
|
||||||
Short: cmdAutheliaStorageUserWebauthnShort,
|
Short: cmdAutheliaStorageUserWebAuthnShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnLong,
|
Long: cmdAutheliaStorageUserWebAuthnLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnExample,
|
Example: cmdAutheliaStorageUserWebAuthnExample,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newStorageUserWebauthnListCmd(ctx),
|
newStorageUserWebAuthnListCmd(ctx),
|
||||||
newStorageUserWebauthnDeleteCmd(ctx),
|
newStorageUserWebAuthnDeleteCmd(ctx),
|
||||||
newStorageUserWebauthnExportCmd(ctx),
|
newStorageUserWebAuthnExportCmd(ctx),
|
||||||
newStorageUserWebauthnImportCmd(ctx),
|
newStorageUserWebAuthnImportCmd(ctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: cmdUseImportFileName,
|
Use: cmdUseImportFileName,
|
||||||
Short: cmdAutheliaStorageUserWebauthnImportShort,
|
Short: cmdAutheliaStorageUserWebAuthnImportShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnImportLong,
|
Long: cmdAutheliaStorageUserWebAuthnImportLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnImportExample,
|
Example: cmdAutheliaStorageUserWebAuthnImportExample,
|
||||||
RunE: ctx.StorageUserWebauthnImportRunE,
|
RunE: ctx.StorageUserWebAuthnImportRunE,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
@ -257,13 +257,13 @@ func newStorageUserWebauthnImportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: cmdUseExport,
|
Use: cmdUseExport,
|
||||||
Short: cmdAutheliaStorageUserWebauthnExportShort,
|
Short: cmdAutheliaStorageUserWebAuthnExportShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnExportLong,
|
Long: cmdAutheliaStorageUserWebAuthnExportLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnExportExample,
|
Example: cmdAutheliaStorageUserWebAuthnExportExample,
|
||||||
RunE: ctx.StorageUserWebauthnExportRunE,
|
RunE: ctx.StorageUserWebAuthnExportRunE,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
@ -274,13 +274,13 @@ func newStorageUserWebauthnExportCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "list [username]",
|
Use: "list [username]",
|
||||||
Short: cmdAutheliaStorageUserWebauthnListShort,
|
Short: cmdAutheliaStorageUserWebAuthnListShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnListLong,
|
Long: cmdAutheliaStorageUserWebAuthnListLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnListExample,
|
Example: cmdAutheliaStorageUserWebAuthnListExample,
|
||||||
RunE: ctx.StorageUserWebauthnListRunE,
|
RunE: ctx.StorageUserWebAuthnListRunE,
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
@ -289,13 +289,13 @@ func newStorageUserWebauthnListCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorageUserWebauthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
func newStorageUserWebAuthnDeleteCmd(ctx *CmdCtx) (cmd *cobra.Command) {
|
||||||
cmd = &cobra.Command{
|
cmd = &cobra.Command{
|
||||||
Use: "delete [username]",
|
Use: "delete [username]",
|
||||||
Short: cmdAutheliaStorageUserWebauthnDeleteShort,
|
Short: cmdAutheliaStorageUserWebAuthnDeleteShort,
|
||||||
Long: cmdAutheliaStorageUserWebauthnDeleteLong,
|
Long: cmdAutheliaStorageUserWebAuthnDeleteLong,
|
||||||
Example: cmdAutheliaStorageUserWebauthnDeleteExample,
|
Example: cmdAutheliaStorageUserWebAuthnDeleteExample,
|
||||||
RunE: ctx.StorageUserWebauthnDeleteRunE,
|
RunE: ctx.StorageUserWebAuthnDeleteRunE,
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
|
|
@ -415,7 +415,7 @@ func (ctx *CmdCtx) StorageSchemaInfoRunE(_ *cobra.Command, _ []string) (err erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *CmdCtx) StorageUserWebauthnExportRunE(cmd *cobra.Command, args []string) (err error) {
|
func (ctx *CmdCtx) StorageUserWebAuthnExportRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = ctx.providers.StorageProvider.Close()
|
_ = ctx.providers.StorageProvider.Close()
|
||||||
}()
|
}()
|
||||||
|
@ -443,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'",
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -15,8 +15,8 @@ type UserInfo struct {
|
||||||
// True if a TOTP device has been registered.
|
// True if a TOTP device has been registered.
|
||||||
HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"`
|
HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"`
|
||||||
|
|
||||||
// True if a Webauthn device has been registered.
|
// True if a WebAuthn device has been registered.
|
||||||
HasWebauthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
|
HasWebAuthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
|
||||||
|
|
||||||
// True if a duo device has been configured as the preferred.
|
// True if a duo device has been configured as the preferred.
|
||||||
HasDuo bool `db:"has_duo" json:"has_duo" valid:"required"`
|
HasDuo bool `db:"has_duo" json:"has_duo" valid:"required"`
|
||||||
|
@ -31,7 +31,7 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string, fallback strin
|
||||||
|
|
||||||
before := i.Method
|
before := i.Method
|
||||||
|
|
||||||
totp, webauthn, duo := utils.IsStringInSlice(SecondFactorMethodTOTP, methods), utils.IsStringInSlice(SecondFactorMethodWebauthn, methods), utils.IsStringInSlice(SecondFactorMethodDuo, methods)
|
totp, webauthn, duo := utils.IsStringInSlice(SecondFactorMethodTOTP, methods), utils.IsStringInSlice(SecondFactorMethodWebAuthn, methods), utils.IsStringInSlice(SecondFactorMethodDuo, methods)
|
||||||
|
|
||||||
if i.Method == "" && utils.IsStringInSlice(fallback, methods) {
|
if i.Method == "" && utils.IsStringInSlice(fallback, methods) {
|
||||||
i.Method = fallback
|
i.Method = fallback
|
||||||
|
@ -50,8 +50,8 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
|
||||||
switch {
|
switch {
|
||||||
case i.HasTOTP && totp:
|
case i.HasTOTP && totp:
|
||||||
i.Method = SecondFactorMethodTOTP
|
i.Method = SecondFactorMethodTOTP
|
||||||
case i.HasWebauthn && webauthn:
|
case i.HasWebAuthn && webauthn:
|
||||||
i.Method = SecondFactorMethodWebauthn
|
i.Method = SecondFactorMethodWebAuthn
|
||||||
case i.HasDuo && duo:
|
case i.HasDuo && duo:
|
||||||
i.Method = SecondFactorMethodDuo
|
i.Method = SecondFactorMethodDuo
|
||||||
case fallback != "" && utils.IsStringInSlice(fallback, methods):
|
case fallback != "" && utils.IsStringInSlice(fallback, methods):
|
||||||
|
@ -59,7 +59,7 @@ func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallbac
|
||||||
case totp:
|
case totp:
|
||||||
i.Method = SecondFactorMethodTOTP
|
i.Method = SecondFactorMethodTOTP
|
||||||
case webauthn:
|
case webauthn:
|
||||||
i.Method = SecondFactorMethodWebauthn
|
i.Method = SecondFactorMethodWebAuthn
|
||||||
case duo:
|
case duo:
|
||||||
i.Method = SecondFactorMethodDuo
|
i.Method = SecondFactorMethodDuo
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
|
|
||||||
has := ""
|
has := ""
|
||||||
|
|
||||||
if have.HasTOTP || have.HasDuo || have.HasWebauthn {
|
if have.HasTOTP || have.HasDuo || have.HasWebAuthn {
|
||||||
has += " has"
|
has += " has"
|
||||||
|
|
||||||
if have.HasTOTP {
|
if have.HasTOTP {
|
||||||
|
@ -31,8 +31,8 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
has += " " + SecondFactorMethodDuo
|
has += " " + SecondFactorMethodDuo
|
||||||
}
|
}
|
||||||
|
|
||||||
if have.HasWebauthn {
|
if have.HasWebAuthn {
|
||||||
has += " " + SecondFactorMethodWebauthn
|
has += " " + SecondFactorMethodWebAuthn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,60 +62,60 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP},
|
methods: []string{SecondFactorMethodTOTP},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP},
|
methods: []string{SecondFactorMethodTOTP},
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -125,15 +125,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn},
|
methods: []string{SecondFactorMethodWebAuthn},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -141,31 +141,31 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: false,
|
changed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -173,15 +173,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -189,13 +189,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
|
@ -205,13 +205,13 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebAuthn: true,
|
||||||
},
|
},
|
||||||
methods: nil,
|
methods: nil,
|
||||||
changed: false,
|
changed: false,
|
||||||
|
@ -221,15 +221,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
fallback: SecondFactorMethodDuo,
|
fallback: SecondFactorMethodDuo,
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
|
@ -238,15 +238,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebAuthn},
|
||||||
fallback: SecondFactorMethodDuo,
|
fallback: SecondFactorMethodDuo,
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
|
@ -255,15 +255,15 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -271,30 +271,30 @@ func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodWebAuthn, SecondFactorMethodDuo},
|
||||||
fallback: SecondFactorMethodWebauthn,
|
fallback: SecondFactorMethodWebAuthn,
|
||||||
changed: true,
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebAuthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebAuthn: false,
|
||||||
},
|
},
|
||||||
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo},
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo},
|
||||||
fallback: SecondFactorMethodDuo,
|
fallback: SecondFactorMethodDuo,
|
||||||
|
|
|
@ -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,13 +266,15 @@ 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 {
|
||||||
|
if aaguid, err = uuid.Parse(*o.AAGUID); err != nil {
|
||||||
return err
|
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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 = ?;`
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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*
|
||||||
|
|
107
web/package.json
107
web/package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6105
web/pnpm-lock.yaml
6105
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,34 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
import NotificationBar from "@components/NotificationBar";
|
import NotificationBar from "@components/NotificationBar";
|
||||||
|
import NotificationsContext from "@hooks/NotificationsContext";
|
||||||
|
import { Notification } from "@models/Notifications";
|
||||||
|
|
||||||
|
const testNotification: Notification = {
|
||||||
|
message: "Test notification",
|
||||||
|
level: "success",
|
||||||
|
timeout: 3,
|
||||||
|
};
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
render(<NotificationBar onClose={() => {}} />);
|
render(<NotificationBar onClose={() => {}} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("displays notification message and level correctly", async () => {
|
||||||
|
render(
|
||||||
|
<NotificationsContext.Provider value={{ notification: testNotification, setNotification: () => {} }}>
|
||||||
|
<NotificationBar onClose={() => {}} />
|
||||||
|
</NotificationsContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const alert = await screen.getByRole("alert");
|
||||||
|
const message = await screen.findByText(testNotification.message);
|
||||||
|
|
||||||
|
expect(alert).toHaveClass(
|
||||||
|
`MuiAlert-filled${testNotification.level.charAt(0).toUpperCase() + testNotification.level.substring(1)}`,
|
||||||
|
{ exact: false },
|
||||||
|
);
|
||||||
|
expect(message).toHaveTextContent(testNotification.message);
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
|
||||||
|
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
withTranslation: () => (Component: any) => {
|
||||||
|
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
|
||||||
|
return Component;
|
||||||
|
},
|
||||||
|
Trans: ({ children }: any) => children,
|
||||||
|
useTranslation: () => {
|
||||||
|
return {
|
||||||
|
t: (str) => str,
|
||||||
|
i18n: {
|
||||||
|
changeLanguage: () => new Promise(() => {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||||
|
|
||||||
|
global.localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders privacy policy and accepts when Accept button is clicked", () => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||||
|
document.body.setAttribute("data-privacypolicyaccept", "true");
|
||||||
|
|
||||||
|
const { container } = render(<PrivacyPolicyDrawer />);
|
||||||
|
fireEvent.click(screen.getByText("Accept"));
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when privacy policy is disabled", () => {
|
||||||
|
render(<PrivacyPolicyDrawer />);
|
||||||
|
expect(screen.queryByText("Privacy Policy")).toBeNull();
|
||||||
|
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
|
||||||
|
expect(screen.queryByText("Accept")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when acceptance is not required", () => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||||
|
|
||||||
|
render(<PrivacyPolicyDrawer />);
|
||||||
|
expect(screen.queryByText("Privacy Policy")).toBeNull();
|
||||||
|
expect(screen.queryByText("You must view and accept the Privacy Policy before using")).toBeNull();
|
||||||
|
expect(screen.queryByText("Accept")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when already accepted", () => {
|
||||||
|
global.localStorage.setItem("privacy-policy-accepted", "true");
|
||||||
|
|
||||||
|
const { container } = render(<PrivacyPolicyDrawer />);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
|
||||||
|
import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
|
||||||
|
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
withTranslation: () => (Component: any) => {
|
||||||
|
Component.defaultProps = { ...Component.defaultProps, t: (children: any) => children };
|
||||||
|
return Component;
|
||||||
|
},
|
||||||
|
Trans: ({ children }: any) => children,
|
||||||
|
useTranslation: () => {
|
||||||
|
return {
|
||||||
|
t: (str) => str,
|
||||||
|
i18n: {
|
||||||
|
changeLanguage: () => new Promise(() => {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("renders a link to the privacy policy with the correct text", () => {
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "http://example.com/privacy-policy");
|
||||||
|
|
||||||
|
const { getByRole } = render(<PrivacyPolicyLink />);
|
||||||
|
const link = getByRole("link");
|
||||||
|
expect(link).toHaveAttribute("href", "http://example.com/privacy-policy");
|
||||||
|
expect(link).toHaveTextContent("Privacy Policy");
|
||||||
|
});
|
|
@ -1,9 +1,37 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { act, render } from "@testing-library/react";
|
||||||
|
|
||||||
import TimerIcon from "@components/TimerIcon";
|
import TimerIcon from "@components/TimerIcon";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers().setSystemTime(new Date(2023, 1, 1, 8));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
render(<TimerIcon width={32} height={32} period={30} />);
|
render(<TimerIcon width={32} height={32} period={30} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders a timer icon with updating progress for a given period", async () => {
|
||||||
|
const { container } = render(<TimerIcon width={32} height={32} period={30} />);
|
||||||
|
const initialProgress =
|
||||||
|
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
|
||||||
|
"stroke-dasharray",
|
||||||
|
);
|
||||||
|
expect(initialProgress).toBe("0 31.6");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedProgress =
|
||||||
|
container.firstElementChild!.firstElementChild!.nextElementSibling!.nextElementSibling!.getAttribute(
|
||||||
|
"stroke-dasharray",
|
||||||
|
);
|
||||||
|
expect(updatedProgress).toBe("3.16 31.6");
|
||||||
|
expect(Number(updatedProgress!.split(/\s(.+)/)[0])).toBeGreaterThan(Number(initialProgress!.split(/\s(.+)/)[0]));
|
||||||
|
});
|
||||||
|
|
|
@ -2,12 +2,41 @@ import React from "react";
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
|
|
||||||
import TypographyWithTooltip from "@components/TypographyWithTootip";
|
import TypographyWithTooltip, { Props } from "@components/TypographyWithTooltip";
|
||||||
|
|
||||||
|
const defaultProps: Props = {
|
||||||
|
variant: "h5",
|
||||||
|
value: "Example",
|
||||||
|
};
|
||||||
|
|
||||||
it("renders without crashing", () => {
|
it("renders without crashing", () => {
|
||||||
render(<TypographyWithTooltip value={"Example"} variant={"h5"} />);
|
render(<TypographyWithTooltip {...defaultProps} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders with tooltip without crashing", () => {
|
it("renders with tooltip without crashing", () => {
|
||||||
render(<TypographyWithTooltip value={"Example"} tooltip={"A tooltip"} variant={"h5"} />);
|
const props: Props = {
|
||||||
|
...defaultProps,
|
||||||
|
tooltip: "A tooltip",
|
||||||
|
};
|
||||||
|
render(<TypographyWithTooltip {...props} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the text correctly", () => {
|
||||||
|
const props: Props = {
|
||||||
|
...defaultProps,
|
||||||
|
value: "Test text",
|
||||||
|
};
|
||||||
|
const { getByText } = render(<TypographyWithTooltip {...props} />);
|
||||||
|
const element = getByText(props.value!);
|
||||||
|
expect(element).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the tooltip correctly", () => {
|
||||||
|
const props: Props = {
|
||||||
|
...defaultProps,
|
||||||
|
tooltip: "Test tooltip",
|
||||||
|
};
|
||||||
|
const { getByText } = render(<TypographyWithTooltip {...props} />);
|
||||||
|
const element = getByText(props.value!);
|
||||||
|
expect(element).toHaveAttribute("aria-label", props.tooltip);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
|
|
||||||
document.body.setAttribute("data-basepath", "");
|
|
||||||
document.body.setAttribute("data-duoselfenrollment", "true");
|
|
||||||
document.body.setAttribute("data-rememberme", "true");
|
|
||||||
document.body.setAttribute("data-resetpassword", "true");
|
|
||||||
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
|
||||||
document.body.setAttribute("data-privacypolicyurl", "");
|
|
||||||
document.body.setAttribute("data-privacypolicyaccept", "false");
|
|
||||||
document.body.setAttribute("data-theme", "light");
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import matchers, { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Vi {
|
||||||
|
interface JestAssertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend(matchers);
|
||||||
|
|
||||||
|
const localStorageMock = (function () {
|
||||||
|
let store = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getItem(key) {
|
||||||
|
return store[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem(key, value) {
|
||||||
|
store[key] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
store = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(key) {
|
||||||
|
delete store[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
getAll() {
|
||||||
|
return store;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||||
|
|
||||||
|
document.body.setAttribute("data-basepath", "");
|
||||||
|
document.body.setAttribute("data-duoselfenrollment", "true");
|
||||||
|
document.body.setAttribute("data-rememberme", "true");
|
||||||
|
document.body.setAttribute("data-resetpassword", "true");
|
||||||
|
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||||
|
document.body.setAttribute("data-theme", "light");
|
|
@ -21,7 +21,7 @@
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
"types": ["@types/jest", "vite/client", "vite-plugin-svgr/client"],
|
"types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|
|
@ -5,18 +5,17 @@ import istanbul from "vite-plugin-istanbul";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const isCoverage = process.env.VITE_COVERAGE === "true";
|
const isCoverage = process.env.VITE_COVERAGE === "true";
|
||||||
const sourcemap = isCoverage ? "inline" : undefined;
|
const sourcemap = isCoverage ? "inline" : undefined;
|
||||||
|
|
||||||
const istanbulPlugin = isCoverage
|
const istanbulPlugin = isCoverage
|
||||||
? istanbul({
|
? istanbul({
|
||||||
include: "src/*",
|
checkProd: false,
|
||||||
exclude: ["node_modules"],
|
exclude: ["node_modules"],
|
||||||
extension: [".js", ".jsx", ".ts", ".tsx"],
|
extension: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
checkProd: false,
|
|
||||||
forceBuildInstrument: true,
|
forceBuildInstrument: true,
|
||||||
|
include: "src/*",
|
||||||
requireEnv: true,
|
requireEnv: true,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -24,14 +23,11 @@ export default defineConfig(({ mode }) => {
|
||||||
return {
|
return {
|
||||||
base: "./",
|
base: "./",
|
||||||
build: {
|
build: {
|
||||||
sourcemap,
|
|
||||||
outDir: "../internal/server/public_html",
|
|
||||||
emptyOutDir: true,
|
|
||||||
assetsDir: "static",
|
assetsDir: "static",
|
||||||
|
emptyOutDir: true,
|
||||||
|
outDir: "../internal/server/public_html",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: `static/js/[name].[hash].js`,
|
|
||||||
chunkFileNames: `static/js/[name].[hash].js`,
|
|
||||||
assetFileNames: ({ name }) => {
|
assetFileNames: ({ name }) => {
|
||||||
if (name && name.endsWith(".css")) {
|
if (name && name.endsWith(".css")) {
|
||||||
return "static/css/[name].[hash].[ext]";
|
return "static/css/[name].[hash].[ext]";
|
||||||
|
@ -39,12 +35,26 @@ export default defineConfig(({ mode }) => {
|
||||||
|
|
||||||
return "static/media/[name].[hash].[ext]";
|
return "static/media/[name].[hash].[ext]";
|
||||||
},
|
},
|
||||||
|
chunkFileNames: `static/js/[name].[hash].js`,
|
||||||
|
entryFileNames: `static/js/[name].[hash].js`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
sourcemap,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
|
||||||
open: false,
|
open: false,
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
coverage: {
|
||||||
|
provider: "istanbul",
|
||||||
|
},
|
||||||
|
environment: "happy-dom",
|
||||||
|
globals: true,
|
||||||
|
onConsoleLog(log) {
|
||||||
|
if (log.includes('No routes matched location "blank"')) return false;
|
||||||
|
},
|
||||||
|
setupFiles: ["src/setupTests.ts"],
|
||||||
},
|
},
|
||||||
plugins: [eslintPlugin({ cache: false }), istanbulPlugin, react(), svgr(), tsconfigPaths()],
|
plugins: [eslintPlugin({ cache: false }), istanbulPlugin, react(), svgr(), tsconfigPaths()],
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue