feat: webauthn (#2707)

This implements Webauthn. Old devices can be used to authenticate via the appid compatibility layer which should be automatic. New devices will be registered via Webauthn, and devices which do not support FIDO2 will no longer be able to be registered. At this time it does not fully support multiple devices (backend does, frontend doesn't allow registration of additional devices). Does not support passwordless.
This commit is contained in:
James Elliott 2022-03-03 22:20:43 +11:00 committed by GitHub
parent 3c0d9b3b57
commit 8f05846e21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
118 changed files with 3780 additions and 1649 deletions

View File

@ -52,9 +52,9 @@ Here is what Authelia's portal looks like:
This is a list of the key features of Authelia: This is a list of the key features of Authelia:
* Several second factor methods: * Several second factor methods:
* **[Security Key (U2F)](https://www.authelia.com/docs/features/2fa/security-key)** with [Yubikey]. * **[Security Keys](https://www.authelia.com/docs/features/2fa/security-key)** that support [FIDO2] [Webauthn] with devices like a [YubiKey].
* **[Time-based One-Time password](https://www.authelia.com/docs/features/2fa/one-time-password)** * **[Time-based One-Time password](https://www.authelia.com/docs/features/2fa/one-time-password)**
with [Google Authenticator]. with compatible authenticator applications.
* **[Mobile Push Notifications](https://www.authelia.com/docs/features/2fa/push-notifications)** * **[Mobile Push Notifications](https://www.authelia.com/docs/features/2fa/push-notifications)**
with [Duo](https://duo.com/). with [Duo](https://duo.com/).
* Password reset with identity verification using email confirmation. * Password reset with identity verification using email confirmation.
@ -345,10 +345,10 @@ for providing us with free licenses to their great tools.
[Apache 2.0]: https://www.apache.org/licenses/LICENSE-2.0 [Apache 2.0]: https://www.apache.org/licenses/LICENSE-2.0
[TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm [TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
[Security Key]: https://www.yubico.com/about/background/fido/ [FIDO2]: https://www.yubico.com/authentication-standards/fido2/
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/ [YubiKey]: https://www.yubico.com/products/yubikey-5-overview/
[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
[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
[config.template.yml]: ./config.template.yml [config.template.yml]: ./config.template.yml
[nginx]: https://www.nginx.com/ [nginx]: https://www.nginx.com/
[Traefik]: https://traefik.io/ [Traefik]: https://traefik.io/

View File

@ -23,7 +23,7 @@ tags:
- name: User Information - name: User Information
description: User configuration endpoints description: User configuration endpoints
- name: Second Factor - name: Second Factor
description: TOTP, U2F and Duo endpoints description: TOTP, Webauthn and Duo endpoints
paths: paths:
/api/configuration: /api/configuration:
get: get:
@ -430,35 +430,34 @@ paths:
$ref: '#/components/schemas/middlewares.ErrorResponse' $ref: '#/components/schemas/middlewares.ErrorResponse'
security: security:
- authelia_auth: [] - authelia_auth: []
/api/secondfactor/u2f/sign_request: /api/secondfactor/webauthn/assertion:
post: get:
tags: tags:
- Second Factor - Second Factor
summary: Second Factor Authentication - U2F (Request) summary: Second Factor Authentication - Webauthn (Request)
description: This endpoint starts the second factor authentication process with the U2F key. description: This endpoint starts the second factor authentication process with the FIDO2 Webauthn credential.
responses: responses:
"200": "200":
description: Successful Operation description: Successful Operation
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/u2f.WebSignRequest' $ref: '#/components/schemas/webauthn.PublicKeyCredentialRequestOptions'
"401": "401":
description: Unauthorized description: Unauthorized
security: security:
- authelia_auth: [] - authelia_auth: []
/api/secondfactor/u2f/sign:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: Second Factor Authentication - U2F summary: Second Factor Authentication - Webauthn
description: "This endpoint completes second factor authentication with a U2F key." description: This endpoint completes the second factor authentication process with the FIDO2 Webauthn credential.
requestBody: requestBody:
required: true required: true
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/handlers.signU2FRequestBody" $ref: "#/components/schemas/webauthn.CredentialAssertionResponse"
responses: responses:
"200": "200":
description: Successful Operation description: Successful Operation
@ -470,16 +469,17 @@ paths:
description: Unauthorized description: Unauthorized
security: security:
- authelia_auth: [] - authelia_auth: []
/api/secondfactor/u2f/identity/start: /api/secondfactor/webauthn/identity/start:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: Identity Verification U2F Token Creation summary: Identity Verification Webauthn Credential Creation
description: > description: >
This endpoint performs identity verification to begin the U2F device registration process. This endpoint performs identity verification to begin the FIDO2 Webauthn credential attestation process
(registration).
The session generated from this endpoint must be utilised for the subsequent steps in the The session generated from this endpoint must be utilised for the subsequent steps in the
`/api/secondfactor/u2f/identity/finish` and `/api/secondfactor/u2f/register` endpoints. `/api/secondfactor/webauthn/identity/finish` and `/api/secondfactor/webauthn/attestation` endpoints.
responses: responses:
"200": "200":
description: Successful Operation description: Successful Operation
@ -489,17 +489,17 @@ paths:
$ref: '#/components/schemas/middlewares.OkResponse' $ref: '#/components/schemas/middlewares.OkResponse'
security: security:
- authelia_auth: [] - authelia_auth: []
/api/secondfactor/u2f/identity/finish: /api/secondfactor/webauthn/identity/finish:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: Identity Verification U2F Token Validation summary: Identity Verification FIDO2 Webauthn Credential Validation
description: > description: >
This endpoint performs identity and token verification, upon success generates a U2F device registration This endpoint performs identity and token verification, upon success generates a FIDO2 Webauthn device
challenge. attestation challenge (registration).
The session cookie generated from the `/api/secondfactor/u2f/identity/start` endpoint must be utilised for the The session cookie generated from the `/api/secondfactor/webauthn/identity/start` endpoint must be utilised
subsequent steps here and in the `/api/secondfactor/u2f/register` endpoint. for the subsequent steps here and in the `/api/secondfactor/webauthn/attestation` endpoint.
requestBody: requestBody:
required: true required: true
content: content:
@ -512,21 +512,21 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/u2f.WebRegisterRequest' $ref: '#/components/schemas/webauthn.PublicKeyCredentialCreationOptions'
security: security:
- authelia_auth: [] - authelia_auth: []
/api/secondfactor/u2f/register: /api/secondfactor/webauthn/attestation:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: U2F Device Registration summary: Webauthn Credential Attestation
description: This endpoint performs U2F device registration. description: This endpoint performs Webauthn credential attestation (registration).
requestBody: requestBody:
required: true required: true
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/u2f.RegisterResponse' $ref: '#/components/schemas/webauthn.CredentialAttestationResponse'
responses: responses:
"200": "200":
description: Successful Operation description: Successful Operation
@ -653,12 +653,13 @@ components:
properties: properties:
available_methods: available_methods:
type: array type: array
description: List of available 2FA methods. If no methods exist 2FA is disabled.
items: items:
type: string enum:
example: [totp, u2f, mobile_push] - "totp"
second_factor_enabled: - "webauthn"
type: boolean - "mobile_push"
description: If second factor is enabled. example: [totp, webauthn, mobile_push]
handlers.DuoDeviceBody: handlers.DuoDeviceBody:
required: required:
- device - device
@ -781,24 +782,6 @@ components:
targetURL: targetURL:
type: string type: string
example: https://secure.example.com example: https://secure.example.com
handlers.signU2FRequestBody:
type: object
properties:
targetURL:
type: string
example: https://secure.example.com
signResponse:
type: object
properties:
clientData:
type: string
example: 6prxyWqSsR6MXFchtQRzwZVTedWq7Zdc6XreLt6xRDXKeqJN7vzKAfYcKwRD3AT57bP4YFL4hbxat4LUysBNss
keyHandle:
type: string
example: pWgBrwr9meS5vArdffPtD4Px6AqZS7MfGEf776Rz438ujwHjeXwQEZuK53sRQ4wjeAgRCW4wX9VRj8dyKjc273
signatureData:
type: string
example: p3Pe26B6T2E7EEEc59P4p869qwxy8cQAU2ttyGtGrQHb4XL2ZxCpWrawsSHNSTRZQd7jEW59Y3Ku9vSNRzj7Ly
handlers.StateResponse: handlers.StateResponse:
type: object type: object
properties: properties:
@ -846,9 +829,12 @@ components:
example: John Doe example: John Doe
method: method:
type: string type: string
enum: [totp, u2f, mobile_push] enum:
- "totp"
- "webauthn"
- "mobile_push"
example: totp example: totp
has_u2f: has_webauthn:
type: boolean type: boolean
example: false example: false
has_totp: has_totp:
@ -883,7 +869,10 @@ components:
properties: properties:
method: method:
type: string type: string
enum: [totp, u2f, mobile_push] enum:
- "totp"
- "webauthn"
- "mobile_push"
example: totp example: totp
middlewares.ErrorResponse: middlewares.ErrorResponse:
type: object type: object
@ -910,16 +899,70 @@ components:
example: OK example: OK
data: data:
type: object type: object
u2f.RegisterResponse: webauthn.PublicKeyCredential:
type: object type: object
properties: properties:
version: rawId:
type: string type: string
registrationData: format: byte
id:
type: string type: string
clientData: type:
type: string type: string
u2f.WebRegisterRequest: webauthn.AuthenticatorResponse:
type: object
properties:
clientDataJSON:
type: string
format: byte
webauthn.CredentialAttestationResponse:
allOf:
- $ref: '#/components/schemas/webauthn.PublicKeyCredential'
- type: object
properties:
clientExtensionResults:
type: object
properties:
appidExclude:
type: boolean
response:
allOf:
- $ref: '#/components/schemas/webauthn.AuthenticatorResponse'
- type: object
properties:
attestationObject:
type: string
format: byte
webauthn.CredentialAssertionResponse:
allOf:
- $ref: '#/components/schemas/webauthn.PublicKeyCredential'
- type: object
properties:
response:
allOf:
- $ref: '#/components/schemas/webauthn.AuthenticatorResponse'
- type: object
required: [authenticatorData, clientDataJSON, signature]
properties:
authenticatorData:
type: string
format: byte
clientDataJSON:
type: string
format: byte
clientExtensionResults:
type: object
properties:
appid:
type: boolean
example: false
signature:
type: string
format: byte
userHandle:
type: string
format: byte
webauthn.PublicKeyCredentialCreationOptions:
type: object type: object
properties: properties:
status: status:
@ -928,35 +971,50 @@ components:
data: data:
type: object type: object
properties: properties:
appId: publicKey:
type: string allOf:
example: https://auth.example.com - $ref: '#/components/schemas/webauthn.AttestationType'
registerRequests: - $ref: '#/components/schemas/webauthn.AuthenticatorSelectionCriteria'
type: array - $ref: '#/components/schemas/webauthn.CredentialUserEntity'
items: - $ref: '#/components/schemas/webauthn.CredentialRPEntity'
type: object - type: object
required:
- "challenge"
- "pubKeyCredParams"
properties: properties:
version:
type: string
example: U2F_V2
challenge: challenge:
type: string type: string
example: XGYKUzSmTpM1KxxpekArviW0w0OU2pwwRAocgn8TkVQ format: byte
registeredKeys: pubKeyCredParams:
type: array type: array
items: items:
type: object type: object
required:
- "alg"
- "type"
properties: properties:
appId: alg:
type: integer
type:
type: string
example: public-key
enum:
- "public-key"
timeout:
type: integer
example: 60000
excludeCredentials:
type: array
items:
allOf:
- $ref: '#/components/schemas/webauthn.CredentialDescriptor'
extensions:
type: object
properties:
appidExclude:
type: string type: string
example: https://auth.example.com example: https://auth.example.com
version: webauthn.PublicKeyCredentialRequestOptions:
type: string
example: U2F_V2
keyHandle:
type: string
example: pWgBrwr9meS5vArdffPtD4Px6AqZS7MfGEf776Rz438ujwHjeXwQEZuK53sRQ4wjeAgRCW4wX9VRj8dyKjc273
u2f.WebSignRequest:
type: object type: object
properties: properties:
status: status:
@ -965,26 +1023,172 @@ components:
data: data:
type: object type: object
properties: properties:
appId: publicKey:
type: string allOf:
example: https://auth.example.com - $ref: '#/components/schemas/webauthn.UserVerification'
- type: object
required:
- "challenge"
properties:
challenge: challenge:
type: string type: string
example: XGYKUzSmTpM1KxxpekArviW0w0OU2pwwRAocgn8TkVQ timeout:
registeredKeys: type: integer
example: 60000
rpId:
type: string
example: auth.example.com
allowCredentials:
type: array type: array
items: items:
allOf:
- $ref: '#/components/schemas/webauthn.CredentialDescriptor'
extensions:
type: object type: object
properties: properties:
appId: appid:
type: string type: string
example: https://auth.example.com example: https://auth.example.com
version: webauthn.Transports:
type: object
properties:
transports:
type: array
items:
type: string type: string
example: U2F_V2 example:
keyHandle: - "usb"
- "nfc"
enum:
- "usb"
- "nfc"
- "ble"
- "internal"
webauthn.UserVerification:
type: object
properties:
userVerification:
type: string type: string
example: pWgBrwr9meS5vArdffPtD4Px6AqZS7MfGEf776Rz438ujwHjeXwQEZuK53sRQ4wjeAgRCW4wX9VRj8dyKjc273 example: preferred
enum:
- "required"
- "preferred"
- "discouraged"
webauthn.AttestationType:
type: object
properties:
attestation:
type: string
example: direct
enum:
- "none"
- "indirect"
- "direct"
webauthn.AuthenticatorSelectionCriteria:
type: object
properties:
authenticatorSelection:
type: object
properties:
authenticatorAttachment:
type: string
example: cross-platform
enum:
- "platform"
- "cross-platform"
residentKey:
type: string
example: discouraged
enum:
- "discouraged"
- "preferred"
- "required"
requireResidentKey:
type: boolean
webauthn.CredentialDescriptor:
allOf:
- $ref: '#/components/schemas/webauthn.Transports'
- type: object
required:
- "id"
- "type"
properties:
id:
type: string
format: byte
type:
type: string
example: public-key
enum:
- "public-key"
webauthn.CredentialEntity:
type: object
required:
- "id"
- "name"
properties:
id:
type: string
name:
type: string
icon:
type: string
webauthn.CredentialRPEntity:
type: object
required:
- "rp"
properties:
rp:
allOf:
- $ref: '#/components/schemas/webauthn.CredentialEntity'
webauthn.CredentialUserEntity:
type: object
required:
- "user"
properties:
user:
allOf:
- $ref: '#/components/schemas/webauthn.CredentialEntity'
- type: object
required:
- "displayName"
properties:
displayName:
type: string
webauthn.AuthenticationExtensionsClientOutputs:
type: object
properties:
clientExtensionResults:
type: object
properties:
appid:
type: boolean
example: true
appidExclude:
type: boolean
example: false
uvm:
type: array
items:
type: string
format: byte
credProps:
type: object
properties:
rk:
type: boolean
example: false
largeBlob:
type: object
properties:
supported:
type: boolean
example: false
blob:
type: string
written:
type: boolean
example: false
securitySchemes: securitySchemes:
authelia_auth: authelia_auth:
type: apiKey type: apiKey

View File

@ -98,6 +98,9 @@ log:
## ##
## Parameters used for TOTP generation. ## Parameters used for TOTP generation.
totp: totp:
## Disable TOTP.
disable: false
## The issuer name displayed in the Authenticator application of your choice. ## The issuer name displayed in the Authenticator application of your choice.
issuer: authelia.com issuer: authelia.com
@ -121,6 +124,28 @@ totp:
skew: 1 skew: 1
## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation. ## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
##
## WebAuthn Configuration
##
## Parameters used for WebAuthn.
webauthn:
## Disable Webauthn.
disable: false
## Adjust the interaction timeout for Webauthn dialogues.
timeout: 60s
## The display name the browser should show the user for when using Webauthn to login/register.
display_name: Authelia
## Conveyance preference controls if we collect the attestation statement including the AAGUID from the device.
## Options are none, indirect, direct.
attestation_conveyance_preference: indirect
## User verification controls if the user must make a gesture or action to confirm they are present.
## Options are required, preferred, discouraged.
user_verification: preferred
## ##
## Duo Push API Configuration ## Duo Push API Configuration
## ##
@ -576,7 +601,7 @@ storage:
## ##
## Notification Provider ## Notification Provider
## ##
## Notifications are sent to users when they require a password reset, a U2F registration or a TOTP registration. ## Notifications are sent to users when they require a password reset, a Webauthn registration or a TOTP registration.
## The available providers are: filesystem, smtp. You must use only one of these providers. ## The available providers are: filesystem, smtp. You must use only one of these providers.
notifier: notifier:
## You can disable the notifier startup check by setting this to true. ## You can disable the notifier startup check by setting this to true.

View File

@ -14,6 +14,7 @@ full example of TOTP configuration below, as well as sections describing them.
## Configuration ## Configuration
```yaml ```yaml
totp: totp:
disable: false
issuer: authelia.com issuer: authelia.com
algorithm: sha1 algorithm: sha1
digits: 6 digits: 6
@ -23,6 +24,18 @@ totp:
## Options ## Options
### disable
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
This disables One-Time Password (TOTP) if set to true.
### issuer ### issuer
<div markdown="1"> <div markdown="1">
type: string type: string

View File

@ -18,7 +18,8 @@ This means all Authelia versions between two schema versions use the first schem
For example for version pre1, it is used for all versions between it and the version 1 schema, so 4.0.0 to 4.32.2. In For example for version pre1, it is used for all versions between it and the version 1 schema, so 4.0.0 to 4.32.2. In
this instance if you wanted to downgrade to pre1 you would need to use an Authelia binary with version 4.33.0 or higher. this instance if you wanted to downgrade to pre1 you would need to use an Authelia binary with version 4.33.0 or higher.
|Schema Version|Authelia Version|Notes | | Schema Version | Authelia Version | Notes |
|:------------:|:--------------:|:----------------------------------------------------------:| |:--------------:|:----------------:|:-------------------------------------------------------------------------------------------------:|
|pre1 |4.0.0 |Downgrading to this version requires you use the --pre1 flag| | pre1 | 4.0.0 | Downgrading to this version requires you use the --pre1 flag |
|1 |4.33.0 | | | 1 | 4.33.0 | Initial migration managed version |
| 2 | 4.34.0 | Webauthn - added webauthn_devices table, altered totp_config to include device created/used dates |

View File

@ -0,0 +1,109 @@
---
layout: default
title: Webauthn
parent: Configuration
nav_order: 16
---
The Webauthn section has tunable options for the Webauthn implementation.
## Configuration
```yaml
webauthn:
disable: false
display_name: Authelia
attestation_conveyance_preference: indirect
user_verification: preferred
timeout: 60s
```
## Options
### disable
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
This disables Webauthn if set to true.
### display_name
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: Authelia
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Sets the display name which is sent to the client to be displayed. It's up to individual browsers and potentially
individual operating systems if and how they display this information.
See the [W3C Webauthn Documentation](https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialentity-name) for more information.
### attestation_conveyance_preference
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: indirect
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Sets the conveyance preference. Conveyancing allows collection of attestation statements about the authenticator such as
the AAGUID. The AAGUID indicates the model of the device.
See the [W3C Webauthn Documentation](https://www.w3.org/TR/webauthn-2/#enum-attestation-convey) for more information.
Available Options:
| Value | Description |
|:--------:|:---------------------------------------------------------------------------------------------------------------------------------------------:|
| none | The client will be instructed not to perform conveyancing |
| indirect | The client will be instructed to perform conveyancing but the client can choose how to do this including using a third party anonymization CA |
| direct | The client will be instructed to perform conveyancing with an attestation statement directly signed by the device |
### user_verification
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: preferred
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Sets the user verification preference.
See the [W3C Webauthn Documentation](https://www.w3.org/TR/webauthn-2/#enum-userVerificationRequirement) for more information.
Available Options:
| Value | Description |
|:-----------:|:------------------------------------------------------------------------------------------------------:|
| discouraged | The client will be discouraged from asking for user verification |
| preferred | The client if compliant will ask the user for verification if the device supports it |
| required | The client will ask the user for verification or will fail if the device does not support verification |
### timeout
<div markdown="1">
type: string (duration)
{: .label .label-config .label-purple }
default: 60s
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
This adjusts the requested timeout for a Webauthn interaction. The period of time is in
[duration notation format](index.md#duration-notation-format).
## FAQ
See the [Security Key FAQ](../features/2fa/security-key.md#faq) for the FAQ.

View File

@ -10,8 +10,8 @@ has_children: true
There are multiple supported options for the second factor. There are multiple supported options for the second factor.
* Time-based One-Time passwords with [Google Authenticator] * Time-based One-Time passwords with compatible authenticator applications.
* Security Keys with tokens like [Yubikey]. * Security Keys that support [FIDO2]&nbsp;[Webauthn] with devices like a [YubiKey].
* Push notifications on your mobile using [Duo]. * Push notifications on your mobile using [Duo].
<p align="center"> <p align="center">
@ -20,5 +20,6 @@ There are multiple supported options for the second factor.
[Duo]: https://duo.com/ [Duo]: https://duo.com/
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/ [FIDO2]: https://www.yubico.com/authentication-standards/fido2/
[Google Authenticator]: https://google-authenticator.com/ [Webauthn]: https://www.yubico.com/authentication-standards/webauthn/
[YubiKey]: https://www.yubico.com/products/yubikey-5-overview/

View File

@ -8,35 +8,34 @@ grand_parent: Features
# Security Keys # Security Keys
**Authelia** supports hardware-based second factors leveraging security keys like **Authelia** supports hardware-based second factors leveraging [FIDO2]&nbsp;[Webauthn] compatible security keys like
[YubiKey]'s. [YubiKey]'s.
Security keys are among the most secure second factor. This method is already Security keys are among the most secure second factor. This method is already supported by many major applications and
supported by many major applications and platforms like Google, Facebook, Github, platforms like Google, Facebook, GitHub, some banks, and much more.
some banks, and much more...
<p align="center"> <p align="center">
<img src="../../images/yubikey.jpg" width="150"> <img src="../../images/yubikey.jpg" width="150">
</p> </p>
Normally, the protocol requires your security key to be enrolled on each site before Normally, the protocol requires your security key to be enrolled on each site before being able to authenticate with it.
being able to authenticate with it. Since Authelia provides Single Sign-On, your users Since Authelia provides Single Sign-On, your users will need to enroll their device only once to get access to all your
will need to enroll their device only once to get access to all your applications. applications.
<p align="center"> <p align="center">
<img src="../../images/REGISTER-U2F.png" width="400"> <img src="../../images/REGISTER-U2F.png" width="400">
</p> </p>
After having successfully passed the first factor, select *Security Key* method and After having successfully passed the first factor, select *Security Key* method and click on *Register device* link.
click on *Register device* link. This will send you an email to verify your identity. This will send you an email to verify your identity.
*NOTE: This e-mail has likely been sent to the mailbox at https://mail.example.com:8080/ if you're testing Authelia.* *NOTE: This e-mail has likely been sent to the mailbox at https://mail.example.com:8080/ if you're testing Authelia.*
Confirm your identity by clicking on **Register** and you'll be asked to Confirm your identity by clicking on **Register** and you'll be asked to touch the token of your security key to
touch the token of your security key to complete the enrollment. complete the enrollment.
Upon successful enrollment, you can authenticate using your security key Upon successful enrollment, you can authenticate using your security key by simply touching the token again when
by simply touching the token again when requested: requested:
<p align="center"> <p align="center">
<img src="../../images/2FA-U2F.png" width="400"> <img src="../../images/2FA-U2F.png" width="400">
@ -44,20 +43,32 @@ by simply touching the token again when requested:
Easy, right?! Easy, right?!
## Limitations
Users currently can only enroll a single U2F device in **Authelia**.
Multiple single type device enrollment will be available when [this issue](https://github.com/authelia/authelia/issues/275) has been resolved.
## FAQ ## FAQ
### Can I register multiple FIDO2 Webauthn devices?
At present this is not possible in the frontend. However the backend technically supports it. We plan to add this to the
frontend in the near future. Subscribe to [this issue](https://github.com/authelia/authelia/issues/275) for updates.
### Can I perform a passwordless login?
Not at this time. We will tackle this at a later date.
### Why don't I have access to the *Security Key* option? ### Why don't I have access to the *Security Key* option?
U2F protocol is a new protocol that is only supported by recent browsers The [Webauthn] protocol is a new protocol that is only supported by modern browsers. Please ensure your browser is up to
and might even be enabled on some of them. Please be sure your browser date, supports [Webauthn], and that the feature is not disabled if the option is not available to you in **Authelia**.
supports U2F and that the feature is enabled to make the option
available in **Authelia**.
### Can my FIDO U2F device operate with Authelia?
At the present time there is no plan to support [FIDO U2F] within Authelia. We do implement a backwards compatible appid
extension within **Authelia** however this only works for devices registered before the upgrade to the [FIDO2]&nbsp;[Webauthn]
protocol.
If there was sufficient interest in supporting registration of old U2F / FIDO devices in **Authelia** we would consider
adding support for this after or at the same time of the multi-device enhancements.
[FIDO U2F]: https://www.yubico.com/authentication-standards/fido-u2f/
[FIDO2]: https://www.yubico.com/authentication-standards/fido2/
[Webauthn]: https://www.yubico.com/authentication-standards/webauthn/
[YubiKey]: https://www.yubico.com/products/yubikey-5-overview/ [YubiKey]: https://www.yubico.com/products/yubikey-5-overview/

View File

@ -29,8 +29,8 @@ protect your apps.
Multiple 2-factor methods are available for satisfying every users. Multiple 2-factor methods are available for satisfying every users.
* Time-based One-Time passwords with [Google Authenticator]. * Time-based One-Time passwords with compatible authenticator applications.
* Security Keys with tokens like [Yubikey]. * Security Keys that support [FIDO2]&nbsp;[Webauthn] with devices like a [YubiKey].
* Push notifications on your mobile using [Duo]. * Push notifications on your mobile using [Duo].
**Authelia** is available as Docker images, static binaries and AUR packages **Authelia** is available as Docker images, static binaries and AUR packages
@ -49,5 +49,6 @@ so that you can test it in minutes. Let's begin with the
[Duo]: https://duo.com/ [Duo]: https://duo.com/
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/ [FIDO2]: https://www.yubico.com/authentication-standards/fido2/
[Google Authenticator]: https://google-authenticator.com/ [Webauthn]: https://www.yubico.com/authentication-standards/webauthn/
[YubiKey]: https://www.yubico.com/products/yubikey-5-overview/

View File

@ -89,10 +89,10 @@ that users who have access to the database do not also have access to this key.
The encrypted data in the database is as follows: The encrypted data in the database is as follows:
|Table |Column |Rational | | Table | Column | Rational |
|:-----------------:|:--------:|:----------------------------------------------------------------------------------------------------:| |:-------------------:|:----------:|:------------------------------------------------------------------------------------------------------:|
|totp_configurations|secret |Prevents a [Leaked Database](#leaked-database) or [Bad Actors](#bad-actors) from compromising security| | totp_configurations | secret | Prevents a [Leaked Database](#leaked-database) or [Bad Actors](#bad-actors) from compromising security |
|u2f_devices |public_key|Prevents [Bad Actors](#bad-actors) from compromising security | | webauthn_devices | public_key | Prevents [Bad Actors](#bad-actors) from compromising security |
### Leaked Database ### Leaked Database

9
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/deckarep/golang-set v1.8.0 github.com/deckarep/golang-set v1.8.0
github.com/duo-labs/webauthn v0.0.0-20210727191636-9f1b88ef44cc
github.com/duosecurity/duo_api_golang v0.0.0-20220201180708-96a8851a8448 github.com/duosecurity/duo_api_golang v0.0.0-20220201180708-96a8851a8448
github.com/fasthttp/router v1.4.6 github.com/fasthttp/router v1.4.6
github.com/fasthttp/session/v2 v2.4.7 github.com/fasthttp/session/v2 v2.4.7
@ -30,7 +31,6 @@ require (
github.com/spf13/cobra v1.3.0 github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/tstranex/u2f v1.0.0
github.com/valyala/fasthttp v1.33.0 github.com/valyala/fasthttp v1.33.0
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0
@ -42,17 +42,20 @@ require (
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cloudflare/cfssl v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
github.com/go-redis/redis/v8 v8.11.4 // indirect github.com/go-redis/redis/v8 v8.11.4 // indirect
github.com/gobuffalo/pop/v5 v5.3.3 // indirect github.com/gobuffalo/pop/v5 v5.3.3 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/certificate-transparency-go v1.0.21 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
@ -88,6 +91,7 @@ require (
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect
github.com/tinylib/msgp v1.1.6 // indirect github.com/tinylib/msgp v1.1.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/ysmood/goob v0.3.1 // indirect github.com/ysmood/goob v0.3.1 // indirect
github.com/ysmood/gson v0.6.4 // indirect github.com/ysmood/gson v0.6.4 // indirect
github.com/ysmood/leakless v0.7.0 // indirect github.com/ysmood/leakless v0.7.0 // indirect
@ -96,7 +100,7 @@ require (
go.opentelemetry.io/otel v0.20.0 // indirect go.opentelemetry.io/otel v0.20.0 // indirect
go.opentelemetry.io/otel/metric v0.20.0 // indirect go.opentelemetry.io/otel/metric v0.20.0 // indirect
go.opentelemetry.io/otel/trace v0.20.0 // indirect go.opentelemetry.io/otel/trace v0.20.0 // indirect
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
golang.org/x/mod v0.5.0 // indirect golang.org/x/mod v0.5.0 // indirect
golang.org/x/net v0.0.0-20220111093109-d55c255bac03 // indirect golang.org/x/net v0.0.0-20220111093109-d55c255bac03 // indirect
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
@ -110,6 +114,7 @@ require (
) )
replace ( replace (
github.com/duo-labs/webauthn => github.com/authelia/webauthn v0.0.0-20220220015615-e607391e7e09
github.com/mattn/go-sqlite3 v2.0.3+incompatible => github.com/mattn/go-sqlite3 v1.14.11 github.com/mattn/go-sqlite3 v2.0.3+incompatible => github.com/mattn/go-sqlite3 v1.14.11
github.com/tidwall/gjson => github.com/tidwall/gjson v1.11.0 github.com/tidwall/gjson => github.com/tidwall/gjson v1.11.0
) )

43
go.sum
View File

@ -1,4 +1,5 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -57,6 +58,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8= github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs= github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@ -78,6 +81,7 @@ github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -101,6 +105,8 @@ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:o
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/authelia/webauthn v0.0.0-20220220015615-e607391e7e09 h1:QaQybILdKa95iPRh6nzNwyqjnRMFq/YFc2z7E5ikEdM=
github.com/authelia/webauthn v0.0.0-20220220015615-e607391e7e09/go.mod h1:mUL5Zt6ReLbdDClw2lvyl5apOOLyG0whwAxh550tXRE=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@ -133,6 +139,7 @@ github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4r
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -146,6 +153,11 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
github.com/cloudflare/cfssl v1.5.0 h1:vFJDAvQgFSRbCn9zg8KpSrrEZrBAQ4KO5oNK7SXEyb0=
github.com/cloudflare/cfssl v1.5.0/go.mod h1:sPPkBS5L8l8sRc/IOO1jG51Xb34u+TYhL6P//JdODMQ=
github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@ -189,6 +201,7 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -255,6 +268,9 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@ -606,6 +622,7 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
@ -653,6 +670,8 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -871,6 +890,7 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
@ -911,6 +931,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw=
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.14.1 h1:hLQYb23E8/fO+1u53d02A97a8UnsddcvYzq4ERRU4ds= github.com/klauspost/compress v1.14.1 h1:hLQYb23E8/fO+1u53d02A97a8UnsddcvYzq4ERRU4ds=
github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
@ -933,6 +955,7 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -1051,6 +1074,7 @@ github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGE
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moul/http2curl v0.0.0-20170919181001-9ac6cf4d929b/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/moul/http2curl v0.0.0-20170919181001-9ac6cf4d929b/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
@ -1063,6 +1087,7 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@ -1375,8 +1400,6 @@ github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
@ -1394,7 +1417,12 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.33.0 h1:mHBKd98J5NcXuBddgjvim1i3kWzlng1SzLhrnBOU9g8= github.com/valyala/fasthttp v1.33.0 h1:mHBKd98J5NcXuBddgjvim1i3kWzlng1SzLhrnBOU9g8=
github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4= github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -1418,6 +1446,11 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
github.com/zmap/zcrypto v0.0.0-20200513165325-16679db567ff/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4=
github.com/zmap/zcrypto v0.0.0-20200911161511-43ff0ea04f21/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4=
github.com/zmap/zlint/v2 v2.2.1/go.mod h1:ixPWsdq8qLxYRpNUTbcKig3R7WgmspsHGLhCCs6rFAM=
go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0= go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0=
go.elastic.co/apm v1.13.0/go.mod h1:dylGv2HKR0tiCV+wliJz1KHtDyuD8SPe69oV7VyK6WY= go.elastic.co/apm v1.13.0/go.mod h1:dylGv2HKR0tiCV+wliJz1KHtDyuD8SPe69oV7VyK6WY=
go.elastic.co/apm/module/apmhttp v1.8.0/go.mod h1:9LPFlEON51/lRbnWDfqAWErihIiAFDUMfMV27YjoWQ8= go.elastic.co/apm/module/apmhttp v1.8.0/go.mod h1:9LPFlEON51/lRbnWDfqAWErihIiAFDUMfMV27YjoWQ8=
@ -1516,18 +1549,21 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200124225646-8b5121be2f68/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1621,6 +1657,7 @@ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=

View File

@ -19,8 +19,8 @@ const (
const ( const (
// TOTP Method using Time-Based One-Time Password applications like Google Authenticator. // TOTP Method using Time-Based One-Time Password applications like Google Authenticator.
TOTP = "totp" TOTP = "totp"
// U2F Method using U2F devices like Yubikeys. // Webauthn Method using Webauthn devices like YubiKeys.
U2F = "u2f" Webauthn = "webauthn"
// Push Method using Duo application to receive push notifications. // Push Method using Duo application to receive push notifications.
Push = "mobile_push" Push = "mobile_push"
) )
@ -37,7 +37,7 @@ const (
) )
// PossibleMethods is the set of all possible 2FA methods. // PossibleMethods is the set of all possible 2FA methods.
var PossibleMethods = []string{TOTP, U2F, Push} var PossibleMethods = []string{TOTP, Webauthn, Push}
// CryptAlgo the crypt representation of an algorithm used in the prefix of the hash. // CryptAlgo the crypt representation of an algorithm used in the prefix of the hash.
type CryptAlgo string type CryptAlgo string

View File

@ -98,6 +98,9 @@ log:
## ##
## Parameters used for TOTP generation. ## Parameters used for TOTP generation.
totp: totp:
## Disable TOTP.
disable: false
## The issuer name displayed in the Authenticator application of your choice. ## The issuer name displayed in the Authenticator application of your choice.
issuer: authelia.com issuer: authelia.com
@ -121,6 +124,28 @@ totp:
skew: 1 skew: 1
## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation. ## See: https://www.authelia.com/docs/configuration/one-time-password.html#input-validation to read the documentation.
##
## WebAuthn Configuration
##
## Parameters used for WebAuthn.
webauthn:
## Disable Webauthn.
disable: false
## Adjust the interaction timeout for Webauthn dialogues.
timeout: 60s
## The display name the browser should show the user for when using Webauthn to login/register.
display_name: Authelia
## Conveyance preference controls if we collect the attestation statement including the AAGUID from the device.
## Options are none, indirect, direct.
attestation_conveyance_preference: indirect
## User verification controls if the user must make a gesture or action to confirm they are present.
## Options are required, preferred, discouraged.
user_verification: preferred
## ##
## Duo Push API Configuration ## Duo Push API Configuration
## ##
@ -576,7 +601,7 @@ storage:
## ##
## Notification Provider ## Notification Provider
## ##
## Notifications are sent to users when they require a password reset, a U2F registration or a TOTP registration. ## Notifications are sent to users when they require a password reset, a Webauthn registration or a TOTP registration.
## The available providers are: filesystem, smtp. You must use only one of these providers. ## The available providers are: filesystem, smtp. You must use only one of these providers.
notifier: notifier:
## You can disable the notifier startup check by setting this to true. ## You can disable the notifier startup check by setting this to true.

View File

@ -25,15 +25,14 @@ func StringToMailAddressFunc() mapstructure.DecodeHookFunc {
} }
var ( var (
mailAddress *mail.Address parsedAddress *mail.Address
) )
mailAddress, err = mail.ParseAddress(dataStr) if parsedAddress, err = mail.ParseAddress(dataStr); err != nil {
if err != nil {
return nil, fmt.Errorf("could not parse '%s' as a RFC5322 address: %w", dataStr, err) return nil, fmt.Errorf("could not parse '%s' as a RFC5322 address: %w", dataStr, err)
} }
return *mailAddress, nil return *parsedAddress, nil
} }
} }

View File

@ -22,18 +22,18 @@ func TestShouldErrorSecretNotExist(t *testing.T) {
dir, err := os.MkdirTemp("", "authelia-test-secret-not-exist") dir, err := os.MkdirTemp("", "authelia-test-secret-not-exist")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", filepath.Join(dir, "jwt"))) testSetEnv(t, "JWT_SECRET_FILE", filepath.Join(dir, "jwt"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"DUO_API_SECRET_KEY_FILE", filepath.Join(dir, "duo"))) testSetEnv(t, "DUO_API_SECRET_KEY_FILE", filepath.Join(dir, "duo"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", filepath.Join(dir, "session"))) testSetEnv(t, "SESSION_SECRET_FILE", filepath.Join(dir, "session"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", filepath.Join(dir, "authentication"))) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", filepath.Join(dir, "authentication"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"NOTIFIER_SMTP_PASSWORD_FILE", filepath.Join(dir, "notifier"))) testSetEnv(t, "NOTIFIER_SMTP_PASSWORD_FILE", filepath.Join(dir, "notifier"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_REDIS_PASSWORD_FILE", filepath.Join(dir, "redis"))) testSetEnv(t, "SESSION_REDIS_PASSWORD_FILE", filepath.Join(dir, "redis"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", filepath.Join(dir, "redis-sentinel"))) testSetEnv(t, "SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", filepath.Join(dir, "redis-sentinel"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", filepath.Join(dir, "mysql"))) testSetEnv(t, "STORAGE_MYSQL_PASSWORD_FILE", filepath.Join(dir, "mysql"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_POSTGRES_PASSWORD_FILE", filepath.Join(dir, "postgres"))) testSetEnv(t, "STORAGE_POSTGRES_PASSWORD_FILE", filepath.Join(dir, "postgres"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SERVER_TLS_KEY_FILE", filepath.Join(dir, "tls"))) testSetEnv(t, "SERVER_TLS_KEY_FILE", filepath.Join(dir, "tls"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE", filepath.Join(dir, "oidc-key"))) testSetEnv(t, "IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE", filepath.Join(dir, "oidc-key"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE", filepath.Join(dir, "oidc-hmac"))) testSetEnv(t, "IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE", filepath.Join(dir, "oidc-hmac"))
val := schema.NewStructValidator() val := schema.NewStructValidator()
_, _, err = Load(val, NewEnvironmentSource(DefaultEnvPrefix, DefaultEnvDelimiter), NewSecretsSource(DefaultEnvPrefix, DefaultEnvDelimiter)) _, _, err = Load(val, NewEnvironmentSource(DefaultEnvPrefix, DefaultEnvDelimiter), NewSecretsSource(DefaultEnvPrefix, DefaultEnvDelimiter))
@ -76,10 +76,10 @@ func TestLoadShouldReturnErrWithoutSources(t *testing.T) {
func TestShouldHaveNotifier(t *testing.T) { func TestShouldHaveNotifier(t *testing.T) {
testReset() testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc")) testSetEnv(t, "SESSION_SECRET", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc")) testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc")) testSetEnv(t, "JWT_SECRET", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
val := schema.NewStructValidator() val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) _, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -93,10 +93,10 @@ func TestShouldHaveNotifier(t *testing.T) {
func TestShouldValidateConfigurationWithEnv(t *testing.T) { func TestShouldValidateConfigurationWithEnv(t *testing.T) {
testReset() testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc")) testSetEnv(t, "SESSION_SECRET", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc")) testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc")) testSetEnv(t, "JWT_SECRET", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
val := schema.NewStructValidator() val := schema.NewStructValidator()
_, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) _, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -109,12 +109,12 @@ func TestShouldValidateConfigurationWithEnv(t *testing.T) {
func TestShouldNotIgnoreInvalidEnvs(t *testing.T) { func TestShouldNotIgnoreInvalidEnvs(t *testing.T) {
testReset() testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "an env session secret")) testSetEnv(t, "SESSION_SECRET", "an env session secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password")) testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "an env storage mysql password")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL", "a bad env")) testSetEnv(t, "STORAGE_MYSQL", "a bad env")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "an env jwt secret")) testSetEnv(t, "JWT_SECRET", "an env jwt secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_URL", "an env authentication backend ldap password")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_URL", "an env authentication backend ldap password")
val := schema.NewStructValidator() val := schema.NewStructValidator()
keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -132,12 +132,12 @@ func TestShouldNotIgnoreInvalidEnvs(t *testing.T) {
func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T) { func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T) {
testReset() testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "an env session secret")) testSetEnv(t, "SESSION_SECRET", "an env session secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", "./test_resources/example_secret")) testSetEnv(t, "SESSION_SECRET_FILE", "./test_resources/example_secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password")) testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "an env storage mysql password")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret")) testSetEnv(t, "JWT_SECRET_FILE", "./test_resources/example_secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY", "a_very_bad_encryption_key")) testSetEnv(t, "STORAGE_ENCRYPTION_KEY", "a_very_bad_encryption_key")
val := schema.NewStructValidator() val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) _, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -181,11 +181,11 @@ func TestShouldRaiseIOErrOnUnreadableFile(t *testing.T) {
func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) { func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
testReset() testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", "./test_resources/example_secret")) testSetEnv(t, "SESSION_SECRET_FILE", "./test_resources/example_secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret")) testSetEnv(t, "STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret")) testSetEnv(t, "JWT_SECRET_FILE", "./test_resources/example_secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", "./test_resources/example_secret")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", "./test_resources/example_secret")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY_FILE", "./test_resources/example_secret")) testSetEnv(t, "STORAGE_ENCRYPTION_KEY_FILE", "./test_resources/example_secret")
val := schema.NewStructValidator() val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) _, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -204,10 +204,10 @@ func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) { func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
testReset() testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc")) testSetEnv(t, "SESSION_SECRET", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc")) testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc")) testSetEnv(t, "JWT_SECRET", "abc")
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")) testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
val := schema.NewStructValidator() val := schema.NewStructValidator()
keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config_bad_keys.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config_bad_keys.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@ -336,6 +336,10 @@ func TestShouldNotLoadDirectoryConfiguration(t *testing.T) {
assert.EqualError(t, val.Errors()[0], fmt.Sprintf("failed to load configuration from yaml file(%s) source: %s", dir, expectedErr)) assert.EqualError(t, val.Errors()[0], fmt.Sprintf("failed to load configuration from yaml file(%s) source: %s", dir, expectedErr))
} }
func testSetEnv(t *testing.T, key, value string) {
assert.NoError(t, os.Setenv(DefaultEnvPrefix+key, value))
}
func testReset() { func testReset() {
testUnsetEnvName("STORAGE_MYSQL") testUnsetEnvName("STORAGE_MYSQL")
testUnsetEnvName("JWT_SECRET") testUnsetEnvName("JWT_SECRET")

View File

@ -11,7 +11,7 @@ type Configuration struct {
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"` IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"` AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"`
Session SessionConfiguration `koanf:"session"` Session SessionConfiguration `koanf:"session"`
TOTP *TOTPConfiguration `koanf:"totp"` TOTP TOTPConfiguration `koanf:"totp"`
DuoAPI *DuoAPIConfiguration `koanf:"duo_api"` DuoAPI *DuoAPIConfiguration `koanf:"duo_api"`
AccessControl AccessControlConfiguration `koanf:"access_control"` AccessControl AccessControlConfiguration `koanf:"access_control"`
NTP NTPConfiguration `koanf:"ntp"` NTP NTPConfiguration `koanf:"ntp"`
@ -19,4 +19,5 @@ type Configuration struct {
Storage StorageConfiguration `koanf:"storage"` Storage StorageConfiguration `koanf:"storage"`
Notifier *NotifierConfiguration `koanf:"notifier"` Notifier *NotifierConfiguration `koanf:"notifier"`
Server ServerConfiguration `koanf:"server"` Server ServerConfiguration `koanf:"server"`
Webauthn WebauthnConfiguration `koanf:"webauthn"`
} }

View File

@ -2,6 +2,7 @@ package schema
// TOTPConfiguration represents the configuration related to TOTP options. // TOTPConfiguration represents the configuration related to TOTP options.
type TOTPConfiguration struct { type TOTPConfiguration struct {
Disable bool `koanf:"disable"`
Issuer string `koanf:"issuer"` Issuer string `koanf:"issuer"`
Algorithm string `koanf:"algorithm"` Algorithm string `koanf:"algorithm"`
Digits uint `koanf:"digits"` Digits uint `koanf:"digits"`

View File

@ -0,0 +1,27 @@
package schema
import (
"time"
"github.com/duo-labs/webauthn/protocol"
)
// WebauthnConfiguration represents the webauthn config.
type WebauthnConfiguration struct {
Disable bool `koanf:"disable"`
DisplayName string `koanf:"display_name"`
ConveyancePreference protocol.ConveyancePreference `koanf:"attestation_conveyance_preference"`
UserVerification protocol.UserVerificationRequirement `koanf:"user_verification"`
Timeout time.Duration `koanf:"timeout"`
}
// DefaultWebauthnConfiguration describes the default values for the WebauthnConfiguration.
var DefaultWebauthnConfiguration = WebauthnConfiguration{
DisplayName: "Authelia",
Timeout: time.Second * 60,
ConveyancePreference: protocol.PreferIndirectAttestation,
UserVerification: protocol.VerificationPreferred,
}

View File

@ -39,6 +39,8 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
ValidateTOTP(config, validator) ValidateTOTP(config, validator)
ValidateWebauthn(config, validator)
ValidateAuthenticationBackend(&config.AuthenticationBackend, validator) ValidateAuthenticationBackend(&config.AuthenticationBackend, validator)
ValidateAccessControl(config, validator) ValidateAccessControl(config, validator)

View File

@ -3,6 +3,8 @@ package validator
import ( import (
"regexp" "regexp"
"github.com/duo-labs/webauthn/protocol"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
) )
@ -150,6 +152,12 @@ 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.
const (
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'"
)
// Access Control error constants. // Access Control error constants.
const ( const (
errFmtAccessControlDefaultPolicyValue = "access control: option 'default_policy' must be one of '%s' but it is " + errFmtAccessControlDefaultPolicyValue = "access control: option 'default_policy' must be one of '%s' but it is " +
@ -245,6 +253,9 @@ var validSessionSameSiteValues = []string{"none", "lax", "strict"}
var validLoLevels = []string{"trace", "debug", "info", "warn", "error"} var validLoLevels = []string{"trace", "debug", "info", "warn", "error"}
var validWebauthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)}
var validWebauthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)}
var validACLRuleMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} var validACLRuleMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
var validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny} var validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny}
@ -285,12 +296,20 @@ var ValidKeys = []string{
"server.headers.csp_template", "server.headers.csp_template",
// TOTP Keys. // TOTP Keys.
"totp.disable",
"totp.issuer", "totp.issuer",
"totp.algorithm", "totp.algorithm",
"totp.digits", "totp.digits",
"totp.period", "totp.period",
"totp.skew", "totp.skew",
// Webauthn Keys.
"webauthn.disable",
"webauthn.display_name",
"webauthn.attestation_conveyance_preference",
"webauthn.user_verification",
"webauthn.timeout",
// DUO API Keys. // DUO API Keys.
"duo_api.hostname", "duo_api.hostname",
"duo_api.enable_self_enrollment", "duo_api.enable_self_enrollment",

View File

@ -18,7 +18,7 @@ func ValidateNTP(config *schema.Configuration, validator *schema.StructValidator
validator.Push(fmt.Errorf(errFmtNTPVersion, config.NTP.Version)) validator.Push(fmt.Errorf(errFmtNTPVersion, config.NTP.Version))
} }
if config.NTP.MaximumDesync == 0 { if config.NTP.MaximumDesync <= 0 {
config.NTP.MaximumDesync = schema.DefaultNTPConfiguration.MaximumDesync config.NTP.MaximumDesync = schema.DefaultNTPConfiguration.MaximumDesync
} }
} }

View File

@ -15,7 +15,7 @@ func newDefaultNTPConfig() schema.Configuration {
} }
} }
func TestShouldSetDefaultNtpAddress(t *testing.T) { func TestShouldSetDefaultNTPValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultNTPConfig() config := newDefaultNTPConfig()
@ -23,21 +23,16 @@ func TestShouldSetDefaultNtpAddress(t *testing.T) {
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultNTPConfiguration.Address, config.NTP.Address) assert.Equal(t, schema.DefaultNTPConfiguration.Address, config.NTP.Address)
assert.Equal(t, schema.DefaultNTPConfiguration.Version, config.NTP.Version)
assert.Equal(t, schema.DefaultNTPConfiguration.MaximumDesync, config.NTP.MaximumDesync)
assert.Equal(t, schema.DefaultNTPConfiguration.DisableStartupCheck, config.NTP.DisableStartupCheck)
} }
func TestShouldSetDefaultNtpVersion(t *testing.T) { func TestShouldSetDefaultNtpVersion(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultNTPConfig() config := newDefaultNTPConfig()
ValidateNTP(&config, validator) config.NTP.MaximumDesync = -1
assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultNTPConfiguration.Version, config.NTP.Version)
}
func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultNTPConfig()
ValidateNTP(&config, validator) ValidateNTP(&config, validator)
@ -45,16 +40,6 @@ func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) {
assert.Equal(t, schema.DefaultNTPConfiguration.MaximumDesync, config.NTP.MaximumDesync) assert.Equal(t, schema.DefaultNTPConfiguration.MaximumDesync, config.NTP.MaximumDesync)
} }
func TestShouldSetDefaultNtpDisableStartupCheck(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultNTPConfig()
ValidateNTP(&config, validator)
assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultNTPConfiguration.DisableStartupCheck, config.NTP.DisableStartupCheck)
}
func TestShouldRaiseErrorOnInvalidNTPVersion(t *testing.T) { func TestShouldRaiseErrorOnInvalidNTPVersion(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultNTPConfig() config := newDefaultNTPConfig()

View File

@ -8,11 +8,11 @@ import (
// ValidateRegulation validates and update regulator configuration. // ValidateRegulation validates and update regulator configuration.
func ValidateRegulation(config *schema.Configuration, validator *schema.StructValidator) { func ValidateRegulation(config *schema.Configuration, validator *schema.StructValidator) {
if config.Regulation.FindTime == 0 { if config.Regulation.FindTime <= 0 {
config.Regulation.FindTime = schema.DefaultRegulationConfiguration.FindTime // 2 min. config.Regulation.FindTime = schema.DefaultRegulationConfiguration.FindTime // 2 min.
} }
if config.Regulation.BanTime == 0 { if config.Regulation.BanTime <= 0 {
config.Regulation.BanTime = schema.DefaultRegulationConfiguration.BanTime // 5 min. config.Regulation.BanTime = schema.DefaultRegulationConfiguration.BanTime // 5 min.
} }

View File

@ -17,7 +17,7 @@ func newDefaultRegulationConfig() schema.Configuration {
return config return config
} }
func TestShouldSetDefaultRegulationBanTime(t *testing.T) { func TestShouldSetDefaultRegulationTimeDurationsWhenUnset(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultRegulationConfig() config := newDefaultRegulationConfig()
@ -25,12 +25,16 @@ func TestShouldSetDefaultRegulationBanTime(t *testing.T) {
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultRegulationConfiguration.BanTime, config.Regulation.BanTime) assert.Equal(t, schema.DefaultRegulationConfiguration.BanTime, config.Regulation.BanTime)
assert.Equal(t, schema.DefaultRegulationConfiguration.FindTime, config.Regulation.FindTime)
} }
func TestShouldSetDefaultRegulationFindTime(t *testing.T) { func TestShouldSetDefaultRegulationTimeDurationsWhenNegative(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultRegulationConfig() config := newDefaultRegulationConfig()
config.Regulation.BanTime = -1
config.Regulation.FindTime = -1
ValidateRegulation(&config, validator) ValidateRegulation(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)

View File

@ -18,7 +18,7 @@ func newDefaultSessionConfig() schema.SessionConfiguration {
return config return config
} }
func TestShouldSetDefaultSessionName(t *testing.T) { func TestShouldSetDefaultSessionValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
@ -27,39 +27,25 @@ func TestShouldSetDefaultSessionName(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors()) assert.False(t, validator.HasErrors())
assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name) assert.Equal(t, schema.DefaultSessionConfiguration.Name, config.Name)
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration)
assert.Equal(t, schema.DefaultSessionConfiguration.RememberMeDuration, config.RememberMeDuration)
assert.Equal(t, schema.DefaultSessionConfiguration.SameSite, config.SameSite)
} }
func TestShouldSetDefaultSessionInactivity(t *testing.T) { func TestShouldSetDefaultSessionValuesWhenNegative(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultSessionConfig() config := newDefaultSessionConfig()
config.Expiration, config.Inactivity, config.RememberMeDuration = -1, -1, -1
ValidateSession(&config, validator) ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors()) assert.False(t, validator.HasErrors())
assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity) assert.Equal(t, schema.DefaultSessionConfiguration.Inactivity, config.Inactivity)
}
func TestShouldSetDefaultSessionExpiration(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration) assert.Equal(t, schema.DefaultSessionConfiguration.Expiration, config.Expiration)
} assert.Equal(t, schema.DefaultSessionConfiguration.RememberMeDuration, config.RememberMeDuration)
func TestShouldSetDefaultSessionSameSite(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultSessionConfig()
ValidateSession(&config, validator)
assert.False(t, validator.HasWarnings())
assert.False(t, validator.HasErrors())
assert.Equal(t, schema.DefaultSessionConfiguration.SameSite, config.SameSite)
} }
func TestShouldHandleRedisConfigSuccessfully(t *testing.T) { func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {

View File

@ -10,12 +10,6 @@ import (
// ValidateTOTP validates and update TOTP configuration. // ValidateTOTP validates and update TOTP configuration.
func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) { func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) {
if config.TOTP == nil {
config.TOTP = &schema.DefaultTOTPConfiguration
return
}
if config.TOTP.Issuer == "" { if config.TOTP.Issuer == "" {
config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
} }

View File

@ -14,7 +14,7 @@ import (
func TestShouldSetDefaultTOTPValues(t *testing.T) { func TestShouldSetDefaultTOTPValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{}, TOTP: schema.TOTPConfiguration{},
} }
ValidateTOTP(config, validator) ValidateTOTP(config, validator)
@ -30,7 +30,7 @@ func TestShouldNormalizeTOTPAlgorithm(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Algorithm: "sha1", Algorithm: "sha1",
}, },
} }
@ -45,7 +45,7 @@ func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Algorithm: "sha3", Algorithm: "sha3",
}, },
} }
@ -59,7 +59,7 @@ func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
func TestShouldRaiseErrorWhenInvalidTOTPValues(t *testing.T) { func TestShouldRaiseErrorWhenInvalidTOTPValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
TOTP: &schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Period: 5, Period: 5,
Digits: 20, Digits: 20,
}, },

View File

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

View File

@ -0,0 +1,98 @@
package validator
import (
"testing"
"time"
"github.com/duo-labs/webauthn/protocol"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func TestWebauthnShouldSetDefaultValues(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{},
}
ValidateWebauthn(config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultWebauthnConfiguration.DisplayName, config.Webauthn.DisplayName)
assert.Equal(t, schema.DefaultWebauthnConfiguration.Timeout, config.Webauthn.Timeout)
assert.Equal(t, schema.DefaultWebauthnConfiguration.ConveyancePreference, config.Webauthn.ConveyancePreference)
assert.Equal(t, schema.DefaultWebauthnConfiguration.UserVerification, config.Webauthn.UserVerification)
}
func TestWebauthnShouldSetDefaultTimeoutWhenNegative(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{
Timeout: -1,
},
}
ValidateWebauthn(config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultWebauthnConfiguration.Timeout, config.Webauthn.Timeout)
}
func TestWebauthnShouldNotSetDefaultValuesWhenConfigured(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{
DisplayName: "Test",
Timeout: time.Second * 50,
ConveyancePreference: protocol.PreferNoAttestation,
UserVerification: protocol.VerificationDiscouraged,
},
}
ValidateWebauthn(config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, "Test", config.Webauthn.DisplayName)
assert.Equal(t, time.Second*50, config.Webauthn.Timeout)
assert.Equal(t, protocol.PreferNoAttestation, config.Webauthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationDiscouraged, config.Webauthn.UserVerification)
config.Webauthn.ConveyancePreference = protocol.PreferIndirectAttestation
config.Webauthn.UserVerification = protocol.VerificationPreferred
ValidateWebauthn(config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, protocol.PreferIndirectAttestation, config.Webauthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationPreferred, config.Webauthn.UserVerification)
config.Webauthn.ConveyancePreference = protocol.PreferDirectAttestation
config.Webauthn.UserVerification = protocol.VerificationRequired
ValidateWebauthn(config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, protocol.PreferDirectAttestation, config.Webauthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationRequired, config.Webauthn.UserVerification)
}
func TestWebauthnShouldRaiseErrorsOnInvalidOptions(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{
DisplayName: "Test",
Timeout: time.Second * 50,
ConveyancePreference: "no",
UserVerification: "yes",
},
}
ValidateWebauthn(config, validator)
require.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "webauthn: option 'attestation_conveyance_preference' must be one of 'none', 'indirect', 'direct' but it is configured as 'no'")
assert.EqualError(t, validator.Errors()[1], "webauthn: option 'user_verification' must be one of 'discouraged', 'preferred', 'required' but it is configured as 'yes'")
}

View File

@ -10,8 +10,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"
// ActionU2FRegistration 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.
ActionU2FRegistration = "RegisterU2FDevice" ActionWebauthnRegistration = "RegisterWebauthnDevice"
// ActionResetPassword is the string representation of the action for which the token has been produced. // ActionResetPassword is the string representation of the action for which the token has been produced.
ActionResetPassword = "ResetPassword" ActionResetPassword = "ResetPassword"

View File

@ -1,13 +0,0 @@
package handlers
import "errors"
// InternalError is the error message sent when there was an internal error but it should
// be hidden to the end user. In that case the error should be in the server logs.
const InternalError = "Internal error."
// UnauthorizedError is the error message sent when the user is not authorized.
const UnauthorizedError = "You're not authorized."
var errMissingXForwardedHost = errors.New("missing header X-Forwarded-Host")
var errMissingXForwardedProto = errors.New("missing header X-Forwarded-Proto")

View File

@ -7,20 +7,27 @@ import (
// ConfigurationGet get the configuration accessible to authenticated users. // ConfigurationGet get the configuration accessible to authenticated users.
func ConfigurationGet(ctx *middlewares.AutheliaCtx) { func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := configurationBody{} body := configurationBody{
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F} AvailableMethods: make(MethodList, 0, 3),
}
if ctx.Providers.Authorizer.IsSecondFactorEnabled() {
if !ctx.Configuration.TOTP.Disable {
body.AvailableMethods = append(body.AvailableMethods, authentication.TOTP)
}
if !ctx.Configuration.Webauthn.Disable {
body.AvailableMethods = append(body.AvailableMethods, authentication.Webauthn)
}
if ctx.Configuration.DuoAPI != nil { if ctx.Configuration.DuoAPI != nil {
body.AvailableMethods = append(body.AvailableMethods, authentication.Push) body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
} }
}
body.SecondFactorEnabled = ctx.Providers.Authorizer.IsSecondFactorEnabled()
ctx.Logger.Tracef("Second factor enabled: %v", body.SecondFactorEnabled)
ctx.Logger.Tracef("Available methods are %s", body.AvailableMethods) ctx.Logger.Tracef("Available methods are %s", body.AvailableMethods)
err := ctx.SetJSONBody(body) if err := ctx.SetJSONBody(body); err != nil {
if err != nil {
ctx.Logger.Errorf("Unable to set configuration response in body: %s", err) ctx.Logger.Errorf("Unable to set configuration response in body: %s", err)
} }
} }

View File

@ -28,106 +28,171 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
s.mock.Close() s.mock.Close()
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() { func (s *SecondFactorAvailableMethodsFixture) TestShouldHaveAllConfiguredMethods() {
expectedBody := configurationBody{
AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false,
}
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration = schema.Configuration{ s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{}, DuoAPI: &schema.DuoAPIConfiguration{},
} TOTP: schema.TOTPConfiguration{
expectedBody := configurationBody{ Disable: false,
AvailableMethods: []string{"totp", "u2f", "mobile_push"}, },
SecondFactorEnabled: false, Webauthn: schema.WebauthnConfiguration{
} Disable: false,
},
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() {
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(
&schema.Configuration{
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "bypass", DefaultPolicy: "deny",
Rules: []schema.ACLRule{ Rules: []schema.ACLRule{
{ {
Domains: []string{"example.com"}, Domains: []string{"example.com"},
Policy: "deny",
},
{
Domains: []string{"abc.example.com"},
Policy: "single_factor",
},
{
Domains: []string{"def.example.com"},
Policy: "bypass",
},
},
}})
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: false,
})
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenDefaultPolicySetToTwoFactor() {
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "two_factor",
Rules: []schema.ACLRule{
{
Domains: []string{"example.com"},
Policy: "deny",
},
{
Domains: []string{"abc.example.com"},
Policy: "single_factor",
},
{
Domains: []string{"def.example.com"},
Policy: "bypass",
},
},
}})
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "u2f"},
SecondFactorEnabled: true,
})
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenSomePolicySetToTwoFactor() {
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(
&schema.Configuration{
AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "bypass",
Rules: []schema.ACLRule{
{
Domains: []string{"example.com"},
Policy: "deny",
},
{
Domains: []string{"abc.example.com"},
Policy: "two_factor", Policy: "two_factor",
}, },
{
Domains: []string{"def.example.com"},
Policy: "bypass",
}, },
}, }}
}})
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{ s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "u2f"}, AvailableMethods: []string{"totp", "webauthn", "mobile_push"},
SecondFactorEnabled: true, })
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveTOTPFromAvailableMethodsWhenDisabled() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{},
TOTP: schema.TOTPConfiguration{
Disable: true,
},
Webauthn: schema.WebauthnConfiguration{
Disable: false,
},
AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "deny",
Rules: []schema.ACLRule{
{
Domains: []string{"example.com"},
Policy: "two_factor",
},
},
}}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"webauthn", "mobile_push"},
})
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveWebauthnFromAvailableMethodsWhenDisabled() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{},
TOTP: schema.TOTPConfiguration{
Disable: false,
},
Webauthn: schema.WebauthnConfiguration{
Disable: true,
},
AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "deny",
Rules: []schema.ACLRule{
{
Domains: []string{"example.com"},
Policy: "two_factor",
},
},
}}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "mobile_push"},
})
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveDuoFromAvailableMethodsWhenNotConfigured() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: nil,
TOTP: schema.TOTPConfiguration{
Disable: false,
},
Webauthn: schema.WebauthnConfiguration{
Disable: false,
},
AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "deny",
Rules: []schema.ACLRule{
{
Domains: []string{"example.com"},
Policy: "two_factor",
},
},
}}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{"totp", "webauthn"},
})
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveAllMethodsWhenNoTwoFactorACLRulesConfigured() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{},
TOTP: schema.TOTPConfiguration{
Disable: false,
},
Webauthn: schema.WebauthnConfiguration{
Disable: false,
},
AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "deny",
Rules: []schema.ACLRule{
{
Domains: []string{"example.com"},
Policy: "one_factor",
},
},
}}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{},
})
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveAllMethodsWhenAllDisabledOrNotConfigured() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: nil,
TOTP: schema.TOTPConfiguration{
Disable: true,
},
Webauthn: schema.WebauthnConfiguration{
Disable: true,
},
AccessControl: schema.AccessControlConfiguration{
DefaultPolicy: "deny",
Rules: []schema.ACLRule{
{
Domains: []string{"example.com"},
Policy: "two_factor",
},
},
}}
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), configurationBody{
AvailableMethods: []string{},
}) })
} }

View File

@ -1,70 +0,0 @@
package handlers
import (
"fmt"
"github.com/tstranex/u2f"
"github.com/authelia/authelia/v4/internal/middlewares"
)
var u2fConfig = &u2f.Config{
// Chrome 66+ doesn't return the device's attestation
// certificate by default.
SkipAttestationVerify: true,
}
// SecondFactorU2FIdentityStart the handler for initiating the identity validation.
var SecondFactorU2FIdentityStart = middlewares.IdentityVerificationStart(middlewares.IdentityVerificationStartArgs{
MailTitle: "Register your key",
MailButtonContent: "Register",
TargetEndpoint: "/security-key/register",
ActionClaim: ActionU2FRegistration,
IdentityRetrieverFunc: identityRetrieverFromSession,
}, nil)
func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
if ctx.XForwardedProto() == nil {
ctx.Error(errMissingXForwardedProto, messageOperationFailed)
return
}
if ctx.XForwardedHost() == nil {
ctx.Error(errMissingXForwardedHost, messageOperationFailed)
return
}
appID := fmt.Sprintf("%s://%s", ctx.XForwardedProto(), ctx.XForwardedHost())
ctx.Logger.Tracef("U2F appID is %s", appID)
var trustedFacets = []string{appID}
challenge, err := u2f.NewChallenge(appID, trustedFacets)
if err != nil {
ctx.Error(fmt.Errorf("unable to generate new U2F challenge for registration: %s", err), messageOperationFailed)
return
}
// Save the challenge in the user session.
userSession := ctx.GetSession()
userSession.U2FChallenge = challenge
err = ctx.SaveSession(userSession)
if err != nil {
ctx.Error(fmt.Errorf("unable to save U2F challenge in session: %s", err), messageOperationFailed)
return
}
err = ctx.SetJSONBody(u2f.NewWebRegisterRequest(challenge, []u2f.Registration{}))
if err != nil {
ctx.Logger.Errorf("Unable to create request to enrol new token: %s", err)
}
}
// SecondFactorU2FIdentityFinish the handler for finishing the identity validation.
var SecondFactorU2FIdentityFinish = middlewares.IdentityVerificationFinish(
middlewares.IdentityVerificationFinishArgs{
ActionClaim: ActionU2FRegistration,
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
}, secondFactorU2FIdentityFinish)

View File

@ -1,73 +0,0 @@
package handlers
import (
"fmt"
"testing"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/models"
)
type HandlerRegisterU2FStep1Suite struct {
suite.Suite
mock *mocks.MockAutheliaCtx
}
func (s *HandlerRegisterU2FStep1Suite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession := s.mock.Ctx.GetSession()
userSession.Username = testUsername
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
}
func (s *HandlerRegisterU2FStep1Suite) TearDownTest() {
s.mock.Close()
}
func createToken(ctx *mocks.MockAutheliaCtx, username, action string, expiresAt time.Time) (data string, verification models.IdentityVerification) {
verification = models.NewIdentityVerification(uuid.New(), username, action, ctx.Ctx.RemoteIP())
verification.ExpiresAt = expiresAt
claims := verification.ToIdentityVerificationClaim()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, _ := token.SignedString([]byte(ctx.Ctx.Configuration.JWTSecret))
return ss, verification
}
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() {
s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
token, verification := createToken(s.mock, "john", ActionU2FRegistration,
time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil)
s.mock.StorageMock.EXPECT().
ConsumeIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String()), gomock.Eq(models.NewNullIP(s.mock.Ctx.RemoteIP()))).
Return(nil)
SecondFactorU2FIdentityFinish(s.mock.Ctx)
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), "missing header X-Forwarded-Host", s.mock.Hook.LastEntry().Message)
}
func TestShouldRunHandlerRegisterU2FStep1Suite(t *testing.T) {
suite.Run(t, new(HandlerRegisterU2FStep1Suite))
}

View File

@ -1,63 +0,0 @@
package handlers
import (
"crypto/elliptic"
"fmt"
"github.com/tstranex/u2f"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
)
// SecondFactorU2FRegister handler validating the client has successfully validated the challenge
// to complete the U2F registration.
func SecondFactorU2FRegister(ctx *middlewares.AutheliaCtx) {
responseBody := u2f.RegisterResponse{}
err := ctx.ParseBody(&responseBody)
if err != nil {
ctx.Error(fmt.Errorf("unable to parse response body: %v", err), messageUnableToRegisterSecurityKey)
}
userSession := ctx.GetSession()
if userSession.U2FChallenge == nil {
ctx.Error(fmt.Errorf("U2F registration has not been initiated yet"), messageUnableToRegisterSecurityKey)
return
}
// Ensure the challenge is cleared if anything goes wrong.
defer func() {
userSession.U2FChallenge = nil
err := ctx.SaveSession(userSession)
if err != nil {
ctx.Logger.Errorf("Unable to clear U2F challenge in session for user %s: %s", userSession.Username, err)
}
}()
registration, err := u2f.Register(responseBody, *userSession.U2FChallenge, u2fConfig)
if err != nil {
ctx.Error(fmt.Errorf("unable to verify U2F registration: %v", err), messageUnableToRegisterSecurityKey)
return
}
ctx.Logger.Debugf("Register U2F device for user %s", userSession.Username)
publicKey := elliptic.Marshal(elliptic.P256(), registration.PubKey.X, registration.PubKey.Y)
err = ctx.Providers.StorageProvider.SaveU2FDevice(ctx, models.U2FDevice{
Username: userSession.Username,
Description: "Primary",
KeyHandle: registration.KeyHandle,
PublicKey: publicKey},
)
if err != nil {
ctx.Error(fmt.Errorf("unable to register U2F device for user %s: %v", userSession.Username, err), messageUnableToRegisterSecurityKey)
return
}
ctx.ReplyOK()
}

View File

@ -0,0 +1,158 @@
package handlers
import (
"bytes"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/regulation"
)
// SecondFactorWebauthnIdentityStart the handler for initiating the identity validation.
var SecondFactorWebauthnIdentityStart = middlewares.IdentityVerificationStart(middlewares.IdentityVerificationStartArgs{
MailTitle: "Register your key",
MailButtonContent: "Register",
TargetEndpoint: "/webauthn/register",
ActionClaim: ActionWebauthnRegistration,
IdentityRetrieverFunc: identityRetrieverFromSession,
}, nil)
// SecondFactorWebauthnIdentityFinish the handler for finishing the identity validation.
var SecondFactorWebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
middlewares.IdentityVerificationFinishArgs{
ActionClaim: ActionWebauthnRegistration,
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
}, SecondFactorWebauthnAttestationGET)
// SecondFactorWebauthnAttestationGET returns the attestation challenge from the server.
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
var (
w *webauthn.WebAuthn
user *models.WebauthnUser
err error
)
userSession := ctx.GetSession()
if w, err = newWebauthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return
}
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
var credentialCreation *protocol.CredentialCreation
if credentialCreation, userSession.Webauthn, err = w.BeginRegistration(user); err != nil {
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return
}
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return
}
if err = ctx.SetJSONBody(credentialCreation); err != nil {
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return
}
}
// SecondFactorWebauthnAttestationPOST processes the attestation challenge response from the client.
func SecondFactorWebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
var (
err error
w *webauthn.WebAuthn
user *models.WebauthnUser
attestationResponse *protocol.ParsedCredentialCreationData
credential *webauthn.Credential
)
userSession := ctx.GetSession()
if userSession.Webauthn == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle attestation 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)
return
}
if w, err = newWebauthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return
}
if attestationResponse, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if credential, err = w.CreateCredential(user, *userSession.Webauthn, attestationResponse); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
device := models.NewWebauthnDeviceFromCredential(w.Config.RPID, userSession.Username, "Primary", credential)
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession.Webauthn = nil
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
ctx.ReplyOK()
ctx.SetStatusCode(fasthttp.StatusCreated)
}

View File

@ -58,6 +58,16 @@ func SecondFactorTOTPPost(ctx *middlewares.AutheliaCtx) {
return return
} }
config.UpdateSignInInfo(ctx.Clock.Now())
if err = ctx.Providers.StorageProvider.UpdateTOTPConfigurationSignIn(ctx, config.ID, config.LastUsedAt); err != nil {
ctx.Logger.Errorf("Unable to save %s device sign in metadata for user '%s': %v", regulation.AuthTypeTOTP, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession.SetTwoFactor(ctx.Clock.Now()) userSession.SetTwoFactor(ctx.Clock.Now())
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {

View File

@ -2,18 +2,17 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"errors"
"regexp" "regexp"
"testing" "testing"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/tstranex/u2f"
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/models" "github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
) )
type HandlerSignTOTPSuite struct { type HandlerSignTOTPSuite struct {
@ -26,8 +25,6 @@ func (s *HandlerSignTOTPSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T()) s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession := s.mock.Ctx.GetSession() userSession := s.mock.Ctx.GetSession()
userSession.Username = testUsername userSession.Username = testUsername
userSession.U2FChallenge = &u2f.Challenge{}
userSession.U2FRegistration = &session.U2FRegistration{}
err := s.mock.Ctx.SaveSession(userSession) err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err) require.NoError(s.T(), err)
} }
@ -56,6 +53,10 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil) s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil)
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
@ -70,6 +71,42 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
}) })
} }
func (s *HandlerSignTOTPSuite) TestShouldFailWhenTOTPSignInInfoFailsToUpdate() {
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
Return(&config, nil)
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeTOTP,
RemoteIP: models.NewNullIPFromString("0.0.0.0"),
}))
s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil)
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any()).Return(errors.New("failed to perform update"))
s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorTOTPPost(s.mock.Ctx)
s.mock.Assert401KO(s.T(), "Authentication failed, please retry later.")
}
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() { func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"} config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
@ -90,6 +127,10 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil) s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil)
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
}) })
@ -120,10 +161,15 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil) s.mock.TOTPMock.EXPECT().Validate(gomock.Eq("abc"), gomock.Eq(&config)).Return(true, nil)
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
TargetURL: "https://mydomain.local", TargetURL: "https://mydomain.local",
}) })
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) s.mock.Ctx.Request.SetBody(bodyBytes)
@ -135,7 +181,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() { func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
s.mock.StorageMock.EXPECT(). s.mock.StorageMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()). LoadTOTPConfiguration(s.mock.Ctx, "john").
Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil) Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil)
s.mock.StorageMock. s.mock.StorageMock.
@ -149,6 +195,10 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
RemoteIP: models.NewNullIPFromString("0.0.0.0"), RemoteIP: models.NewNullIPFromString("0.0.0.0"),
})) }))
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
s.mock.TOTPMock.EXPECT(). s.mock.TOTPMock.EXPECT().
Validate(gomock.Eq("abc"), gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")})). Validate(gomock.Eq("abc"), gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")})).
Return(true, nil) Return(true, nil)
@ -187,6 +237,10 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi
Validate(gomock.Eq("abc"), gomock.Eq(&config)). Validate(gomock.Eq("abc"), gomock.Eq(&config)).
Return(true, nil) Return(true, nil)
s.mock.StorageMock.
EXPECT().
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
}) })

View File

@ -1,93 +0,0 @@
package handlers
import (
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
"github.com/tstranex/u2f"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
)
// SecondFactorU2FSignGet handler for initiating a signing request.
func SecondFactorU2FSignGet(ctx *middlewares.AutheliaCtx) {
if ctx.XForwardedProto() == nil {
ctx.Error(errMissingXForwardedProto, messageMFAValidationFailed)
return
}
if ctx.XForwardedHost() == nil {
ctx.Error(errMissingXForwardedHost, messageMFAValidationFailed)
return
}
userSession := ctx.GetSession()
appID := fmt.Sprintf("%s://%s", ctx.XForwardedProto(), ctx.XForwardedHost())
var trustedFacets = []string{appID}
challenge, err := u2f.NewChallenge(appID, trustedFacets)
if err != nil {
ctx.Logger.Errorf("Unable to create %s challenge for user '%s': %+v", regulation.AuthTypeU2F, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
device, err := ctx.Providers.StorageProvider.LoadU2FDevice(ctx, userSession.Username)
if err != nil {
respondUnauthorized(ctx, messageMFAValidationFailed)
if err == storage.ErrNoU2FDeviceHandle {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeU2F, fmt.Errorf("no registered U2F device"))
return
}
ctx.Logger.Errorf("Could not load %s devices for user '%s': %+v", regulation.AuthTypeU2F, userSession.Username, err)
return
}
x, y := elliptic.Unmarshal(elliptic.P256(), device.PublicKey)
registration := u2f.Registration{
KeyHandle: device.KeyHandle,
PubKey: ecdsa.PublicKey{
Curve: elliptic.P256(),
X: x,
Y: y,
},
}
// Save the challenge and registration for use in next request.
userSession.U2FRegistration = &session.U2FRegistration{
KeyHandle: device.KeyHandle,
PublicKey: device.PublicKey,
}
userSession.U2FChallenge = challenge
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "challenge and registration", regulation.AuthTypeU2F, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
signRequest := challenge.SignRequest([]u2f.Registration{registration})
if err = ctx.SetJSONBody(signRequest); err != nil {
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeU2F, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
}

View File

@ -1,43 +0,0 @@
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/mocks"
)
type HandlerSignU2FStep1Suite struct {
suite.Suite
mock *mocks.MockAutheliaCtx
}
func (s *HandlerSignU2FStep1Suite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
}
func (s *HandlerSignU2FStep1Suite) TearDownTest() {
s.mock.Close()
}
func (s *HandlerSignU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissing() {
SecondFactorU2FSignGet(s.mock.Ctx)
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), "missing header X-Forwarded-Host", s.mock.Hook.LastEntry().Message)
}
func (s *HandlerSignU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() {
s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http")
SecondFactorU2FSignGet(s.mock.Ctx)
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), "missing header X-Forwarded-Host", s.mock.Hook.LastEntry().Message)
}
func TestShouldRunHandlerSignU2FStep1Suite(t *testing.T) {
suite.Run(t, new(HandlerSignU2FStep1Suite))
}

View File

@ -1,82 +0,0 @@
package handlers
import (
"errors"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/regulation"
)
// SecondFactorU2FSignPost handler for completing a signing request.
func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) {
var (
requestBody signU2FRequestBody
err error
)
if err := ctx.ParseBody(&requestBody); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeU2F, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession := ctx.GetSession()
if userSession.U2FChallenge == nil {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeU2F, errors.New("session did not contain a challenge"))
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if userSession.U2FRegistration == nil {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeU2F, errors.New("session did not contain a registration"))
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = u2fVerifier.Verify(userSession.U2FRegistration.KeyHandle, userSession.U2FRegistration.PublicKey,
requestBody.SignResponse, *userSession.U2FChallenge); err != nil {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeU2F, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil {
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeU2F, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeU2F, nil); err != nil {
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession.SetTwoFactor(ctx.Clock.Now())
err = ctx.SaveSession(userSession)
if err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "authentication time", regulation.AuthTypeU2F, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if userSession.OIDCWorkflowSession != nil {
handleOIDCWorkflowResponse(ctx)
} else {
Handle2FAResponse(ctx, requestBody.TargetURL)
}
}
}

View File

@ -1,196 +0,0 @@
package handlers
import (
"encoding/json"
"regexp"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/tstranex/u2f"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session"
)
type HandlerSignU2FStep2Suite struct {
suite.Suite
mock *mocks.MockAutheliaCtx
}
func (s *HandlerSignU2FStep2Suite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
userSession := s.mock.Ctx.GetSession()
userSession.Username = testUsername
userSession.U2FChallenge = &u2f.Challenge{}
userSession.U2FRegistration = &session.U2FRegistration{}
err := s.mock.Ctx.SaveSession(userSession)
require.NoError(s.T(), err)
}
func (s *HandlerSignU2FStep2Suite) TearDownTest() {
s.mock.Close()
}
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToDefaultURL() {
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeU2F,
RemoteIP: models.NewNullIPFromString("0.0.0.0"),
}))
s.mock.Ctx.Configuration.DefaultRedirectionURL = testRedirectionURL
bodyBytes, err := json.Marshal(signU2FRequestBody{
SignResponse: u2f.SignResponse{},
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{
Redirect: testRedirectionURL,
})
}
func (s *HandlerSignU2FStep2Suite) TestShouldNotReturnRedirectURL() {
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeU2F,
RemoteIP: models.NewNullIPFromString("0.0.0.0"),
}))
bodyBytes, err := json.Marshal(signU2FRequestBody{
SignResponse: u2f.SignResponse{},
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
}
func (s *HandlerSignU2FStep2Suite) TestShouldRedirectUserToSafeTargetURL() {
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeU2F,
RemoteIP: models.NewNullIPFromString("0.0.0.0"),
}))
bodyBytes, err := json.Marshal(signU2FRequestBody{
SignResponse: u2f.SignResponse{},
TargetURL: "https://mydomain.local",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{
Redirect: "https://mydomain.local",
})
}
func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() {
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeU2F,
RemoteIP: models.NewNullIPFromString("0.0.0.0"),
}))
bodyBytes, err := json.Marshal(signU2FRequestBody{
SignResponse: u2f.SignResponse{},
TargetURL: "http://mydomain.local",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
}
func (s *HandlerSignU2FStep2Suite) TestShouldRegenerateSessionForPreventingSessionFixation() {
u2fVerifier := mocks.NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(models.AuthenticationAttempt{
Username: "john",
Successful: true,
Banned: false,
Time: s.mock.Clock.Now(),
Type: regulation.AuthTypeU2F,
RemoteIP: models.NewNullIPFromString("0.0.0.0"),
}))
bodyBytes, err := json.Marshal(signU2FRequestBody{
SignResponse: u2f.SignResponse{},
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
s.Assert().NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
}
func TestRunHandlerSignU2FStep2Suite(t *testing.T) {
suite.Run(t, new(HandlerSignU2FStep2Suite))
}

View File

@ -0,0 +1,204 @@
package handlers
import (
"bytes"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/regulation"
)
// SecondFactorWebauthnAssertionGET handler starts the assertion ceremony.
func SecondFactorWebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
var (
w *webauthn.WebAuthn
user *models.WebauthnUser
err error
)
userSession := ctx.GetSession()
if w, err = newWebauthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
var opts = []webauthn.LoginOption{
webauthn.WithAllowedCredentials(user.WebAuthnCredentialDescriptors()),
}
extensions := make(map[string]interface{})
if user.HasFIDOU2F() {
extensions["appid"] = w.Config.RPOrigin
}
if len(extensions) != 0 {
opts = append(opts, webauthn.WithAssertionExtensions(extensions))
}
var assertion *protocol.CredentialAssertion
if assertion, userSession.Webauthn, err = w.BeginLogin(user, opts...); err != nil {
ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = ctx.SetJSONBody(assertion); err != nil {
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
}
// SecondFactorWebauthnAssertionPOST handler completes the assertion ceremony after verifying the challenge.
func SecondFactorWebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
var (
err error
w *webauthn.WebAuthn
requestBody signWebauthnRequestBody
)
if err = ctx.ParseBody(&requestBody); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession := ctx.GetSession()
if userSession.Webauthn == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle assertion 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)
return
}
if w, err = newWebauthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
var (
assertionResponse *protocol.ParsedCredentialAssertionData
credential *webauthn.Credential
user *models.WebauthnUser
)
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if credential, err = w.ValidateLogin(user, *userSession.Webauthn, assertionResponse); err != nil {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebauthn, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
var found bool
for _, device := range user.Devices {
if bytes.Equal(device.KID.Bytes(), credential.ID) {
device.UpdateSignInInfo(w.Config, ctx.Clock.Now(), credential.Authenticator.SignCount)
found = true
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceSignIn(ctx, device.ID, device.RPID, device.LastUsedAt, device.SignCount, device.CloneWarning); err != nil {
ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
break
}
}
if !found {
ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s' device '%x' count '%d': unable to find device", regulation.AuthTypeWebauthn, userSession.Username, credential.ID, credential.Authenticator.SignCount)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); err != nil {
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeWebauthn, nil); err != nil {
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
userSession.SetTwoFactor(ctx.Clock.Now())
userSession.Webauthn = nil
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
return
}
if userSession.OIDCWorkflowSession != nil {
handleOIDCWorkflowResponse(ctx)
} else {
Handle2FAResponse(ctx, requestBody.TargetURL)
}
}

View File

@ -51,16 +51,16 @@ func TestMethodSetToU2F(t *testing.T) {
}, },
{ {
db: models.UserInfo{ db: models.UserInfo{
Method: "u2f", Method: "webauthn",
HasU2F: true, HasWebauthn: true,
HasTOTP: true, HasTOTP: true,
}, },
err: nil, err: nil,
}, },
{ {
db: models.UserInfo{ db: models.UserInfo{
Method: "u2f", Method: "webauthn",
HasU2F: true, HasWebauthn: true,
HasTOTP: false, HasTOTP: false,
}, },
err: nil, err: nil,
@ -68,7 +68,7 @@ func TestMethodSetToU2F(t *testing.T) {
{ {
db: models.UserInfo{ db: models.UserInfo{
Method: "mobile_push", Method: "mobile_push",
HasU2F: false, HasWebauthn: false,
HasTOTP: false, HasTOTP: false,
}, },
err: nil, err: nil,
@ -116,8 +116,8 @@ func TestMethodSetToU2F(t *testing.T) {
assert.Equal(t, resp.api.Method, actualPreferences.Method) assert.Equal(t, resp.api.Method, actualPreferences.Method)
}) })
t.Run("registered u2f", func(t *testing.T) { t.Run("registered webauthn", func(t *testing.T) {
assert.Equal(t, resp.api.HasU2F, actualPreferences.HasU2F) assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn)
}) })
t.Run("registered totp", func(t *testing.T) { t.Run("registered totp", func(t *testing.T) {
@ -205,14 +205,14 @@ func (s *SaveSuite) TestShouldReturnError500WhenBadMethodProvided() {
MethodPreferencePost(s.mock.Ctx) MethodPreferencePost(s.mock.Ctx)
s.mock.Assert200KO(s.T(), "Operation failed.") s.mock.Assert200KO(s.T(), "Operation failed.")
assert.Equal(s.T(), "unknown method 'abc', it should be one of totp, u2f, mobile_push", s.mock.Hook.LastEntry().Message) assert.Equal(s.T(), "unknown method 'abc', it should be one of totp, webauthn, mobile_push", s.mock.Hook.LastEntry().Message)
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
} }
func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() { func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"webauthn\"}"))
s.mock.StorageMock.EXPECT(). s.mock.StorageMock.EXPECT().
SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("u2f")). SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("webauthn")).
Return(fmt.Errorf("Failure")) Return(fmt.Errorf("Failure"))
MethodPreferencePost(s.mock.Ctx) MethodPreferencePost(s.mock.Ctx)
@ -223,9 +223,9 @@ func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
} }
func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() { func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() {
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"webauthn\"}"))
s.mock.StorageMock.EXPECT(). s.mock.StorageMock.EXPECT().
SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("u2f")). SavePreferred2FAMethod(s.mock.Ctx, gomock.Eq("john"), gomock.Eq("webauthn")).
Return(nil) Return(nil)
MethodPreferencePost(s.mock.Ctx) MethodPreferencePost(s.mock.Ctx)

View File

@ -1,8 +1,6 @@
package handlers package handlers
import ( import (
"github.com/tstranex/u2f"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
) )
@ -14,7 +12,6 @@ type authorizationMatching int
// configurationBody the content returned by the configuration endpoint. // configurationBody the content returned by the configuration endpoint.
type configurationBody struct { type configurationBody struct {
AvailableMethods MethodList `json:"available_methods"` AvailableMethods MethodList `json:"available_methods"`
SecondFactorEnabled bool `json:"second_factor_enabled"` // whether second factor is enabled or not.
} }
// signTOTPRequestBody model of the request body received by TOTP authentication endpoint. // signTOTPRequestBody model of the request body received by TOTP authentication endpoint.
@ -23,9 +20,8 @@ type signTOTPRequestBody struct {
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
} }
// signU2FRequestBody model of the request body of U2F authentication endpoint. // signWebauthnRequestBody model of the request body of Webauthn authentication endpoint.
type signU2FRequestBody struct { type signWebauthnRequestBody struct {
SignResponse u2f.SignResponse `json:"signResponse"`
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
} }

View File

@ -1,32 +0,0 @@
package handlers
import (
"crypto/elliptic"
"github.com/tstranex/u2f"
)
// U2FVerifier is the interface for verifying U2F keys.
type U2FVerifier interface {
Verify(keyHandle []byte, publicKey []byte, signResponse u2f.SignResponse, challenge u2f.Challenge) error
}
// U2FVerifierImpl the production implementation for U2F key verification.
type U2FVerifierImpl struct{}
// Verify verifies U2F keys.
func (uv *U2FVerifierImpl) Verify(keyHandle []byte, publicKey []byte,
signResponse u2f.SignResponse, challenge u2f.Challenge) error {
var registration u2f.Registration
registration.KeyHandle = keyHandle
x, y := elliptic.Unmarshal(elliptic.P256(), publicKey)
registration.PubKey.Curve = elliptic.P256()
registration.PubKey.X = x
registration.PubKey.Y = y
// TODO(c.michaud): store the counter to help detecting cloned U2F keys.
_, err := registration.Authenticate(
signResponse, challenge, 0)
return err
}

View File

@ -0,0 +1,63 @@
package handlers
import (
"fmt"
"net/url"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/session"
)
func getWebAuthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSession) (user *models.WebauthnUser, err error) {
user = &models.WebauthnUser{
Username: userSession.Username,
DisplayName: userSession.DisplayName,
}
if user.DisplayName == "" {
user.DisplayName = user.Username
}
if user.Devices, err = ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username); err != nil {
return nil, err
}
return user, nil
}
func newWebauthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error) {
var (
u *url.URL
)
if u, err = ctx.GetOriginalURL(); err != nil {
return nil, err
}
rpID := u.Hostname()
origin := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
config := &webauthn.Config{
RPDisplayName: ctx.Configuration.Webauthn.DisplayName,
RPID: rpID,
RPOrigin: origin,
RPIcon: "",
AttestationPreference: ctx.Configuration.Webauthn.ConveyancePreference,
AuthenticatorSelection: protocol.AuthenticatorSelection{
AuthenticatorAttachment: protocol.CrossPlatform,
UserVerification: ctx.Configuration.Webauthn.UserVerification,
RequireResidentKey: protocol.ResidentKeyUnrequired(),
},
Timeout: int(ctx.Configuration.Webauthn.Timeout.Milliseconds()),
}
ctx.Logger.Tracef("Creating new Webauthn RP instance with ID %s and Origin %s", config.RPID, config.RPOrigin)
return webauthn.New(config)
}

View File

@ -0,0 +1,167 @@
package handlers
import (
"errors"
"testing"
"github.com/duo-labs/webauthn/protocol"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/session"
)
func TestWebauthnGetUser(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{
Username: "john",
DisplayName: "John Smith",
}
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]models.WebauthnDevice{
{
ID: 1,
RPID: "https://example.com",
Username: "john",
Description: "Primary",
KID: models.NewBase64([]byte("abc123")),
AttestationType: "fido-u2f",
PublicKey: []byte("data"),
SignCount: 0,
CloneWarning: false,
},
{
ID: 2,
RPID: "example.com",
Username: "john",
Description: "Secondary",
KID: models.NewBase64([]byte("123abc")),
AttestationType: "packed",
Transport: "usb,nfc",
PublicKey: []byte("data"),
SignCount: 100,
CloneWarning: false,
},
}, nil)
user, err := getWebAuthnUser(ctx.Ctx, userSession)
require.NoError(t, err)
require.NotNil(t, user)
assert.Equal(t, []byte("john"), user.WebAuthnID())
assert.Equal(t, "john", user.WebAuthnName())
assert.Equal(t, "john", user.Username)
assert.Equal(t, "", user.WebAuthnIcon())
assert.Equal(t, "John Smith", user.WebAuthnDisplayName())
assert.Equal(t, "John Smith", user.DisplayName)
require.Len(t, user.Devices, 2)
assert.Equal(t, 1, user.Devices[0].ID)
assert.Equal(t, "https://example.com", user.Devices[0].RPID)
assert.Equal(t, "john", user.Devices[0].Username)
assert.Equal(t, "Primary", user.Devices[0].Description)
assert.Equal(t, "", user.Devices[0].Transport)
assert.Equal(t, "fido-u2f", user.Devices[0].AttestationType)
assert.Equal(t, []byte("data"), user.Devices[0].PublicKey)
assert.Equal(t, uint32(0), user.Devices[0].SignCount)
assert.False(t, user.Devices[0].CloneWarning)
descriptors := user.WebAuthnCredentialDescriptors()
assert.Equal(t, "fido-u2f", descriptors[0].AttestationType)
assert.Equal(t, []byte("abc123"), descriptors[0].CredentialID)
assert.Equal(t, protocol.PublicKeyCredentialType, descriptors[0].Type)
assert.Len(t, descriptors[0].Transport, 0)
assert.Equal(t, 2, user.Devices[1].ID)
assert.Equal(t, "example.com", user.Devices[1].RPID)
assert.Equal(t, "john", user.Devices[1].Username)
assert.Equal(t, "Secondary", user.Devices[1].Description)
assert.Equal(t, "usb,nfc", user.Devices[1].Transport)
assert.Equal(t, "packed", user.Devices[1].AttestationType)
assert.Equal(t, []byte("data"), user.Devices[1].PublicKey)
assert.Equal(t, uint32(100), user.Devices[1].SignCount)
assert.False(t, user.Devices[1].CloneWarning)
assert.Equal(t, "packed", descriptors[1].AttestationType)
assert.Equal(t, []byte("123abc"), descriptors[1].CredentialID)
assert.Equal(t, protocol.PublicKeyCredentialType, descriptors[1].Type)
assert.Len(t, descriptors[1].Transport, 2)
assert.Equal(t, protocol.AuthenticatorTransport("usb"), descriptors[1].Transport[0])
assert.Equal(t, protocol.AuthenticatorTransport("nfc"), descriptors[1].Transport[1])
}
func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{
Username: "john",
}
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]models.WebauthnDevice{
{
ID: 1,
RPID: "https://example.com",
Username: "john",
Description: "Primary",
KID: models.NewBase64([]byte("abc123")),
AttestationType: "fido-u2f",
PublicKey: []byte("data"),
SignCount: 0,
CloneWarning: false,
},
}, nil)
user, err := getWebAuthnUser(ctx.Ctx, userSession)
require.NoError(t, err)
require.NotNil(t, user)
assert.Equal(t, "john", user.WebAuthnDisplayName())
assert.Equal(t, "john", user.DisplayName)
}
func TestWebauthnGetUserWithErr(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{
Username: "john",
}
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return(nil, errors.New("not found"))
user, err := getWebAuthnUser(ctx.Ctx, userSession)
assert.EqualError(t, err, "not found")
assert.Nil(t, user)
}
func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t)
w, err := newWebauthn(ctx.Ctx)
assert.Nil(t, w)
assert.EqualError(t, err, "Missing header X-Forwarded-Host")
}
func TestWebauthnNewWebauthnShouldReturnErrWhenWebauthnNotConfigured(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t)
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-Proto", "https")
w, err := newWebauthn(ctx.Ctx)
assert.Nil(t, w)
assert.EqualError(t, err, "Configuration error: Missing RPDisplayName")
}

View File

@ -5,8 +5,10 @@ import (
"io" "io"
"log" "log"
"os" "os"
"runtime"
"testing" "testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -81,3 +83,33 @@ func TestShouldFormatLogsAsJSON(t *testing.T) {
assert.Contains(t, string(b), "{\"level\":\"info\",\"msg\":\"This is a test\",") assert.Contains(t, string(b), "{\"level\":\"info\",\"msg\":\"This is a test\",")
} }
func TestShouldRaiseErrorOnInvalidFile(t *testing.T) {
err := InitializeLogger(schema.LogConfiguration{FilePath: "/not/a/valid/path/to.log"}, false)
switch runtime.GOOS {
case "windows":
assert.EqualError(t, err, "open /not/a/valid/path/to.log: The system cannot find the path specified.")
default:
assert.EqualError(t, err, "open /not/a/valid/path/to.log: no such file or directory")
}
}
func TestSetLevels(t *testing.T) {
assert.Equal(t, logrus.InfoLevel, logrus.GetLevel())
setLevelStr("error", false)
assert.Equal(t, logrus.ErrorLevel, logrus.GetLevel())
setLevelStr("warn", false)
assert.Equal(t, logrus.WarnLevel, logrus.GetLevel())
setLevelStr("info", false)
assert.Equal(t, logrus.InfoLevel, logrus.GetLevel())
setLevelStr("debug", false)
assert.Equal(t, logrus.DebugLevel, logrus.GetLevel())
setLevelStr("trace", false)
assert.Equal(t, logrus.TraceLevel, logrus.GetLevel())
}

View File

@ -6,6 +6,5 @@ package mocks
//go:generate mockgen -package mocks -destination user_provider.go -mock_names UserProvider=MockUserProvider github.com/authelia/authelia/v4/internal/authentication UserProvider //go:generate mockgen -package mocks -destination user_provider.go -mock_names UserProvider=MockUserProvider github.com/authelia/authelia/v4/internal/authentication UserProvider
//go:generate mockgen -package mocks -destination notifier.go -mock_names Notifier=MockNotifier github.com/authelia/authelia/v4/internal/notification Notifier //go:generate mockgen -package mocks -destination notifier.go -mock_names Notifier=MockNotifier github.com/authelia/authelia/v4/internal/notification Notifier
//go:generate mockgen -package mocks -destination totp.go -mock_names Provider=MockTOTP github.com/authelia/authelia/v4/internal/totp Provider //go:generate mockgen -package mocks -destination totp.go -mock_names Provider=MockTOTP github.com/authelia/authelia/v4/internal/totp Provider
//go:generate mockgen -package mocks -destination u2f_verifier.go -mock_names U2FVerifier=MockU2FVerifier github.com/authelia/authelia/v4/internal/handlers U2FVerifier
//go:generate mockgen -package mocks -destination storage.go -mock_names Provider=MockStorage github.com/authelia/authelia/v4/internal/storage Provider //go:generate mockgen -package mocks -destination storage.go -mock_names Provider=MockStorage github.com/authelia/authelia/v4/internal/storage Provider
//go:generate mockgen -package mocks -destination duo_api.go -mock_names API=MockAPI github.com/authelia/authelia/v4/internal/duo API //go:generate mockgen -package mocks -destination duo_api.go -mock_names API=MockAPI github.com/authelia/authelia/v4/internal/duo API

View File

@ -197,36 +197,6 @@ func (mr *MockStorageMockRecorder) LoadTOTPConfigurations(arg0, arg1, arg2 inter
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfigurations", reflect.TypeOf((*MockStorage)(nil).LoadTOTPConfigurations), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfigurations", reflect.TypeOf((*MockStorage)(nil).LoadTOTPConfigurations), arg0, arg1, arg2)
} }
// LoadU2FDevice mocks base method.
func (m *MockStorage) LoadU2FDevice(arg0 context.Context, arg1 string) (*models.U2FDevice, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadU2FDevice", arg0, arg1)
ret0, _ := ret[0].(*models.U2FDevice)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadU2FDevice indicates an expected call of LoadU2FDevice.
func (mr *MockStorageMockRecorder) LoadU2FDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadU2FDevice", reflect.TypeOf((*MockStorage)(nil).LoadU2FDevice), arg0, arg1)
}
// LoadU2FDevices mocks base method.
func (m *MockStorage) LoadU2FDevices(arg0 context.Context, arg1, arg2 int) ([]models.U2FDevice, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadU2FDevices", arg0, arg1, arg2)
ret0, _ := ret[0].([]models.U2FDevice)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadU2FDevices indicates an expected call of LoadU2FDevices.
func (mr *MockStorageMockRecorder) LoadU2FDevices(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadU2FDevices", reflect.TypeOf((*MockStorage)(nil).LoadU2FDevices), arg0, arg1, arg2)
}
// LoadUserInfo mocks base method. // LoadUserInfo mocks base method.
func (m *MockStorage) LoadUserInfo(arg0 context.Context, arg1 string) (models.UserInfo, error) { func (m *MockStorage) LoadUserInfo(arg0 context.Context, arg1 string) (models.UserInfo, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -242,6 +212,36 @@ func (mr *MockStorageMockRecorder) LoadUserInfo(arg0, arg1 interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserInfo", reflect.TypeOf((*MockStorage)(nil).LoadUserInfo), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserInfo", reflect.TypeOf((*MockStorage)(nil).LoadUserInfo), arg0, arg1)
} }
// LoadWebauthnDevices mocks base method.
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]models.WebauthnDevice, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDevices", arg0, arg1, arg2)
ret0, _ := ret[0].([]models.WebauthnDevice)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadWebauthnDevices indicates an expected call of LoadWebauthnDevices.
func (mr *MockStorageMockRecorder) LoadWebauthnDevices(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevices", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevices), arg0, arg1, arg2)
}
// LoadWebauthnDevicesByUsername mocks base method.
func (m *MockStorage) LoadWebauthnDevicesByUsername(arg0 context.Context, arg1 string) ([]models.WebauthnDevice, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDevicesByUsername", arg0, arg1)
ret0, _ := ret[0].([]models.WebauthnDevice)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadWebauthnDevicesByUsername indicates an expected call of LoadWebauthnDevicesByUsername.
func (mr *MockStorageMockRecorder) LoadWebauthnDevicesByUsername(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevicesByUsername", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevicesByUsername), arg0, arg1)
}
// SaveIdentityVerification mocks base method. // SaveIdentityVerification mocks base method.
func (m *MockStorage) SaveIdentityVerification(arg0 context.Context, arg1 models.IdentityVerification) error { func (m *MockStorage) SaveIdentityVerification(arg0 context.Context, arg1 models.IdentityVerification) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -298,18 +298,18 @@ func (mr *MockStorageMockRecorder) SaveTOTPConfiguration(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).SaveTOTPConfiguration), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).SaveTOTPConfiguration), arg0, arg1)
} }
// SaveU2FDevice mocks base method. // SaveWebauthnDevice mocks base method.
func (m *MockStorage) SaveU2FDevice(arg0 context.Context, arg1 models.U2FDevice) error { func (m *MockStorage) SaveWebauthnDevice(arg0 context.Context, arg1 models.WebauthnDevice) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveU2FDevice", arg0, arg1) ret := m.ctrl.Call(m, "SaveWebauthnDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// SaveU2FDevice indicates an expected call of SaveU2FDevice. // SaveWebauthnDevice indicates an expected call of SaveWebauthnDevice.
func (mr *MockStorageMockRecorder) SaveU2FDevice(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, "SaveU2FDevice", reflect.TypeOf((*MockStorage)(nil).SaveU2FDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveWebauthnDevice", reflect.TypeOf((*MockStorage)(nil).SaveWebauthnDevice), arg0, arg1)
} }
// SchemaEncryptionChangeKey mocks base method. // SchemaEncryptionChangeKey mocks base method.
@ -457,3 +457,31 @@ func (mr *MockStorageMockRecorder) StartupCheck() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockStorage)(nil).StartupCheck)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockStorage)(nil).StartupCheck))
} }
// UpdateTOTPConfigurationSignIn mocks base method.
func (m *MockStorage) UpdateTOTPConfigurationSignIn(arg0 context.Context, arg1 int, arg2 *time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSignIn", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateTOTPConfigurationSignIn indicates an expected call of UpdateTOTPConfigurationSignIn.
func (mr *MockStorageMockRecorder) UpdateTOTPConfigurationSignIn(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateTOTPConfigurationSignIn), arg0, arg1, arg2)
}
// UpdateWebauthnDeviceSignIn mocks base method.
func (m *MockStorage) UpdateWebauthnDeviceSignIn(arg0 context.Context, arg1 int, arg2 string, arg3 *time.Time, arg4 uint32, arg5 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1, arg2, arg3, arg4, arg5)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateWebauthnDeviceSignIn indicates an expected call of UpdateWebauthnDeviceSignIn.
func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceSignIn(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebauthnDeviceSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateWebauthnDeviceSignIn), arg0, arg1, arg2, arg3, arg4, arg5)
}

View File

@ -1,49 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/authelia/authelia/v4/internal/handlers (interfaces: U2FVerifier)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
u2f "github.com/tstranex/u2f"
)
// MockU2FVerifier is a mock of U2FVerifier interface.
type MockU2FVerifier struct {
ctrl *gomock.Controller
recorder *MockU2FVerifierMockRecorder
}
// MockU2FVerifierMockRecorder is the mock recorder for MockU2FVerifier.
type MockU2FVerifierMockRecorder struct {
mock *MockU2FVerifier
}
// NewMockU2FVerifier creates a new mock instance.
func NewMockU2FVerifier(ctrl *gomock.Controller) *MockU2FVerifier {
mock := &MockU2FVerifier{ctrl: ctrl}
mock.recorder = &MockU2FVerifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockU2FVerifier) EXPECT() *MockU2FVerifierMockRecorder {
return m.recorder
}
// Verify mocks base method.
func (m *MockU2FVerifier) Verify(arg0, arg1 []byte, arg2 u2f.SignResponse, arg3 u2f.Challenge) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
return ret0
}
// Verify indicates an expected call of Verify.
func (mr *MockU2FVerifierMockRecorder) Verify(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockU2FVerifier)(nil).Verify), arg0, arg1, arg2, arg3)
}

8
internal/models/const.go Normal file
View File

@ -0,0 +1,8 @@
package models
const (
errFmtValueNil = "cannot value model type '%T' with value nil to driver.Value"
errFmtScanNil = "cannot scan model type '%T' from value nil: type doesn't support nil values"
errFmtScanInvalidType = "cannot scan model type '%T' from type '%T' with value '%v'"
errFmtScanInvalidTypeErr = "cannot scan model type '%T' from type '%T' with value '%v': %w"
)

View File

@ -4,6 +4,7 @@ import (
"image" "image"
"net/url" "net/url"
"strconv" "strconv"
"time"
"github.com/pquerna/otp" "github.com/pquerna/otp"
) )
@ -11,6 +12,8 @@ import (
// TOTPConfiguration represents a users TOTP configuration row in the database. // TOTPConfiguration represents a users TOTP configuration row in the database.
type TOTPConfiguration struct { type TOTPConfiguration struct {
ID int `db:"id" json:"-"` ID int `db:"id" json:"-"`
CreatedAt time.Time `db:"created_at" json:"-"`
LastUsedAt *time.Time `db:"last_used_at" json:"-"`
Username string `db:"username" json:"-"` Username string `db:"username" json:"-"`
Issuer string `db:"issuer" json:"-"` Issuer string `db:"issuer" json:"-"`
Algorithm string `db:"algorithm" json:"-"` Algorithm string `db:"algorithm" json:"-"`
@ -38,6 +41,11 @@ func (c TOTPConfiguration) URI() (uri string) {
return u.String() return u.String()
} }
// UpdateSignInInfo adjusts the values of the TOTPConfiguration after a sign in.
func (c *TOTPConfiguration) UpdateSignInInfo(now time.Time) {
c.LastUsedAt = &now
}
// Key returns the *otp.Key using TOTPConfiguration.URI with otp.NewKeyFromURL. // Key returns the *otp.Key using TOTPConfiguration.URI with otp.NewKeyFromURL.
func (c TOTPConfiguration) Key() (key *otp.Key, err error) { func (c TOTPConfiguration) Key() (key *otp.Key, err error) {
return otp.NewKeyFromURL(c.URI()) return otp.NewKeyFromURL(c.URI())

View File

@ -2,7 +2,7 @@ package models
import ( import (
"database/sql/driver" "database/sql/driver"
"errors" "encoding/base64"
"fmt" "fmt"
"net" "net"
) )
@ -26,6 +26,11 @@ func NewNullIPFromString(value string) (ip NullIP) {
return NullIP{IP: net.ParseIP(value)} return NullIP{IP: net.ParseIP(value)}
} }
// NewBase64 returns a new Base64.
func NewBase64(data []byte) Base64 {
return Base64{data: data}
}
// IP is a type specific for storage of a net.IP in the database which can't be NULL. // IP is a type specific for storage of a net.IP in the database which can't be NULL.
type IP struct { type IP struct {
IP net.IP IP net.IP
@ -34,16 +39,16 @@ type IP struct {
// Value is the IP implementation of the databases/sql driver.Valuer. // Value is the IP implementation of the databases/sql driver.Valuer.
func (ip IP) Value() (value driver.Value, err error) { func (ip IP) Value() (value driver.Value, err error) {
if ip.IP == nil { if ip.IP == nil {
return nil, errors.New("cannot value nil IP to driver.Value") return nil, fmt.Errorf(errFmtValueNil, ip)
} }
return driver.Value(ip.IP.String()), nil return ip.IP.String(), nil
} }
// Scan is the IP implementation of the sql.Scanner. // Scan is the IP implementation of the sql.Scanner.
func (ip *IP) Scan(src interface{}) (err error) { func (ip *IP) Scan(src interface{}) (err error) {
if src == nil { if src == nil {
return errors.New("cannot scan nil to type IP") return fmt.Errorf(errFmtScanNil, ip)
} }
var value string var value string
@ -54,7 +59,7 @@ func (ip *IP) Scan(src interface{}) (err error) {
case []byte: case []byte:
value = string(v) value = string(v)
default: default:
return fmt.Errorf("invalid type %T for IP %v", src, src) return fmt.Errorf(errFmtScanInvalidType, ip, src, src)
} }
ip.IP = net.ParseIP(value) ip.IP = net.ParseIP(value)
@ -70,10 +75,10 @@ type NullIP struct {
// Value is the NullIP implementation of the databases/sql driver.Valuer. // Value is the NullIP implementation of the databases/sql driver.Valuer.
func (ip NullIP) Value() (value driver.Value, err error) { func (ip NullIP) Value() (value driver.Value, err error) {
if ip.IP == nil { if ip.IP == nil {
return driver.Value(nil), nil return nil, nil
} }
return driver.Value(ip.IP.String()), nil return ip.IP.String(), nil
} }
// Scan is the NullIP implementation of the sql.Scanner. // Scan is the NullIP implementation of the sql.Scanner.
@ -91,7 +96,7 @@ func (ip *NullIP) Scan(src interface{}) (err error) {
case []byte: case []byte:
value = string(v) value = string(v)
default: default:
return fmt.Errorf("invalid type %T for NullIP %v", src, src) return fmt.Errorf(errFmtScanInvalidType, ip, src, src)
} }
ip.IP = net.ParseIP(value) ip.IP = net.ParseIP(value)
@ -99,6 +104,48 @@ func (ip *NullIP) Scan(src interface{}) (err error) {
return nil return nil
} }
// Base64 saves bytes to the database as a base64 encoded string.
type Base64 struct {
data []byte
}
// String returns the Base64 string encoded as base64.
func (b Base64) String() string {
return base64.StdEncoding.EncodeToString(b.data)
}
// Bytes returns the Base64 string encoded as bytes.
func (b Base64) Bytes() []byte {
return b.data
}
// Value is the Base64 implementation of the databases/sql driver.Valuer.
func (b Base64) Value() (value driver.Value, err error) {
return b.String(), nil
}
// Scan is the Base64 implementation of the sql.Scanner.
func (b *Base64) Scan(src interface{}) (err error) {
if src == nil {
return fmt.Errorf(errFmtScanNil, b)
}
switch v := src.(type) {
case string:
if b.data, err = base64.StdEncoding.DecodeString(v); err != nil {
return fmt.Errorf(errFmtScanInvalidTypeErr, b, src, src, err)
}
case []byte:
if b.data, err = base64.StdEncoding.DecodeString(string(v)); err != nil {
b.data = v
}
default:
return fmt.Errorf(errFmtScanInvalidType, b, src, src)
}
return nil
}
// StartupCheck represents a provider that has a startup check. // StartupCheck represents a provider that has a startup check.
type StartupCheck interface { type StartupCheck interface {
StartupCheck() (err error) StartupCheck() (err error)

View File

@ -0,0 +1,109 @@
package models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDatabaseModelTypeIP(t *testing.T) {
ip := IP{}
value, err := ip.Value()
assert.Nil(t, value)
assert.EqualError(t, err, "cannot value model type 'models.IP' with value nil to driver.Value")
err = ip.Scan("192.168.2.0")
assert.NoError(t, err)
assert.True(t, ip.IP.IsPrivate())
assert.False(t, ip.IP.IsLoopback())
assert.Equal(t, "192.168.2.0", ip.IP.String())
value, err = ip.Value()
assert.NoError(t, err)
assert.Equal(t, "192.168.2.0", value)
err = ip.Scan([]byte("127.0.0.0"))
assert.NoError(t, err)
assert.False(t, ip.IP.IsPrivate())
assert.True(t, ip.IP.IsLoopback())
assert.Equal(t, "127.0.0.0", ip.IP.String())
err = ip.Scan(1)
assert.EqualError(t, err, "cannot scan model type '*models.IP' from type 'int' with value '1'")
err = ip.Scan(nil)
assert.EqualError(t, err, "cannot scan model type '*models.IP' from value nil: type doesn't support nil values")
}
func TestDatabaseModelTypeNullIP(t *testing.T) {
ip := NullIP{}
value, err := ip.Value()
assert.Nil(t, value)
assert.NoError(t, err)
err = ip.Scan("192.168.2.0")
assert.NoError(t, err)
assert.True(t, ip.IP.IsPrivate())
assert.False(t, ip.IP.IsLoopback())
assert.Equal(t, "192.168.2.0", ip.IP.String())
value, err = ip.Value()
assert.NoError(t, err)
assert.Equal(t, "192.168.2.0", value)
err = ip.Scan([]byte("127.0.0.0"))
assert.NoError(t, err)
assert.False(t, ip.IP.IsPrivate())
assert.True(t, ip.IP.IsLoopback())
assert.Equal(t, "127.0.0.0", ip.IP.String())
err = ip.Scan(1)
assert.EqualError(t, err, "cannot scan model type '*models.NullIP' from type 'int' with value '1'")
err = ip.Scan(nil)
assert.NoError(t, err)
}
func TestDatabaseModelTypeBase64(t *testing.T) {
b64 := Base64{}
value, err := b64.Value()
assert.Equal(t, "", value)
assert.NoError(t, err)
assert.Nil(t, b64.Bytes())
err = b64.Scan(nil)
assert.EqualError(t, err, "cannot scan model type '*models.Base64' from value nil: type doesn't support nil values")
err = b64.Scan("###")
assert.EqualError(t, err, "cannot scan model type '*models.Base64' from type 'string' with value '###': illegal base64 data at input byte 0")
err = b64.Scan(1)
assert.EqualError(t, err, "cannot scan model type '*models.Base64' from type 'int' with value '1'")
err = b64.Scan("YXV0aGVsaWE=")
assert.NoError(t, err)
assert.Equal(t, []byte("authelia"), b64.Bytes())
assert.Equal(t, "YXV0aGVsaWE=", b64.String())
err = b64.Scan([]byte("c2VjdXJpdHk="))
assert.NoError(t, err)
assert.Equal(t, []byte("security"), b64.Bytes())
assert.Equal(t, "c2VjdXJpdHk=", b64.String())
err = b64.Scan([]byte("###"))
assert.NoError(t, err)
assert.Equal(t, []byte("###"), b64.Bytes())
assert.Equal(t, "IyMj", b64.String())
}

View File

@ -11,8 +11,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 security key has been registered. // True if a Webauthn device has been registered.
HasU2F bool `db:"has_u2f" json:"has_u2f" 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"`

167
internal/models/webauthn.go Normal file
View File

@ -0,0 +1,167 @@
package models
import (
"encoding/hex"
"strings"
"time"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
"github.com/google/uuid"
)
const (
attestationTypeFIDOU2F = "fido-u2f"
)
// WebauthnUser is an object to represent a user for the Webauthn lib.
type WebauthnUser struct {
Username string
DisplayName string
Devices []WebauthnDevice
}
// HasFIDOU2F returns true if the user has any attestation type `fido-u2f` devices.
func (w WebauthnUser) HasFIDOU2F() bool {
for _, c := range w.Devices {
if c.AttestationType == attestationTypeFIDOU2F {
return true
}
}
return false
}
// WebAuthnID implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnID() []byte {
return []byte(w.Username)
}
// WebAuthnName implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnName() string {
return w.Username
}
// WebAuthnDisplayName implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnDisplayName() string {
return w.DisplayName
}
// WebAuthnIcon implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnIcon() string {
return ""
}
// WebAuthnCredentials implements the webauthn.User interface.
func (w WebauthnUser) WebAuthnCredentials() (credentials []webauthn.Credential) {
credentials = make([]webauthn.Credential, len(w.Devices))
var credential webauthn.Credential
for i, device := range w.Devices {
aaguid, err := device.AAGUID.MarshalBinary()
if err != nil {
continue
}
credential = webauthn.Credential{
ID: device.KID.Bytes(),
PublicKey: device.PublicKey,
AttestationType: device.AttestationType,
Authenticator: webauthn.Authenticator{
AAGUID: aaguid,
SignCount: device.SignCount,
CloneWarning: device.CloneWarning,
},
}
transports := strings.Split(device.Transport, ",")
credential.Transport = []protocol.AuthenticatorTransport{}
for _, t := range transports {
if t == "" {
continue
}
credential.Transport = append(credential.Transport, protocol.AuthenticatorTransport(t))
}
credentials[i] = credential
}
return credentials
}
// WebAuthnCredentialDescriptors decodes the users credentials into protocol.CredentialDescriptor's.
func (w WebauthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) {
credentials := w.WebAuthnCredentials()
descriptors = make([]protocol.CredentialDescriptor, len(credentials))
for i, credential := range credentials {
descriptors[i] = credential.Descriptor()
}
return descriptors
}
// NewWebauthnDeviceFromCredential creates a WebauthnDevice from a webauthn.Credential.
func NewWebauthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebauthnDevice) {
transport := make([]string, len(credential.Transport))
for i, t := range credential.Transport {
transport[i] = string(t)
}
device = WebauthnDevice{
RPID: rpid,
Username: username,
CreatedAt: time.Now(),
Description: description,
KID: NewBase64(credential.ID),
PublicKey: credential.PublicKey,
AttestationType: credential.AttestationType,
SignCount: credential.Authenticator.SignCount,
CloneWarning: credential.Authenticator.CloneWarning,
Transport: strings.Join(transport, ","),
}
device.AAGUID, _ = uuid.Parse(hex.EncodeToString(credential.Authenticator.AAGUID))
return device
}
// WebauthnDevice represents a Webauthn Device in the database storage.
type WebauthnDevice struct {
ID int `db:"id"`
CreatedAt time.Time `db:"created_at"`
LastUsedAt *time.Time `db:"last_used_at"`
RPID string `db:"rpid"`
Username string `db:"username"`
Description string `db:"description"`
KID Base64 `db:"kid"`
PublicKey []byte `db:"public_key"`
AttestationType string `db:"attestation_type"`
Transport string `db:"transport"`
AAGUID uuid.UUID `db:"aaguid"`
SignCount uint32 `db:"sign_count"`
CloneWarning bool `db:"clone_warning"`
}
// UpdateSignInInfo adjusts the values of the WebauthnDevice after a sign in.
func (w *WebauthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
w.LastUsedAt = &now
w.SignCount = signCount
if w.RPID != "" {
return
}
switch w.AttestationType {
case attestationTypeFIDOU2F:
w.RPID = config.RPOrigin
default:
w.RPID = config.RPID
}
}

View File

@ -12,11 +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"
// AuthTypeU2F is the string representing an auth log for second-factor authentication via FIDO/CTAP1/U2F. // AuthTypeWebauthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn.
AuthTypeU2F = "U2F" AuthTypeWebauthn = "Webauthn"
// AuthTypeWebAuthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn.
// TODO: Add WebAuthn.
// AuthTypeDuo is the string representing an auth log for second-factor authentication via DUO. // AuthTypeDuo is the string representing an auth log for second-factor authentication via DUO.
AuthTypeDuo = "Duo" AuthTypeDuo = "Duo"

View File

@ -89,31 +89,34 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
middlewares.RequireFirstFactor(handlers.UserInfoGet))) middlewares.RequireFirstFactor(handlers.UserInfoGet)))
r.POST("/api/user/info/2fa_method", autheliaMiddleware( r.POST("/api/user/info/2fa_method", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.MethodPreferencePost))) middlewares.RequireFirstFactor(handlers.MethodPreferencePost)))
if !configuration.TOTP.Disable {
// TOTP related endpoints.
r.GET("/api/user/info/totp", autheliaMiddleware( r.GET("/api/user/info/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.UserTOTPGet))) middlewares.RequireFirstFactor(handlers.UserTOTPGet)))
// TOTP related endpoints.
r.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware( r.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityStart))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityStart)))
r.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware( r.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
r.POST("/api/secondfactor/totp", autheliaMiddleware( r.POST("/api/secondfactor/totp", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost))) middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost)))
}
// U2F related endpoints. if !configuration.Webauthn.Disable {
r.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware( // Webauthn Endpoints.
middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityStart))) r.POST("/api/secondfactor/webauthn/identity/start", autheliaMiddleware(
r.POST("/api/secondfactor/u2f/identity/finish", autheliaMiddleware( middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnIdentityStart)))
middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityFinish))) r.POST("/api/secondfactor/webauthn/identity/finish", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnIdentityFinish)))
r.POST("/api/secondfactor/webauthn/attestation", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnAttestationPOST)))
r.POST("/api/secondfactor/u2f/register", autheliaMiddleware( r.GET("/api/secondfactor/webauthn/assertion", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorU2FRegister))) middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnAssertionGET)))
r.POST("/api/secondfactor/webauthn/assertion", autheliaMiddleware(
r.POST("/api/secondfactor/u2f/sign_request", autheliaMiddleware( middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnAssertionPOST)))
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignGet))) }
r.POST("/api/secondfactor/u2f/sign", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignPost(&handlers.U2FVerifierImpl{}))))
// Configure DUO api endpoint only if configuration exists. // Configure DUO api endpoint only if configuration exists.
if configuration.DuoAPI != nil { if configuration.DuoAPI != nil {

View File

@ -4,10 +4,10 @@ import (
"context" "context"
"time" "time"
"github.com/duo-labs/webauthn/webauthn"
"github.com/fasthttp/session/v2" "github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/redis" "github.com/fasthttp/session/v2/providers/redis"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/tstranex/u2f"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/authorization"
@ -22,12 +22,6 @@ type ProviderConfig struct {
providerName string providerName string
} }
// U2FRegistration is a serializable version of a U2F registration.
type U2FRegistration struct {
KeyHandle []byte
PublicKey []byte
}
// UserSession is the structure representing the session of a user. // UserSession is the structure representing the session of a user.
type UserSession struct { type UserSession struct {
Username string Username string
@ -43,12 +37,8 @@ type UserSession struct {
FirstFactorAuthnTimestamp int64 FirstFactorAuthnTimestamp int64
SecondFactorAuthnTimestamp int64 SecondFactorAuthnTimestamp int64
// The challenge generated in first step of U2F registration (after identity verification) or authentication. // Webauthn holds the session registration data for this session.
// This is used reused in the second phase to check that the challenge has been completed. Webauthn *webauthn.SessionData
U2FChallenge *u2f.Challenge
// The registration representing a U2F device in DB set after identity verification.
// This is used in second phase of a U2F authentication.
U2FRegistration *U2FRegistration
// Represent an OIDC workflow session initiated by the client if not null. // Represent an OIDC workflow session initiated by the client if not null.
OIDCWorkflowSession *OIDCWorkflowSession OIDCWorkflowSession *OIDCWorkflowSession

View File

@ -8,7 +8,7 @@ const (
tableUserPreferences = "user_preferences" tableUserPreferences = "user_preferences"
tableIdentityVerification = "identity_verification" tableIdentityVerification = "identity_verification"
tableTOTPConfigurations = "totp_configurations" tableTOTPConfigurations = "totp_configurations"
tableU2FDevices = "u2f_devices" tableWebauthnDevices = "webauthn_devices"
tableDuoDevices = "duo_devices" tableDuoDevices = "duo_devices"
tableAuthenticationLogs = "authentication_logs" tableAuthenticationLogs = "authentication_logs"
tableMigrations = "migrations" tableMigrations = "migrations"
@ -25,6 +25,7 @@ const (
const ( const (
tablePre1TOTPSecrets = "totp_secrets" tablePre1TOTPSecrets = "totp_secrets"
tablePre1IdentityVerificationTokens = "identity_verification_tokens" tablePre1IdentityVerificationTokens = "identity_verification_tokens"
tablePre1U2FDevices = "u2f_devices"
tablePre1Config = "config" tablePre1Config = "config"
@ -40,9 +41,9 @@ const (
var tablesPre1 = []string{ var tablesPre1 = []string{
tablePre1TOTPSecrets, tablePre1TOTPSecrets,
tablePre1IdentityVerificationTokens, tablePre1IdentityVerificationTokens,
tablePre1U2FDevices,
tableUserPreferences, tableUserPreferences,
tableU2FDevices,
tableAuthenticationLogs, tableAuthenticationLogs,
} }
@ -55,7 +56,7 @@ const (
const ( const (
// This is the latest schema version for the purpose of tests. // This is the latest schema version for the purpose of tests.
testLatestVersion = 1 testLatestVersion = 2
) )
const ( const (

View File

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

View File

@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS authentication_logs (
username VARCHAR(100) NOT NULL, username VARCHAR(100) NOT NULL,
auth_type VARCHAR(8) NOT NULL DEFAULT '1FA', auth_type VARCHAR(8) NOT NULL DEFAULT '1FA',
remote_ip VARCHAR(39) NULL DEFAULT NULL, remote_ip VARCHAR(39) NULL DEFAULT NULL,
request_uri TEXT NOT NULL, request_uri TEXT,
request_method VARCHAR(8) NOT NULL DEFAULT '', request_method VARCHAR(8) NOT NULL DEFAULT '',
PRIMARY KEY (id) PRIMARY KEY (id)
); );

View File

@ -0,0 +1,37 @@
ALTER TABLE totp_configurations RENAME _bkp_DOWN_V0002_totp_configurations;
ALTER TABLE webauthn_devices RENAME _bkp_DOWN_V0002_webauthn_devices;
CREATE TABLE IF NOT EXISTS totp_configurations (
id INTEGER AUTO_INCREMENT,
username VARCHAR(100) NOT NULL,
issuer VARCHAR(100),
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
period INTEGER NOT NULL DEFAULT 30,
secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (username)
);
INSERT INTO totp_configurations (id, username, issuer, algorithm, digits, period, secret)
SELECT id, username, issuer, algorithm, digits, period, secret
FROM _bkp_DOWN_V0002_totp_configurations;
CREATE TABLE IF NOT EXISTS u2f_devices (
id INTEGER AUTO_INCREMENT,
username VARCHAR(100) NOT NULL,
description VARCHAR(30) NOT NULL DEFAULT 'Primary',
key_handle BLOB NOT NULL,
public_key BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (username, description)
);
INSERT INTO u2f_devices (id, username, description, key_handle, public_key)
SELECT id, username, description, FROM_BASE64(kid), public_key
FROM _bkp_DOWN_V0002_webauthn_devices
WHERE attestation_type = 'fido-u2f';
UPDATE user_preferences
SET second_factor_method = 'u2f'
WHERE second_factor_method = 'webauthn';

View File

@ -0,0 +1,47 @@
ALTER TABLE totp_configurations RENAME _bkp_UP_V0002_totp_configurations;
ALTER TABLE u2f_devices RENAME _bkp_UP_V0002_u2f_devices;
CREATE TABLE IF NOT EXISTS totp_configurations (
id INTEGER AUTO_INCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NULL DEFAULT NULL,
username VARCHAR(100) NOT NULL,
issuer VARCHAR(100),
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
period INTEGER NOT NULL DEFAULT 30,
secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (username)
);
INSERT INTO totp_configurations (id, username, issuer, algorithm, digits, period, secret)
SELECT id, username, issuer, algorithm, digits, period, secret
FROM _bkp_UP_V0002_totp_configurations;
CREATE TABLE IF NOT EXISTS webauthn_devices (
id INTEGER AUTO_INCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NULL DEFAULT NULL,
rpid TEXT,
username VARCHAR(100) NOT NULL,
description VARCHAR(30) NOT NULL DEFAULT 'Primary',
kid VARCHAR(100) NOT NULL,
public_key BLOB NOT NULL,
attestation_type VARCHAR(32),
transport VARCHAR(20) DEFAULT '',
aaguid CHAR(36) NOT NULL,
sign_count INTEGER DEFAULT 0,
clone_warning BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
UNIQUE KEY (username, description),
UNIQUE KEY (kid)
);
INSERT INTO webauthn_devices (id, rpid, username, description, kid, public_key, attestation_type, aaguid, sign_count)
SELECT id, '', username, description, TO_BASE64(key_handle), public_key, 'fido-u2f', '00000000-0000-0000-0000-000000000000', 0
FROM _bkp_UP_V0002_u2f_devices;
UPDATE user_preferences
SET second_factor_method = 'webauthn'
WHERE second_factor_method = 'u2f';

View File

@ -0,0 +1,37 @@
ALTER TABLE totp_configurations RENAME TO _bkp_DOWN_V0002_totp_configurations;
ALTER TABLE webauthn_devices RENAME TO _bkp_DOWN_V0002_webauthn_devices;
CREATE TABLE IF NOT EXISTS totp_configurations (
id SERIAL,
username VARCHAR(100) NOT NULL,
issuer VARCHAR(100),
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
period INTEGER NOT NULL DEFAULT 30,
secret BYTEA NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
INSERT INTO totp_configurations (id, username, issuer, algorithm, digits, period, secret)
SELECT id, username, issuer, algorithm, digits, period, secret
FROM _bkp_DOWN_V0002_totp_configurations;
CREATE TABLE IF NOT EXISTS u2f_devices (
id SERIAL,
username VARCHAR(100) NOT NULL,
description VARCHAR(30) NOT NULL DEFAULT 'Primary',
key_handle BYTEA NOT NULL,
public_key BYTEA NOT NULL,
PRIMARY KEY (id),
UNIQUE (username, description)
);
INSERT INTO u2f_devices (id, username, description, key_handle, public_key)
SELECT id, username, description, DECODE(kid, 'base64'), public_key
FROM _bkp_DOWN_V0002_webauthn_devices
WHERE attestation_type = 'fido-u2f';
UPDATE user_preferences
SET second_factor_method = 'u2f'
WHERE second_factor_method = 'webauthn';

View File

@ -0,0 +1,47 @@
ALTER TABLE totp_configurations RENAME TO _bkp_UP_V0002_totp_configurations;
ALTER TABLE u2f_devices RENAME TO _bkp_UP_V0002_u2f_devices;
CREATE TABLE IF NOT EXISTS totp_configurations (
id SERIAL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL,
username VARCHAR(100) NOT NULL,
issuer VARCHAR(100),
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
period INTEGER NOT NULL DEFAULT 30,
secret BYTEA NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
INSERT INTO totp_configurations (id, username, issuer, algorithm, digits, period, secret)
SELECT id, username, issuer, algorithm, digits, period, secret
FROM _bkp_UP_V0002_totp_configurations;
CREATE TABLE IF NOT EXISTS webauthn_devices (
id SERIAL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL,
rpid TEXT,
username VARCHAR(100) NOT NULL,
description VARCHAR(30) NOT NULL DEFAULT 'Primary',
kid VARCHAR(100) NOT NULL,
public_key BYTEA NOT NULL,
attestation_type VARCHAR(32),
transport VARCHAR(20) DEFAULT '',
aaguid CHAR(36) NOT NULL,
sign_count INTEGER DEFAULT 0,
clone_warning BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
UNIQUE (username, description),
UNIQUE (kid)
);
INSERT INTO webauthn_devices (id, rpid, username, description, kid, public_key, attestation_type, aaguid, sign_count)
SELECT id, '', username, description, ENCODE(key_handle::BYTEA, 'base64'), public_key, 'fido-u2f', '00000000-0000-0000-0000-000000000000', 0
FROM _bkp_UP_V0002_u2f_devices;
UPDATE user_preferences
SET second_factor_method = 'webauthn'
WHERE second_factor_method = 'u2f';

View File

@ -0,0 +1,37 @@
ALTER TABLE totp_configurations RENAME TO _bkp_DOWN_V0002_totp_configurations;
ALTER TABLE webauthn_devices RENAME TO _bkp_DOWN_V0002_webauthn_devices;
CREATE TABLE IF NOT EXISTS totp_configurations (
id INTEGER,
username VARCHAR(100) NOT NULL,
issuer VARCHAR(100),
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
period INTEGER NOT NULL DEFAULT 30,
secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
INSERT INTO totp_configurations (id, username, issuer, algorithm, digits, period, secret)
SELECT id, username, issuer, algorithm, digits, period, secret
FROM _bkp_DOWN_V0002_totp_configurations;
CREATE TABLE IF NOT EXISTS u2f_devices (
id INTEGER,
username VARCHAR(100) NOT NULL,
description VARCHAR(30) NOT NULL DEFAULT 'Primary',
key_handle BLOB NOT NULL,
public_key BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE (username, description)
);
INSERT INTO u2f_devices (id, username, description, key_handle, public_key)
SELECT id, username, description, B642BIN(kid), public_key
FROM _bkp_DOWN_V0002_webauthn_devices
WHERE attestation_type = 'fido-u2f';
UPDATE user_preferences
SET second_factor_method = 'u2f'
WHERE second_factor_method = 'webauthn';

View File

@ -0,0 +1,47 @@
ALTER TABLE totp_configurations RENAME TO _bkp_UP_V0002_totp_configurations;
ALTER TABLE u2f_devices RENAME TO _bkp_UP_V0002_u2f_devices;
CREATE TABLE IF NOT EXISTS totp_configurations (
id INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NULL DEFAULT NULL,
username VARCHAR(100) NOT NULL,
issuer VARCHAR(100),
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
period INTEGER NOT NULL DEFAULT 30,
secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
INSERT INTO totp_configurations (id, username, issuer, algorithm, digits, period, secret)
SELECT id, username, issuer, algorithm, digits, period, secret
FROM _bkp_UP_V0002_totp_configurations;
CREATE TABLE IF NOT EXISTS webauthn_devices (
id INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NULL DEFAULT NULL,
rpid TEXT,
username VARCHAR(100) NOT NULL,
description VARCHAR(30) NOT NULL DEFAULT 'Primary',
kid VARCHAR(100) NOT NULL,
public_key BLOB NOT NULL,
attestation_type VARCHAR(32),
transport VARCHAR(20) DEFAULT '',
aaguid CHAR(36) NOT NULL,
sign_count INTEGER DEFAULT 0,
clone_warning BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
UNIQUE (username, description),
UNIQUE (kid)
);
INSERT INTO webauthn_devices (id, rpid, username, description, kid, public_key, attestation_type, aaguid, sign_count)
SELECT id, '', username, description, BIN2B64(key_handle), public_key, 'fido-u2f', '00000000-0000-0000-0000-000000000000', 0
FROM _bkp_UP_V0002_u2f_devices;
UPDATE user_preferences
SET second_factor_method = 'webauthn'
WHERE second_factor_method = 'u2f';

View File

@ -22,13 +22,15 @@ type Provider interface {
FindIdentityVerification(ctx context.Context, jti string) (found bool, err error) FindIdentityVerification(ctx context.Context, jti string) (found bool, err error)
SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error)
UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt *time.Time) (err error)
DeleteTOTPConfiguration(ctx context.Context, username string) (err error) DeleteTOTPConfiguration(ctx context.Context, username string) (err error)
LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error) LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error)
LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error)
SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) SaveWebauthnDevice(ctx context.Context, device models.WebauthnDevice) (err error)
LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt *time.Time, signCount uint32, cloneWarning bool) (err error)
LoadU2FDevices(ctx context.Context, limit, page int) (devices []models.U2FDevice, err error) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []models.WebauthnDevice, err error)
LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []models.WebauthnDevice, err error)
SavePreferredDuoDevice(ctx context.Context, device models.DuoDevice) (err error) SavePreferredDuoDevice(ctx context.Context, device models.DuoDevice) (err error)
DeletePreferredDuoDevice(ctx context.Context, username string) (err error) DeletePreferredDuoDevice(ctx context.Context, username string) (err error)

View File

@ -44,13 +44,17 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlUpdateTOTPConfigSecret: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecret, tableTOTPConfigurations), sqlUpdateTOTPConfigSecret: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecret, tableTOTPConfigurations),
sqlUpdateTOTPConfigSecretByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecretByUsername, tableTOTPConfigurations), sqlUpdateTOTPConfigSecretByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecretByUsername, tableTOTPConfigurations),
sqlUpdateTOTPConfigRecordSignIn: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignIn, tableTOTPConfigurations),
sqlUpdateTOTPConfigRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignInByUsername, tableTOTPConfigurations),
sqlUpsertU2FDevice: fmt.Sprintf(queryFmtUpsertU2FDevice, tableU2FDevices), sqlUpsertWebauthnDevice: fmt.Sprintf(queryFmtUpsertWebauthnDevice, tableWebauthnDevices),
sqlSelectU2FDevice: fmt.Sprintf(queryFmtSelectU2FDevice, tableU2FDevices), sqlSelectWebauthnDevices: fmt.Sprintf(queryFmtSelectWebauthnDevices, tableWebauthnDevices),
sqlSelectU2FDevices: fmt.Sprintf(queryFmtSelectU2FDevices, tableU2FDevices), sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices),
sqlUpdateU2FDevicePublicKey: fmt.Sprintf(queryFmtUpdateU2FDevicePublicKey, tableU2FDevices), sqlUpdateWebauthnDevicePublicKey: fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKey, tableWebauthnDevices),
sqlUpdateU2FDevicePublicKeyByUsername: fmt.Sprintf(queryFmtUpdateUpdateU2FDevicePublicKeyByUsername, tableU2FDevices), sqlUpdateWebauthnDevicePublicKeyByUsername: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDevicePublicKeyByUsername, tableWebauthnDevices),
sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices),
sqlUpdateWebauthnDeviceRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignInByUsername, tableWebauthnDevices),
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices), sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices), sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),
@ -58,7 +62,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, tableU2FDevices, tableDuoDevices, tableUserPreferences), sqlSelectUserInfo: fmt.Sprintf(queryFmtSelectUserInfo, tableTOTPConfigurations, tableWebauthnDevices, tableDuoDevices, tableUserPreferences),
sqlInsertMigration: fmt.Sprintf(queryFmtInsertMigration, tableMigrations), sqlInsertMigration: fmt.Sprintf(queryFmtInsertMigration, tableMigrations),
sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations), sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations),
@ -102,14 +106,18 @@ type SQLProvider struct {
sqlUpdateTOTPConfigSecret string sqlUpdateTOTPConfigSecret string
sqlUpdateTOTPConfigSecretByUsername string sqlUpdateTOTPConfigSecretByUsername string
sqlUpdateTOTPConfigRecordSignIn string
sqlUpdateTOTPConfigRecordSignInByUsername string
// Table: u2f_devices. // Table: webauthn_devices.
sqlUpsertU2FDevice string sqlUpsertWebauthnDevice string
sqlSelectU2FDevice string sqlSelectWebauthnDevices string
sqlSelectU2FDevices string sqlSelectWebauthnDevicesByUsername string
sqlUpdateU2FDevicePublicKey string sqlUpdateWebauthnDevicePublicKey string
sqlUpdateU2FDevicePublicKeyByUsername string sqlUpdateWebauthnDevicePublicKeyByUsername string
sqlUpdateWebauthnDeviceRecordSignIn string
sqlUpdateWebauthnDeviceRecordSignInByUsername string
// Table: duo_devices. // Table: duo_devices.
sqlUpsertDuoDevice string sqlUpsertDuoDevice string
@ -182,9 +190,11 @@ func (p *SQLProvider) StartupCheck() (err error) {
// SavePreferred2FAMethod save the preferred method for 2FA to the database. // SavePreferred2FAMethod save the preferred method for 2FA to the database.
func (p *SQLProvider) SavePreferred2FAMethod(ctx context.Context, username string, method string) (err error) { func (p *SQLProvider) SavePreferred2FAMethod(ctx context.Context, username string, method string) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, method) if _, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, method); err != nil {
return fmt.Errorf("error upserting preferred two factor method for user '%s': %w", username, err)
}
return err return nil
} }
// LoadPreferred2FAMethod load the preferred method for 2FA from the database. // LoadPreferred2FAMethod load the preferred method for 2FA from the database.
@ -228,7 +238,7 @@ func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification
if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification,
verification.JTI, verification.IssuedAt, verification.IssuedIP, verification.ExpiresAt, verification.JTI, verification.IssuedAt, verification.IssuedIP, verification.ExpiresAt,
verification.Username, verification.Action); err != nil { verification.Username, verification.Action); err != nil {
return fmt.Errorf("error inserting identity verification: %w", err) return fmt.Errorf("error inserting identity verification for user '%s' with uuid '%s': %w", verification.Username, verification.JTI, err)
} }
return nil return nil
@ -267,12 +277,23 @@ func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string)
// SaveTOTPConfiguration save a TOTP configuration of a given user in the database. // SaveTOTPConfiguration save a TOTP configuration of a given user in the database.
func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) { func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) {
if config.Secret, err = p.encrypt(config.Secret); err != nil { if config.Secret, err = p.encrypt(config.Secret); err != nil {
return fmt.Errorf("error encrypting the TOTP configuration secret: %v", err) return fmt.Errorf("error encrypting the TOTP configuration secret for user '%s': %w", config.Username, err)
} }
if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig, if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
config.Username, config.Issuer, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil { config.CreatedAt, config.LastUsedAt,
return fmt.Errorf("error upserting TOTP configuration: %w", err) config.Username, config.Issuer,
config.Algorithm, config.Digits, config.Period, config.Secret); err != nil {
return fmt.Errorf("error upserting TOTP configuration for user '%s': %w", config.Username, err)
}
return nil
}
// UpdateTOTPConfigurationSignIn updates a registered Webauthn devices sign in information.
func (p *SQLProvider) UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt *time.Time) (err error) {
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 nil return nil
@ -281,7 +302,7 @@ func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.T
// DeleteTOTPConfiguration delete a TOTP configuration from the database given a username. // DeleteTOTPConfiguration delete a TOTP configuration from the database given a username.
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) { func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username); err != nil {
return fmt.Errorf("error deleting TOTP configuration: %w", err) return fmt.Errorf("error deleting TOTP configuration for user '%s': %w", username, err)
} }
return nil return nil
@ -296,11 +317,11 @@ func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string
return nil, ErrNoTOTPConfiguration return nil, ErrNoTOTPConfiguration
} }
return nil, fmt.Errorf("error selecting TOTP configuration: %w", err) return nil, fmt.Errorf("error selecting TOTP configuration for user '%s': %w", username, err)
} }
if config.Secret, err = p.decrypt(config.Secret); err != nil { if config.Secret, err = p.decrypt(config.Secret); err != nil {
return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err) return nil, fmt.Errorf("error decrypting the TOTP secret for user '%s': %w", username, err)
} }
return config, nil return config, nil
@ -308,35 +329,20 @@ func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string
// LoadTOTPConfigurations load a set of TOTP configurations. // LoadTOTPConfigurations load a set of TOTP configurations.
func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error) { func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error) {
rows, err := p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, limit, limit*page) configs = make([]models.TOTPConfiguration, 0, limit)
if err != nil {
if err = p.db.SelectContext(ctx, &configs, p.sqlSelectTOTPConfigs, limit, limit*page); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return configs, nil return nil, nil
} }
return nil, fmt.Errorf("error selecting TOTP configurations: %w", err) return nil, fmt.Errorf("error selecting TOTP configurations: %w", err)
} }
defer func() { for i, c := range configs {
if err := rows.Close(); err != nil { if configs[i].Secret, err = p.decrypt(c.Secret); err != nil {
p.log.Errorf(logFmtErrClosingConn, err) return nil, fmt.Errorf("error decrypting TOTP configuration for user '%s': %w", c.Username, err)
} }
}()
configs = make([]models.TOTPConfiguration, 0, limit)
var config models.TOTPConfiguration
for rows.Next() {
if err = rows.StructScan(&config); err != nil {
return nil, fmt.Errorf("error scanning TOTP configuration to struct: %w", err)
}
if config.Secret, err = p.decrypt(config.Secret); err != nil {
return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err)
}
configs = append(configs, config)
} }
return configs, nil return configs, nil
@ -351,90 +357,89 @@ func (p *SQLProvider) updateTOTPConfigurationSecret(ctx context.Context, config
} }
if err != nil { if err != nil {
return fmt.Errorf("error updating TOTP configuration secret: %w", err) return fmt.Errorf("error updating TOTP configuration secret for user '%s': %w", config.Username, err)
} }
return nil return nil
} }
// SaveU2FDevice saves a registered U2F device. // SaveWebauthnDevice saves a registered Webauthn device.
func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) { func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device models.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 the U2F device public key: %v", err) return fmt.Errorf("error encrypting the Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err)
} }
if _, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.Description, device.KeyHandle, device.PublicKey); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlUpsertWebauthnDevice,
return fmt.Errorf("error upserting U2F device: %v", err) device.CreatedAt, device.LastUsedAt,
device.RPID, device.Username, device.Description,
device.KID, device.PublicKey,
device.AttestationType, device.Transport, device.AAGUID, device.SignCount, device.CloneWarning,
); err != nil {
return fmt.Errorf("error upserting Webauthn device for user '%s' kid '%x': %w", device.Username, device.KID, err)
} }
return nil return nil
} }
// LoadU2FDevice loads a U2F device registration for a given username. // UpdateWebauthnDeviceSignIn updates a registered Webauthn devices sign in information.
func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) { func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt *time.Time, signCount uint32, cloneWarning bool) (err error) {
device = &models.U2FDevice{} if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn, rpid, lastUsedAt, signCount, cloneWarning, id); err != nil {
return fmt.Errorf("error updating Webauthn signin metadata for id '%x': %w", id, err)
if err = p.db.GetContext(ctx, device, p.sqlSelectU2FDevice, username); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoU2FDeviceHandle
} }
return nil, fmt.Errorf("error selecting U2F device: %w", err) return nil
}
if device.PublicKey, err = p.decrypt(device.PublicKey); err != nil {
return nil, fmt.Errorf("error decrypting the U2F device public key: %v", err)
}
return device, nil
} }
// LoadU2FDevices loads U2F device registrations. // LoadWebauthnDevices loads Webauthn device registrations.
func (p *SQLProvider) LoadU2FDevices(ctx context.Context, limit, page int) (devices []models.U2FDevice, err error) { func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []models.WebauthnDevice, err error) {
rows, err := p.db.QueryxContext(ctx, p.sqlSelectU2FDevices, limit, limit*page) devices = make([]models.WebauthnDevice, 0, limit)
if 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 devices, nil return nil, nil
} }
return nil, fmt.Errorf("error selecting U2F devices: %w", err) return nil, fmt.Errorf("error selecting Webauthn devices: %w", err)
} }
defer func() { for i, device := range devices {
if err := rows.Close(); err != nil { if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil {
p.log.Errorf(logFmtErrClosingConn, err) return nil, fmt.Errorf("error decrypting Webauthn public key for user '%s': %w", device.Username, err)
} }
}()
devices = make([]models.U2FDevice, 0, limit)
var device models.U2FDevice
for rows.Next() {
if err = rows.StructScan(&device); err != nil {
return nil, fmt.Errorf("error scanning U2F device to struct: %w", err)
}
if device.PublicKey, err = p.decrypt(device.PublicKey); err != nil {
return nil, fmt.Errorf("error decrypting the U2F device public key: %v", err)
}
devices = append(devices, device)
} }
return devices, nil return devices, nil
} }
func (p *SQLProvider) updateU2FDevicePublicKey(ctx context.Context, device models.U2FDevice) (err error) { // LoadWebauthnDevicesByUsername loads all webauthn devices registration for a given username.
func (p *SQLProvider) LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []models.WebauthnDevice, err error) {
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevicesByUsername, username); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoWebauthnDevice
}
return nil, fmt.Errorf("error selecting Webauthn devices for user '%s': %w", username, err)
}
for i, device := range devices {
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 devices, nil
}
func (p *SQLProvider) updateWebauthnDevicePublicKey(ctx context.Context, device models.WebauthnDevice) (err error) {
switch device.ID { switch device.ID {
case 0: case 0:
_, err = p.db.ExecContext(ctx, p.sqlUpdateU2FDevicePublicKeyByUsername, device.PublicKey, device.Username) _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDevicePublicKeyByUsername, device.PublicKey, device.Username, device.KID)
default: default:
_, err = p.db.ExecContext(ctx, p.sqlUpdateU2FDevicePublicKey, device.PublicKey, device.ID) _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDevicePublicKey, device.PublicKey, device.ID)
} }
if err != nil { if err != nil {
return fmt.Errorf("error updating U2F public key: %w", err) return fmt.Errorf("error updating Webauthn public key for user '%s' kid '%x': %w", device.Username, device.KID, err)
} }
return nil return nil
@ -442,26 +447,32 @@ func (p *SQLProvider) updateU2FDevicePublicKey(ctx context.Context, device model
// SavePreferredDuoDevice saves a Duo device. // SavePreferredDuoDevice saves a Duo device.
func (p *SQLProvider) SavePreferredDuoDevice(ctx context.Context, device models.DuoDevice) (err error) { func (p *SQLProvider) SavePreferredDuoDevice(ctx context.Context, device models.DuoDevice) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlUpsertDuoDevice, device.Username, device.Device, device.Method) if _, err = p.db.ExecContext(ctx, p.sqlUpsertDuoDevice, device.Username, device.Device, device.Method); err != nil {
return err return fmt.Errorf("error upserting preferred duo device for user '%s': %w", device.Username, err)
}
return nil
} }
// DeletePreferredDuoDevice deletes a Duo device of a given user. // DeletePreferredDuoDevice deletes a Duo device of a given user.
func (p *SQLProvider) DeletePreferredDuoDevice(ctx context.Context, username string) (err error) { func (p *SQLProvider) DeletePreferredDuoDevice(ctx context.Context, username string) (err error) {
_, err = p.db.ExecContext(ctx, p.sqlDeleteDuoDevice, username) if _, err = p.db.ExecContext(ctx, p.sqlDeleteDuoDevice, username); err != nil {
return err return fmt.Errorf("error deleting preferred duo device for user '%s': %w", username, err)
}
return nil
} }
// LoadPreferredDuoDevice loads a Duo device of a given user. // LoadPreferredDuoDevice loads a Duo device of a given user.
func (p *SQLProvider) LoadPreferredDuoDevice(ctx context.Context, username string) (device *models.DuoDevice, err error) { func (p *SQLProvider) LoadPreferredDuoDevice(ctx context.Context, username string) (device *models.DuoDevice, err error) {
device = &models.DuoDevice{} device = &models.DuoDevice{}
if err := p.db.QueryRowxContext(ctx, p.sqlSelectDuoDevice, username).StructScan(device); err != nil { if err = p.db.QueryRowxContext(ctx, p.sqlSelectDuoDevice, username).StructScan(device); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, ErrNoDuoDevice return nil, ErrNoDuoDevice
} }
return nil, err return nil, fmt.Errorf("error selecting preferred duo device for user '%s': %w", username, err)
} }
return device, nil return device, nil
@ -472,7 +483,7 @@ func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt model
if _, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt, if _, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt,
attempt.Time, attempt.Successful, attempt.Banned, attempt.Username, attempt.Time, attempt.Successful, attempt.Banned, attempt.Username,
attempt.Type, attempt.RemoteIP, attempt.RequestURI, attempt.RequestMethod); err != nil { attempt.Type, attempt.RemoteIP, attempt.RequestURI, attempt.RequestMethod); err != nil {
return fmt.Errorf("error inserting authentication attempt: %w", err) return fmt.Errorf("error inserting authentication attempt for user '%s': %w", attempt.Username, err)
} }
return nil return nil
@ -480,31 +491,14 @@ func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt model
// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log. // LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) { func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) {
rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page) attempts = make([]models.AuthenticationAttempt, 0, limit)
if err != nil {
if err = p.db.SelectContext(ctx, &attempts, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoAuthenticationLogs return nil, ErrNoAuthenticationLogs
} }
return nil, fmt.Errorf("error selecting authentication logs: %w", err) return nil, fmt.Errorf("error selecting authentication logs for user '%s': %w", username, err)
}
defer func() {
if err := rows.Close(); err != nil {
p.log.Errorf(logFmtErrClosingConn, err)
}
}()
var attempt models.AuthenticationAttempt
attempts = make([]models.AuthenticationAttempt, 0, limit)
for rows.Next() {
if err = rows.StructScan(&attempt); err != nil {
return nil, err
}
attempts = append(attempts, attempt)
} }
return attempts, nil return attempts, nil

View File

@ -26,7 +26,7 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr
// Specific alterations to this provider. // Specific alterations to this provider.
// PostgreSQL doesn't have a UPSERT statement but has an ON CONFLICT operation instead. // PostgreSQL doesn't have a UPSERT statement but has an ON CONFLICT operation instead.
provider.sqlUpsertU2FDevice = fmt.Sprintf(queryFmtPostgresUpsertU2FDevice, tableU2FDevices) provider.sqlUpsertWebauthnDevice = fmt.Sprintf(queryFmtPostgresUpsertWebauthnDevice, tableWebauthnDevices)
provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtPostgresUpsertDuoDevice, tableDuoDevices) provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtPostgresUpsertDuoDevice, tableDuoDevices)
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations) provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations)
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtPostgresUpsertPreferred2FAMethod, tableUserPreferences) provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtPostgresUpsertPreferred2FAMethod, tableUserPreferences)
@ -40,14 +40,18 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr
provider.sqlInsertIdentityVerification = provider.db.Rebind(provider.sqlInsertIdentityVerification) provider.sqlInsertIdentityVerification = provider.db.Rebind(provider.sqlInsertIdentityVerification)
provider.sqlConsumeIdentityVerification = provider.db.Rebind(provider.sqlConsumeIdentityVerification) provider.sqlConsumeIdentityVerification = provider.db.Rebind(provider.sqlConsumeIdentityVerification)
provider.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig) provider.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig)
provider.sqlUpdateTOTPConfigRecordSignIn = provider.db.Rebind(provider.sqlUpdateTOTPConfigRecordSignIn)
provider.sqlUpdateTOTPConfigRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigRecordSignInByUsername)
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.sqlUpdateTOTPConfigSecret = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecret) provider.sqlUpdateTOTPConfigSecret = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecret)
provider.sqlUpdateTOTPConfigSecretByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecretByUsername) provider.sqlUpdateTOTPConfigSecretByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecretByUsername)
provider.sqlSelectU2FDevice = provider.db.Rebind(provider.sqlSelectU2FDevice) provider.sqlSelectWebauthnDevices = provider.db.Rebind(provider.sqlSelectWebauthnDevices)
provider.sqlSelectU2FDevices = provider.db.Rebind(provider.sqlSelectU2FDevices) provider.sqlSelectWebauthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByUsername)
provider.sqlUpdateU2FDevicePublicKey = provider.db.Rebind(provider.sqlUpdateU2FDevicePublicKey) provider.sqlUpdateWebauthnDevicePublicKey = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKey)
provider.sqlUpdateU2FDevicePublicKeyByUsername = provider.db.Rebind(provider.sqlUpdateU2FDevicePublicKeyByUsername) provider.sqlUpdateWebauthnDevicePublicKeyByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKeyByUsername)
provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn)
provider.sqlUpdateWebauthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignInByUsername)
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)
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt) provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)

View File

@ -1,6 +1,10 @@
package storage package storage
import ( import (
"database/sql"
"encoding/base64"
"github.com/mattn/go-sqlite3"
_ "github.com/mattn/go-sqlite3" // Load the SQLite Driver used in the connection string. _ "github.com/mattn/go-sqlite3" // Load the SQLite Driver used in the connection string.
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
@ -14,7 +18,7 @@ type SQLiteProvider struct {
// NewSQLiteProvider constructs a SQLite provider. // NewSQLiteProvider constructs a SQLite provider.
func NewSQLiteProvider(config *schema.Configuration) (provider *SQLiteProvider) { func NewSQLiteProvider(config *schema.Configuration) (provider *SQLiteProvider) {
provider = &SQLiteProvider{ provider = &SQLiteProvider{
SQLProvider: NewSQLProvider(config, providerSQLite, "sqlite3", config.Storage.Local.Path), SQLProvider: NewSQLProvider(config, providerSQLite, "sqlite3e", config.Storage.Local.Path),
} }
// All providers have differing SELECT existing table statements. // All providers have differing SELECT existing table statements.
@ -22,3 +26,27 @@ func NewSQLiteProvider(config *schema.Configuration) (provider *SQLiteProvider)
return provider return provider
} }
func sqlite3BLOBToTEXTBase64(data []byte) (b64 string) {
return base64.StdEncoding.EncodeToString(data)
}
func sqlite3TEXTBase64ToBLOB(b64 string) (data []byte, err error) {
return base64.StdEncoding.DecodeString(b64)
}
func init() {
sql.Register("sqlite3e", &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) (err error) {
if err = conn.RegisterFunc("BIN2B64", sqlite3BLOBToTEXTBase64, true); err != nil {
return err
}
if err = conn.RegisterFunc("B642BIN", sqlite3TEXTBase64ToBLOB, true); err != nil {
return err
}
return nil
},
})
}

View File

@ -26,7 +26,7 @@ func (p *SQLProvider) SchemaEncryptionChangeKey(ctx context.Context, encryptionK
return err return err
} }
if err = p.schemaEncryptionChangeKeyU2F(ctx, tx, key); err != nil { if err = p.schemaEncryptionChangeKeyWebauthn(ctx, tx, key); err != nil {
return err return err
} }
@ -79,11 +79,11 @@ func (p *SQLProvider) schemaEncryptionChangeKeyTOTP(ctx context.Context, tx *sql
return nil return nil
} }
func (p *SQLProvider) schemaEncryptionChangeKeyU2F(ctx context.Context, tx *sqlx.Tx, key [32]byte) (err error) { func (p *SQLProvider) schemaEncryptionChangeKeyWebauthn(ctx context.Context, tx *sqlx.Tx, key [32]byte) (err error) {
var devices []models.U2FDevice var devices []models.WebauthnDevice
for page := 0; true; page++ { for page := 0; true; page++ {
if devices, err = p.LoadU2FDevices(ctx, 10, page); err != nil { if devices, err = p.LoadWebauthnDevices(ctx, 10, page); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err) return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
} }
@ -100,7 +100,7 @@ func (p *SQLProvider) schemaEncryptionChangeKeyU2F(ctx context.Context, tx *sqlx
return fmt.Errorf("rollback due to error: %w", err) return fmt.Errorf("rollback due to error: %w", err)
} }
if err = p.updateU2FDevicePublicKey(ctx, device); err != nil { if err = p.updateWebauthnDevicePublicKey(ctx, device); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err) return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
} }
@ -223,7 +223,7 @@ func (p *SQLProvider) schemaEncryptionCheckU2F(ctx context.Context) (err error)
var rows *sqlx.Rows var rows *sqlx.Rows
for page := 0; true; page++ { for page := 0; true; page++ {
if rows, err = p.db.QueryxContext(ctx, p.sqlSelectU2FDevices, pageSize, pageSize*page); err != nil { if rows, err = p.db.QueryxContext(ctx, p.sqlSelectWebauthnDevices, pageSize, pageSize*page); err != nil {
_ = rows.Close() _ = rows.Close()
return fmt.Errorf("error selecting U2F devices: %w", err) return fmt.Errorf("error selecting U2F devices: %w", err)

View File

@ -35,7 +35,7 @@ const (
const ( const (
queryFmtSelectUserInfo = ` queryFmtSelectUserInfo = `
SELECT second_factor_method, (SELECT EXISTS (SELECT id FROM %s WHERE username = ?)) AS has_totp, (SELECT EXISTS (SELECT id FROM %s WHERE username = ?)) AS has_u2f, (SELECT EXISTS (SELECT id FROM %s WHERE username = ?)) AS has_duo SELECT second_factor_method, (SELECT EXISTS (SELECT id FROM %s WHERE username = ?)) AS has_totp, (SELECT EXISTS (SELECT id FROM %s WHERE username = ?)) AS has_webauthn, (SELECT EXISTS (SELECT id FROM %s WHERE username = ?)) AS has_duo
FROM %s FROM %s
WHERE username = ?;` WHERE username = ?;`
@ -96,14 +96,24 @@ const (
WHERE username = ?;` WHERE username = ?;`
queryFmtUpsertTOTPConfiguration = ` queryFmtUpsertTOTPConfiguration = `
REPLACE INTO %s (username, issuer, algorithm, digits, period, secret) REPLACE INTO %s (created_at, last_used_at, username, issuer, algorithm, digits, period, secret)
VALUES (?, ?, ?, ?, ?, ?);` VALUES (?, ?, ?, ?, ?, ?, ?, ?);`
queryFmtPostgresUpsertTOTPConfiguration = ` queryFmtPostgresUpsertTOTPConfiguration = `
INSERT INTO %s (username, issuer, algorithm, digits, period, secret) INSERT INTO %s (created_at, last_used_at, username, issuer, algorithm, digits, period, secret)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (username) ON CONFLICT (username)
DO UPDATE SET issuer = $2, algorithm = $3, digits = $4, period = $5, secret = $6;` DO UPDATE SET created_at = $1, last_used_at = $2, issuer = $4, algorithm = $5, digits = $6, period = $7, secret = $8;`
queryFmtUpdateTOTPConfigRecordSignIn = `
UPDATE %s
SET last_used_at = ?
WHERE id = ?;`
queryFmtUpdateTOTPConfigRecordSignInByUsername = `
UPDATE %s
SET last_used_at = ?
WHERE username = ?;`
queryFmtDeleteTOTPConfiguration = ` queryFmtDeleteTOTPConfiguration = `
DELETE FROM %s DELETE FROM %s
@ -111,36 +121,50 @@ const (
) )
const ( const (
queryFmtSelectU2FDevice = ` queryFmtSelectWebauthnDevices = `
SELECT id, username, key_handle, public_key SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
FROM %s
WHERE username = ?;`
queryFmtSelectU2FDevices = `
SELECT id, username, key_handle, public_key
FROM %s FROM %s
LIMIT ? LIMIT ?
OFFSET ?;` OFFSET ?;`
queryFmtUpdateU2FDevicePublicKey = ` queryFmtSelectWebauthnDevicesByUsername = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
FROM %s
WHERE username = ?;`
queryFmtUpdateWebauthnDevicePublicKey = `
UPDATE %s UPDATE %s
SET public_key = ? SET public_key = ?
WHERE id = ?;` WHERE id = ?;`
queryFmtUpdateUpdateU2FDevicePublicKeyByUsername = ` queryFmtUpdateUpdateWebauthnDevicePublicKeyByUsername = `
UPDATE %s UPDATE %s
SET public_key = ? SET public_key = ?
WHERE username = ?;` WHERE username = ? AND kid = ?;`
queryFmtUpsertU2FDevice = ` queryFmtUpdateWebauthnDeviceRecordSignIn = `
REPLACE INTO %s (username, description, key_handle, public_key) UPDATE %s
VALUES (?, ?, ?, ?);` SET
rpid = ?, last_used_at = ?, sign_count = ?,
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
WHERE id = ?;`
queryFmtPostgresUpsertU2FDevice = ` queryFmtUpdateWebauthnDeviceRecordSignInByUsername = `
INSERT INTO %s (username, description, key_handle, public_key) UPDATE %s
VALUES ($1, $2, $3, $4) SET
rpid = ?, last_used_at = ?, sign_count = ?,
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
WHERE username = ? AND kid = ?;`
queryFmtUpsertWebauthnDevice = `
REPLACE INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
queryFmtPostgresUpsertWebauthnDevice = `
INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (username, description) ON CONFLICT (username, description)
DO UPDATE SET key_handle=$3, public_key=$4;` DO UPDATE SET created_at = $1, last_used_at = $2, rpid = $3, kid = $6, public_key = $7, attestation_type = $8, transport = $9, aaguid = $10, sign_count = $11, clone_warning = $12;`
) )
const ( const (
@ -152,7 +176,7 @@ const (
INSERT INTO %s (username, device, method) INSERT INTO %s (username, device, method)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
ON CONFLICT (username) ON CONFLICT (username)
DO UPDATE SET device=$2, method=$3;` DO UPDATE SET device = $2, method = $3;`
queryFmtDeleteDuoDevice = ` queryFmtDeleteDuoDevice = `
DELETE DELETE
@ -194,5 +218,5 @@ const (
INSERT INTO %s (name, value) INSERT INTO %s (name, value)
VALUES ($1, $2) VALUES ($1, $2)
ON CONFLICT (name) ON CONFLICT (name)
DO UPDATE SET value=$2;` DO UPDATE SET value = $2;`
) )

View File

@ -29,7 +29,7 @@ func (p *SQLProvider) schemaMigratePre1To1(ctx context.Context) (err error) {
tablePre1Config, tablePre1Config,
tablePre1TOTPSecrets, tablePre1TOTPSecrets,
tablePre1IdentityVerificationTokens, tablePre1IdentityVerificationTokens,
tableU2FDevices, tablePre1U2FDevices,
tableUserPreferences, tableUserPreferences,
tableAuthenticationLogs, tableAuthenticationLogs,
tableAlphaPreferences, tableAlphaPreferences,
@ -93,7 +93,7 @@ func (p *SQLProvider) schemaMigratePre1Rename(ctx context.Context, tables, table
} }
if p.name == providerPostgres { if p.name == providerPostgres {
if table == tableU2FDevices || table == tableUserPreferences { if table == tablePre1U2FDevices || table == tableUserPreferences {
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE %s RENAME CONSTRAINT %s_pkey TO %s_pkey;`, if _, err = p.db.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE %s RENAME CONSTRAINT %s_pkey TO %s_pkey;`,
tableNew, table, tableNew)); err != nil { tableNew, table, tableNew)); err != nil {
continue continue
@ -132,7 +132,7 @@ func (p *SQLProvider) schemaMigratePre1To1Rollback(ctx context.Context, up bool)
return err return err
} }
if p.name == providerPostgres && (tableNew == tableU2FDevices || tableNew == tableUserPreferences) { if p.name == providerPostgres && (tableNew == tablePre1U2FDevices || tableNew == tableUserPreferences) {
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE %s RENAME CONSTRAINT %s_pkey TO %s_pkey;`, if _, err = p.db.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE %s RENAME CONSTRAINT %s_pkey TO %s_pkey;`,
tableNew, table, tableNew)); err != nil { tableNew, table, tableNew)); err != nil {
continue continue
@ -236,7 +236,7 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error)
} }
func (p *SQLProvider) schemaMigratePre1To1U2F(ctx context.Context) (err error) { func (p *SQLProvider) schemaMigratePre1To1U2F(ctx context.Context) (err error) {
rows, err := p.db.Queryx(fmt.Sprintf(p.db.Rebind(queryFmtPre1To1SelectU2FDevices), tablePrefixBackup+tableU2FDevices)) rows, err := p.db.Queryx(fmt.Sprintf(p.db.Rebind(queryFmtPre1To1SelectU2FDevices), tablePrefixBackup+tablePre1U2FDevices))
if err != nil { if err != nil {
return err return err
} }
@ -276,7 +276,7 @@ func (p *SQLProvider) schemaMigratePre1To1U2F(ctx context.Context) (err error) {
} }
for _, device := range devices { for _, device := range devices {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1To1InsertU2FDevice), tableU2FDevices), device.Username, device.KeyHandle, device.PublicKey) _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1To1InsertU2FDevice), tablePre1U2FDevices), device.Username, device.KeyHandle, device.PublicKey)
if err != nil { if err != nil {
return err return err
} }
@ -295,7 +295,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1(ctx context.Context) (err error) {
tableMigrations, tableMigrations,
tableTOTPConfigurations, tableTOTPConfigurations,
tableIdentityVerification, tableIdentityVerification,
tableU2FDevices, tablePre1U2FDevices,
tableDuoDevices, tableDuoDevices,
tableUserPreferences, tableUserPreferences,
tableAuthenticationLogs, tableAuthenticationLogs,
@ -429,7 +429,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1TOTP(ctx context.Context) (err error)
} }
func (p *SQLProvider) schemaMigrate1ToPre1U2F(ctx context.Context) (err error) { func (p *SQLProvider) schemaMigrate1ToPre1U2F(ctx context.Context) (err error) {
rows, err := p.db.QueryxContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1SelectU2FDevices), tablePrefixBackup+tableU2FDevices)) rows, err := p.db.QueryxContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1SelectU2FDevices), tablePrefixBackup+tablePre1U2FDevices))
if err != nil { if err != nil {
return err return err
} }
@ -460,7 +460,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1U2F(ctx context.Context) (err error) {
} }
for _, device := range devices { for _, device := range devices {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1InsertU2FDevice), tableU2FDevices), device.Username, base64.StdEncoding.EncodeToString(device.KeyHandle), base64.StdEncoding.EncodeToString(device.PublicKey)) _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1InsertU2FDevice), tablePre1U2FDevices), device.Username, base64.StdEncoding.EncodeToString(device.KeyHandle), base64.StdEncoding.EncodeToString(device.PublicKey))
if err != nil { if err != nil {
return err return err
} }

View File

@ -60,7 +60,7 @@ const (
var ( var (
storageLocalTmpConfig = schema.Configuration{ storageLocalTmpConfig = schema.Configuration{
TOTP: &schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Issuer: "Authelia", Issuer: "Authelia",
Period: 6, Period: 6,
}, },

View File

@ -41,18 +41,17 @@ func (s *BackendProtectionScenario) AssertRequestStatusCode(method, url string,
func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() { func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/webauthn/assertion", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/register", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/webauthn/attestation", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign_request", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/user/info/2fa_method", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/user/info/2fa_method", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/user/info", AutheliaBaseURL), 403) s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/user/info", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration", AutheliaBaseURL), 403) s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/finish", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/start", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/start", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/finish", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/totp/identity/finish", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/webauthn/identity/start", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/webauthn/identity/finish", AutheliaBaseURL), 403)
} }
func (s *BackendProtectionScenario) TestInvalidEndpointsReturn404() { func (s *BackendProtectionScenario) TestInvalidEndpointsReturn404() {

View File

@ -262,9 +262,17 @@ func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config=/config/configuration.storage.yml"}) output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
pattern := regexp.MustCompile(`^Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification, totp_configurations, u2f_devices, duo_devices, user_preferences, migrations, encryption\nSchema Encryption Key: valid`) s.Assert().Contains(output, "Schema Version: ")
s.Assert().Contains(output, "authentication_logs")
s.Assert().Regexp(pattern, output) s.Assert().Contains(output, "identity_verification")
s.Assert().Contains(output, "duo_devices")
s.Assert().Contains(output, "user_preferences")
s.Assert().Contains(output, "migrations")
s.Assert().Contains(output, "encryption")
s.Assert().Contains(output, "encryption")
s.Assert().Contains(output, "webauthn_devices")
s.Assert().Contains(output, "totp_configurations")
s.Assert().Contains(output, "Schema Encryption Key: valid")
} }
func (s *CLISuite) TestStorage03ShouldExportTOTP() { func (s *CLISuite) TestStorage03ShouldExportTOTP() {
@ -399,8 +407,17 @@ func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
pattern := regexp.MustCompile(`Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification, totp_configurations, u2f_devices, duo_devices, user_preferences, migrations, encryption\nSchema Encryption Key: invalid`) s.Assert().Contains(output, "Schema Version: ")
s.Assert().Regexp(pattern, output) s.Assert().Contains(output, "authentication_logs")
s.Assert().Contains(output, "identity_verification")
s.Assert().Contains(output, "duo_devices")
s.Assert().Contains(output, "user_preferences")
s.Assert().Contains(output, "migrations")
s.Assert().Contains(output, "encryption")
s.Assert().Contains(output, "encryption")
s.Assert().Contains(output, "webauthn_devices")
s.Assert().Contains(output, "totp_configurations")
s.Assert().Contains(output, "Schema Encryption Key: invalid")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)

View File

@ -462,6 +462,7 @@ func (s *DuoPushSuite) TestDuoPushRedirectionURLSuite() {
func (s *DuoPushSuite) TestAvailableMethodsScenario() { func (s *DuoPushSuite) TestAvailableMethodsScenario() {
suite.Run(s.T(), NewAvailableMethodsScenario([]string{ suite.Run(s.T(), NewAvailableMethodsScenario([]string{
"TIME-BASED ONE-TIME PASSWORD", "TIME-BASED ONE-TIME PASSWORD",
"SECURITY KEY - WEBAUTHN",
"PUSH NOTIFICATION", "PUSH NOTIFICATION",
})) }))
} }

View File

@ -288,7 +288,7 @@ func (s *StandaloneSuite) TestResetPasswordScenario() {
} }
func (s *StandaloneSuite) TestAvailableMethodsScenario() { func (s *StandaloneSuite) TestAvailableMethodsScenario() {
suite.Run(s.T(), NewAvailableMethodsScenario([]string{"TIME-BASED ONE-TIME PASSWORD"})) suite.Run(s.T(), NewAvailableMethodsScenario([]string{"TIME-BASED ONE-TIME PASSWORD", "SECURITY KEY - WEBAUTHN"}))
} }
func (s *StandaloneSuite) TestRedirectionURLScenario() { func (s *StandaloneSuite) TestRedirectionURLScenario() {

View File

@ -0,0 +1,15 @@
package totp
import (
"testing"
"github.com/pquerna/otp"
"github.com/stretchr/testify/assert"
)
func TestOTPStringToAlgo(t *testing.T) {
assert.Equal(t, otp.AlgorithmSHA1, otpStringToAlgo("SHA1"))
assert.Equal(t, otp.AlgorithmSHA256, otpStringToAlgo("SHA256"))
assert.Equal(t, otp.AlgorithmSHA512, otpStringToAlgo("SHA512"))
assert.Equal(t, otp.AlgorithmSHA1, otpStringToAlgo(""))
}

View File

@ -7,6 +7,6 @@ import (
// Provider for TOTP functionality. // Provider for TOTP functionality.
type Provider interface { type Provider interface {
Generate(username string) (config *models.TOTPConfiguration, err error) Generate(username string) (config *models.TOTPConfiguration, err error)
GenerateCustom(username, algorithm string, digits, period, secretSize uint) (config *models.TOTPConfiguration, err error) GenerateCustom(username string, algorithm string, digits, period, secretSize uint) (config *models.TOTPConfiguration, err error)
Validate(token string, config *models.TOTPConfiguration) (valid bool, err error) Validate(token string, config *models.TOTPConfiguration) (valid bool, err error)
} }

View File

@ -11,9 +11,9 @@ import (
) )
// NewTimeBasedProvider creates a new totp.TimeBased which implements the totp.Provider. // NewTimeBasedProvider creates a new totp.TimeBased which implements the totp.Provider.
func NewTimeBasedProvider(config *schema.TOTPConfiguration) (provider *TimeBased) { func NewTimeBasedProvider(config schema.TOTPConfiguration) (provider *TimeBased) {
provider = &TimeBased{ provider = &TimeBased{
config: config, config: &config,
} }
if config.Skew != nil { if config.Skew != nil {
@ -49,6 +49,7 @@ func (p TimeBased) GenerateCustom(username, algorithm string, digits, period, se
} }
config = &models.TOTPConfiguration{ config = &models.TOTPConfiguration{
CreatedAt: time.Now(),
Username: username, Username: username,
Issuer: p.config.Issuer, Issuer: p.config.Issuer,
Algorithm: algorithm, Algorithm: algorithm,

View File

@ -0,0 +1,89 @@
package totp
import (
"encoding/base32"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func TestTOTPGenerateCustom(t *testing.T) {
totp := NewTimeBasedProvider(schema.TOTPConfiguration{
Issuer: "Authelia",
Algorithm: "SHA1",
Digits: 6,
Period: 30,
})
assert.Equal(t, uint(1), totp.skew)
config, err := totp.GenerateCustom("john", "SHA1", 6, 30, 32)
assert.NoError(t, err)
assert.Equal(t, uint(6), config.Digits)
assert.Equal(t, uint(30), config.Period)
assert.Equal(t, "SHA1", config.Algorithm)
assert.Less(t, time.Since(config.CreatedAt), time.Second)
assert.Greater(t, time.Since(config.CreatedAt), time.Second*-1)
secret := make([]byte, base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(config.Secret)))
_, err = base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, config.Secret)
assert.NoError(t, err)
assert.Len(t, secret, 32)
config, err = totp.GenerateCustom("john", "SHA1", 6, 30, 42)
assert.NoError(t, err)
assert.Equal(t, uint(6), config.Digits)
assert.Equal(t, uint(30), config.Period)
assert.Equal(t, "SHA1", config.Algorithm)
assert.Less(t, time.Since(config.CreatedAt), time.Second)
assert.Greater(t, time.Since(config.CreatedAt), time.Second*-1)
secret = make([]byte, base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(config.Secret)))
_, err = base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, config.Secret)
assert.NoError(t, err)
assert.Len(t, secret, 42)
_, err = totp.GenerateCustom("", "SHA1", 6, 30, 32)
assert.EqualError(t, err, "AccountName must be set")
}
func TestTOTPGenerate(t *testing.T) {
skew := uint(2)
totp := NewTimeBasedProvider(schema.TOTPConfiguration{
Issuer: "Authelia",
Algorithm: "SHA256",
Digits: 8,
Period: 60,
Skew: &skew,
})
assert.Equal(t, uint(2), totp.skew)
config, err := totp.Generate("john")
assert.NoError(t, err)
assert.Equal(t, "Authelia", config.Issuer)
assert.Less(t, time.Since(config.CreatedAt), time.Second)
assert.Greater(t, time.Since(config.CreatedAt), time.Second*-1)
assert.Equal(t, uint(8), config.Digits)
assert.Equal(t, uint(60), config.Period)
assert.Equal(t, "SHA256", config.Algorithm)
secret := make([]byte, base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(config.Secret)))
_, err = base32.StdEncoding.WithPadding(base32.NoPadding).Decode(secret, config.Secret)
assert.NoError(t, err)
assert.Len(t, secret, 32)
}

View File

@ -23,8 +23,7 @@
"react-i18next": "11.15.5", "react-i18next": "11.15.5",
"react-loading": "2.0.3", "react-loading": "2.0.3",
"react-otp-input": "2.4.0", "react-otp-input": "2.4.0",
"react-router-dom": "6.2.2", "react-router-dom": "6.2.2"
"u2f-api": "1.2.1"
}, },
"scripts": { "scripts": {
"prepare": "cd .. && husky install .github", "prepare": "cd .. && husky install .github",

View File

@ -51,7 +51,6 @@ specifiers:
react-router-dom: 6.2.2 react-router-dom: 6.2.2
react-test-renderer: 17.0.2 react-test-renderer: 17.0.2
typescript: 4.6.2 typescript: 4.6.2
u2f-api: 1.2.1
vite: 2.8.6 vite: 2.8.6
vite-plugin-eslint: 1.3.0 vite-plugin-eslint: 1.3.0
vite-plugin-istanbul: 2.5.0 vite-plugin-istanbul: 2.5.0
@ -80,7 +79,6 @@ dependencies:
react-loading: 2.0.3_react@17.0.2 react-loading: 2.0.3_react@17.0.2
react-otp-input: 2.4.0_react-dom@17.0.2+react@17.0.2 react-otp-input: 2.4.0_react-dom@17.0.2+react@17.0.2
react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2 react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2
u2f-api: 1.2.1
devDependencies: devDependencies:
'@commitlint/cli': 16.2.1 '@commitlint/cli': 16.2.1
@ -7846,10 +7844,6 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/u2f-api/1.2.1:
resolution: {integrity: sha512-4kfxen+mVxYkdJWHIS7c4HxsqNUs6fkrxlMLi7uQTVVYMVfTh73PhYoAmp1B0PRGELGDiKmXlssgqxcPvtpfSg==}
dev: false
/unbox-primitive/1.0.1: /unbox-primitive/1.0.1:
resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==} resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==}
dependencies: dependencies:

View File

@ -6,13 +6,13 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NotificationBar from "@components/NotificationBar"; import NotificationBar from "@components/NotificationBar";
import { import {
FirstFactorRoute, ConsentRoute,
IndexRoute,
LogoutRoute,
RegisterOneTimePasswordRoute,
RegisterWebauthnRoute,
ResetPasswordStep2Route, ResetPasswordStep2Route,
ResetPasswordStep1Route, ResetPasswordStep1Route,
RegisterSecurityKeyRoute,
RegisterOneTimePasswordRoute,
LogoutRoute,
ConsentRoute,
} from "@constants/Routes"; } from "@constants/Routes";
import NotificationsContext from "@hooks/NotificationsContext"; import NotificationsContext from "@hooks/NotificationsContext";
import { Notification } from "@models/Notifications"; import { Notification } from "@models/Notifications";
@ -20,7 +20,7 @@ import * as themes from "@themes/index";
import { getBasePath } from "@utils/BasePath"; import { getBasePath } from "@utils/BasePath";
import { getDuoSelfEnrollment, getRememberMe, getResetPassword, getTheme } from "@utils/Configuration"; import { getDuoSelfEnrollment, getRememberMe, getResetPassword, getTheme } from "@utils/Configuration";
import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword"; import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword";
import RegisterSecurityKey from "@views/DeviceRegistration/RegisterSecurityKey"; import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn";
import ConsentView from "@views/LoginPortal/ConsentView/ConsentView"; import ConsentView from "@views/LoginPortal/ConsentView/ConsentView";
import LoginPortal from "@views/LoginPortal/LoginPortal"; import LoginPortal from "@views/LoginPortal/LoginPortal";
import SignOut from "@views/LoginPortal/SignOut/SignOut"; import SignOut from "@views/LoginPortal/SignOut/SignOut";
@ -67,12 +67,12 @@ const App: React.FC = () => {
<Routes> <Routes>
<Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} /> <Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} />
<Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} /> <Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} />
<Route path={RegisterSecurityKeyRoute} element={<RegisterSecurityKey />} /> <Route path={RegisterWebauthnRoute} element={<RegisterWebauthn />} />
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} /> <Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
<Route path={LogoutRoute} element={<SignOut />} /> <Route path={LogoutRoute} element={<SignOut />} />
<Route path={ConsentRoute} element={<ConsentView />} /> <Route path={ConsentRoute} element={<ConsentView />} />
<Route <Route
path={`${FirstFactorRoute}*`} path={`${IndexRoute}*`}
element={ element={
<LoginPortal <LoginPortal
duoSelfEnrollment={getDuoSelfEnrollment()} duoSelfEnrollment={getDuoSelfEnrollment()}

View File

@ -1,14 +1,14 @@
export const FirstFactorRoute: string = "/"; export const IndexRoute: string = "/";
export const AuthenticatedRoute: string = "/authenticated"; export const AuthenticatedRoute: string = "/authenticated";
export const ConsentRoute: string = "/consent"; export const ConsentRoute: string = "/consent";
export const SecondFactorRoute: string = "/2fa/"; export const SecondFactorRoute: string = "/2fa/";
export const SecondFactorU2FSubRoute: string = "security-key"; export const SecondFactorWebauthnSubRoute: string = "webauthn";
export const SecondFactorTOTPSubRoute: string = "one-time-password"; export const SecondFactorTOTPSubRoute: string = "one-time-password";
export const SecondFactorPushSubRoute: string = "push-notification"; export const SecondFactorPushSubRoute: string = "push-notification";
export const ResetPasswordStep1Route: string = "/reset-password/step1"; export const ResetPasswordStep1Route: string = "/reset-password/step1";
export const ResetPasswordStep2Route: string = "/reset-password/step2"; export const ResetPasswordStep2Route: string = "/reset-password/step2";
export const RegisterSecurityKeyRoute: string = "/security-key/register"; export const RegisterWebauthnRoute: string = "/webauthn/register";
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register"; export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
export const LogoutRoute: string = "/logout"; export const LogoutRoute: string = "/logout";

View File

@ -34,7 +34,7 @@
"Reset": "Reset", "Reset": "Reset",
"Scan QR Code": "Scan QR Code", "Scan QR Code": "Scan QR Code",
"Secret": "Secret", "Secret": "Secret",
"Security Key - U2F": "Security Key - U2F", "Security Key - WebAuthN": "Security Key - WebAuthN",
"Select a Device": "Select a Device", "Select a Device": "Select a Device",
"Sign in": "Sign in", "Sign in": "Sign in",
"Sign out": "Sign out", "Sign out": "Sign out",

View File

@ -34,7 +34,7 @@
"Reset": "Restablecer", "Reset": "Restablecer",
"Scan QR Code": "Escanear Código QR", "Scan QR Code": "Escanear Código QR",
"Secret": "Secreto", "Secret": "Secreto",
"Security Key - U2F": "Llave de Seguridad - U2F", "Security Key - WebAuthN": "Llave de Seguridad - WebAuthN",
"Select a Device": "Seleccionar Dispositivo", "Select a Device": "Seleccionar Dispositivo",
"Sign in": "Iniciar Sesión", "Sign in": "Iniciar Sesión",
"Sign out": "Cerrar Sesión", "Sign out": "Cerrar Sesión",

View File

@ -2,5 +2,4 @@ import { SecondFactorMethod } from "@models/Methods";
export interface Configuration { export interface Configuration {
available_methods: Set<SecondFactorMethod>; available_methods: Set<SecondFactorMethod>;
second_factor_enabled: boolean;
} }

View File

@ -1,5 +1,5 @@
export enum SecondFactorMethod { export enum SecondFactorMethod {
TOTP = 1, TOTP = 1,
U2F = 2, Webauthn,
MobilePush = 3, MobilePush,
} }

View File

@ -3,7 +3,7 @@ import { SecondFactorMethod } from "@models/Methods";
export interface UserInfo { export interface UserInfo {
display_name: string; display_name: string;
method: SecondFactorMethod; method: SecondFactorMethod;
has_u2f: boolean; has_webauthn: boolean;
has_totp: boolean; has_totp: boolean;
has_duo: boolean; has_duo: boolean;
} }

129
web/src/models/Webauthn.ts Normal file
View File

@ -0,0 +1,129 @@
export interface PublicKeyCredentialCreationOptionsStatus {
options?: PublicKeyCredentialCreationOptions;
status: number;
}
export interface CredentialCreation {
publicKey: PublicKeyCredentialCreationOptionsJSON;
}
export interface PublicKeyCredentialCreationOptionsJSON
extends Omit<PublicKeyCredentialCreationOptions, "challenge" | "excludeCredentials" | "user"> {
challenge: string;
excludeCredentials?: PublicKeyCredentialDescriptorJSON[];
user: PublicKeyCredentialUserEntityJSON;
}
export interface PublicKeyCredentialRequestOptionsStatus {
options?: PublicKeyCredentialRequestOptions;
status: number;
}
export interface CredentialRequest {
publicKey: PublicKeyCredentialRequestOptionsJSON;
}
export interface PublicKeyCredentialRequestOptionsJSON
extends Omit<PublicKeyCredentialRequestOptions, "allowCredentials" | "challenge"> {
allowCredentials?: PublicKeyCredentialDescriptorJSON[];
challenge: string;
}
export interface PublicKeyCredentialDescriptorJSON extends Omit<PublicKeyCredentialDescriptor, "id"> {
id: string;
}
export interface PublicKeyCredentialUserEntityJSON extends Omit<PublicKeyCredentialUserEntity, "id"> {
id: string;
}
export interface AuthenticatorAssertionResponseJSON
extends Omit<AuthenticatorAssertionResponse, "authenticatorData" | "clientDataJSON" | "signature" | "userHandle"> {
authenticatorData: string;
clientDataJSON: string;
signature: string;
userHandle: string;
}
export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAttestationResponse {
getTransports?: () => AuthenticatorTransport[];
getAuthenticatorData?: () => ArrayBuffer;
getPublicKey?: () => ArrayBuffer;
getPublicKeyAlgorithm?: () => COSEAlgorithmIdentifier[];
}
export interface AttestationPublicKeyCredential extends PublicKeyCredential {
response: AuthenticatorAttestationResponseFuture;
}
export interface AuthenticatorAttestationResponseJSON
extends Omit<AuthenticatorAttestationResponseFuture, "clientDataJSON" | "attestationObject"> {
clientDataJSON: string;
attestationObject: string;
}
export interface AttestationPublicKeyCredentialJSON
extends Omit<AttestationPublicKeyCredential, "response" | "rawId" | "getClientExtensionResults"> {
rawId: string;
response: AuthenticatorAttestationResponseJSON;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
transports?: AuthenticatorTransport[];
}
export interface PublicKeyCredentialJSON
extends Omit<PublicKeyCredential, "rawId" | "response" | "getClientExtensionResults"> {
rawId: string;
clientExtensionResults: AuthenticationExtensionsClientOutputs;
response: AuthenticatorAssertionResponseJSON;
targetURL?: string;
}
export enum AttestationResult {
Success = 1,
Failure,
FailureExcluded,
FailureUserConsent,
FailureUserVerificationOrResidentKey,
FailureSyntax,
FailureSupport,
FailureUnknown,
FailureWebauthnNotSupported,
FailureToken,
}
export interface AttestationPublicKeyCredentialResult {
credential?: AttestationPublicKeyCredential;
result: AttestationResult;
}
export interface AttestationPublicKeyCredentialResultJSON {
credential?: AttestationPublicKeyCredentialJSON;
result: AttestationResult;
}
export enum AssertionResult {
Success = 1,
Failure,
FailureUserConsent,
FailureU2FFacetID,
FailureSyntax,
FailureUnknown,
FailureUnknownSecurity,
FailureWebauthnNotSupported,
FailureChallenge,
}
export interface DiscoverableAssertionResult {
result: AssertionResult;
username: string;
}
export interface AssertionPublicKeyCredentialResult {
credential?: PublicKeyCredential;
result: AssertionResult;
}
export interface AssertionPublicKeyCredentialResultJSON {
credential?: PublicKeyCredentialJSON;
result: AssertionResult;
}

View File

@ -11,12 +11,11 @@ export const FirstFactorPath = basePath + "/api/firstfactor";
export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start"; export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start";
export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish"; export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
export const InitiateU2FRegistrationPath = basePath + "/api/secondfactor/u2f/identity/start"; export const WebauthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start";
export const CompleteU2FRegistrationStep1Path = basePath + "/api/secondfactor/u2f/identity/finish"; export const WebauthnIdentityFinishPath = basePath + "/api/secondfactor/webauthn/identity/finish";
export const CompleteU2FRegistrationStep2Path = basePath + "/api/secondfactor/u2f/register"; export const WebauthnAttestationPath = basePath + "/api/secondfactor/webauthn/attestation";
export const InitiateU2FSignInPath = basePath + "/api/secondfactor/u2f/sign_request"; export const WebauthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion";
export const CompleteU2FSignInPath = basePath + "/api/secondfactor/u2f/sign";
export const InitiateDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_devices"; export const InitiateDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_devices";
export const CompleteDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_device"; export const CompleteDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_device";
@ -48,6 +47,12 @@ export interface Response<T> {
data: T; data: T;
} }
export interface OptionalDataResponse<T> {
status: "OK";
data?: T;
}
export type OptionalDataServiceResponse<T> = OptionalDataResponse<T> | ErrorResponse;
export type ServiceResponse<T> = Response<T> | ErrorResponse; export type ServiceResponse<T> = Response<T> | ErrorResponse;
function toErrorResponse<T>(resp: AxiosResponse<ServiceResponse<T>>): ErrorResponse | undefined { function toErrorResponse<T>(resp: AxiosResponse<ServiceResponse<T>>): ErrorResponse | undefined {

View File

@ -5,7 +5,6 @@ import { toEnum, Method2FA } from "@services/UserInfo";
interface ConfigurationPayload { interface ConfigurationPayload {
available_methods: Method2FA[]; available_methods: Method2FA[];
second_factor_enabled: boolean;
} }
export async function getConfiguration(): Promise<Configuration> { export async function getConfiguration(): Promise<Configuration> {

Some files were not shown because too many files have changed in this diff Show More