mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
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:
parent
3c0d9b3b57
commit
8f05846e21
10
README.md
10
README.md
|
@ -52,9 +52,9 @@ Here is what Authelia's portal looks like:
|
|||
This is a list of the key features of Authelia:
|
||||
|
||||
* 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)**
|
||||
with [Google Authenticator].
|
||||
with compatible authenticator applications.
|
||||
* **[Mobile Push Notifications](https://www.authelia.com/docs/features/2fa/push-notifications)**
|
||||
with [Duo](https://duo.com/).
|
||||
* 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
|
||||
[TOTP]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
|
||||
[Security Key]: https://www.yubico.com/about/background/fido/
|
||||
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
|
||||
[FIDO2]: https://www.yubico.com/authentication-standards/fido2/
|
||||
[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
|
||||
[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
|
||||
[config.template.yml]: ./config.template.yml
|
||||
[nginx]: https://www.nginx.com/
|
||||
[Traefik]: https://traefik.io/
|
||||
|
|
376
api/openapi.yml
376
api/openapi.yml
|
@ -23,7 +23,7 @@ tags:
|
|||
- name: User Information
|
||||
description: User configuration endpoints
|
||||
- name: Second Factor
|
||||
description: TOTP, U2F and Duo endpoints
|
||||
description: TOTP, Webauthn and Duo endpoints
|
||||
paths:
|
||||
/api/configuration:
|
||||
get:
|
||||
|
@ -430,35 +430,34 @@ paths:
|
|||
$ref: '#/components/schemas/middlewares.ErrorResponse'
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/secondfactor/u2f/sign_request:
|
||||
post:
|
||||
/api/secondfactor/webauthn/assertion:
|
||||
get:
|
||||
tags:
|
||||
- Second Factor
|
||||
summary: Second Factor Authentication - U2F (Request)
|
||||
description: This endpoint starts the second factor authentication process with the U2F key.
|
||||
summary: Second Factor Authentication - Webauthn (Request)
|
||||
description: This endpoint starts the second factor authentication process with the FIDO2 Webauthn credential.
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/u2f.WebSignRequest'
|
||||
$ref: '#/components/schemas/webauthn.PublicKeyCredentialRequestOptions'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/secondfactor/u2f/sign:
|
||||
post:
|
||||
tags:
|
||||
- Second Factor
|
||||
summary: Second Factor Authentication - U2F
|
||||
description: "This endpoint completes second factor authentication with a U2F key."
|
||||
summary: Second Factor Authentication - Webauthn
|
||||
description: This endpoint completes the second factor authentication process with the FIDO2 Webauthn credential.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/handlers.signU2FRequestBody"
|
||||
$ref: "#/components/schemas/webauthn.CredentialAssertionResponse"
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
|
@ -470,16 +469,17 @@ paths:
|
|||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/secondfactor/u2f/identity/start:
|
||||
/api/secondfactor/webauthn/identity/start:
|
||||
post:
|
||||
tags:
|
||||
- Second Factor
|
||||
summary: Identity Verification U2F Token Creation
|
||||
summary: Identity Verification Webauthn Credential Creation
|
||||
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
|
||||
`/api/secondfactor/u2f/identity/finish` and `/api/secondfactor/u2f/register` endpoints.
|
||||
`/api/secondfactor/webauthn/identity/finish` and `/api/secondfactor/webauthn/attestation` endpoints.
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
|
@ -489,17 +489,17 @@ paths:
|
|||
$ref: '#/components/schemas/middlewares.OkResponse'
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/secondfactor/u2f/identity/finish:
|
||||
/api/secondfactor/webauthn/identity/finish:
|
||||
post:
|
||||
tags:
|
||||
- Second Factor
|
||||
summary: Identity Verification U2F Token Validation
|
||||
summary: Identity Verification FIDO2 Webauthn Credential Validation
|
||||
description: >
|
||||
This endpoint performs identity and token verification, upon success generates a U2F device registration
|
||||
challenge.
|
||||
This endpoint performs identity and token verification, upon success generates a FIDO2 Webauthn device
|
||||
attestation challenge (registration).
|
||||
|
||||
The session cookie generated from the `/api/secondfactor/u2f/identity/start` endpoint must be utilised for the
|
||||
subsequent steps here and in the `/api/secondfactor/u2f/register` endpoint.
|
||||
The session cookie generated from the `/api/secondfactor/webauthn/identity/start` endpoint must be utilised
|
||||
for the subsequent steps here and in the `/api/secondfactor/webauthn/attestation` endpoint.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
@ -512,21 +512,21 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/u2f.WebRegisterRequest'
|
||||
$ref: '#/components/schemas/webauthn.PublicKeyCredentialCreationOptions'
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/secondfactor/u2f/register:
|
||||
/api/secondfactor/webauthn/attestation:
|
||||
post:
|
||||
tags:
|
||||
- Second Factor
|
||||
summary: U2F Device Registration
|
||||
description: This endpoint performs U2F device registration.
|
||||
summary: Webauthn Credential Attestation
|
||||
description: This endpoint performs Webauthn credential attestation (registration).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/u2f.RegisterResponse'
|
||||
$ref: '#/components/schemas/webauthn.CredentialAttestationResponse'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
|
@ -653,12 +653,13 @@ components:
|
|||
properties:
|
||||
available_methods:
|
||||
type: array
|
||||
description: List of available 2FA methods. If no methods exist 2FA is disabled.
|
||||
items:
|
||||
type: string
|
||||
example: [totp, u2f, mobile_push]
|
||||
second_factor_enabled:
|
||||
type: boolean
|
||||
description: If second factor is enabled.
|
||||
enum:
|
||||
- "totp"
|
||||
- "webauthn"
|
||||
- "mobile_push"
|
||||
example: [totp, webauthn, mobile_push]
|
||||
handlers.DuoDeviceBody:
|
||||
required:
|
||||
- device
|
||||
|
@ -781,24 +782,6 @@ components:
|
|||
targetURL:
|
||||
type: string
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -846,9 +829,12 @@ components:
|
|||
example: John Doe
|
||||
method:
|
||||
type: string
|
||||
enum: [totp, u2f, mobile_push]
|
||||
enum:
|
||||
- "totp"
|
||||
- "webauthn"
|
||||
- "mobile_push"
|
||||
example: totp
|
||||
has_u2f:
|
||||
has_webauthn:
|
||||
type: boolean
|
||||
example: false
|
||||
has_totp:
|
||||
|
@ -883,7 +869,10 @@ components:
|
|||
properties:
|
||||
method:
|
||||
type: string
|
||||
enum: [totp, u2f, mobile_push]
|
||||
enum:
|
||||
- "totp"
|
||||
- "webauthn"
|
||||
- "mobile_push"
|
||||
example: totp
|
||||
middlewares.ErrorResponse:
|
||||
type: object
|
||||
|
@ -910,16 +899,70 @@ components:
|
|||
example: OK
|
||||
data:
|
||||
type: object
|
||||
u2f.RegisterResponse:
|
||||
webauthn.PublicKeyCredential:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
rawId:
|
||||
type: string
|
||||
registrationData:
|
||||
format: byte
|
||||
id:
|
||||
type: string
|
||||
clientData:
|
||||
type:
|
||||
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
|
||||
properties:
|
||||
status:
|
||||
|
@ -928,35 +971,50 @@ components:
|
|||
data:
|
||||
type: object
|
||||
properties:
|
||||
appId:
|
||||
type: string
|
||||
example: https://auth.example.com
|
||||
registerRequests:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
publicKey:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/webauthn.AttestationType'
|
||||
- $ref: '#/components/schemas/webauthn.AuthenticatorSelectionCriteria'
|
||||
- $ref: '#/components/schemas/webauthn.CredentialUserEntity'
|
||||
- $ref: '#/components/schemas/webauthn.CredentialRPEntity'
|
||||
- type: object
|
||||
required:
|
||||
- "challenge"
|
||||
- "pubKeyCredParams"
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
example: U2F_V2
|
||||
challenge:
|
||||
type: string
|
||||
example: XGYKUzSmTpM1KxxpekArviW0w0OU2pwwRAocgn8TkVQ
|
||||
registeredKeys:
|
||||
format: byte
|
||||
pubKeyCredParams:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- "alg"
|
||||
- "type"
|
||||
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
|
||||
example: https://auth.example.com
|
||||
version:
|
||||
type: string
|
||||
example: U2F_V2
|
||||
keyHandle:
|
||||
type: string
|
||||
example: pWgBrwr9meS5vArdffPtD4Px6AqZS7MfGEf776Rz438ujwHjeXwQEZuK53sRQ4wjeAgRCW4wX9VRj8dyKjc273
|
||||
u2f.WebSignRequest:
|
||||
webauthn.PublicKeyCredentialRequestOptions:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
|
@ -965,26 +1023,172 @@ components:
|
|||
data:
|
||||
type: object
|
||||
properties:
|
||||
appId:
|
||||
type: string
|
||||
example: https://auth.example.com
|
||||
publicKey:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/webauthn.UserVerification'
|
||||
- type: object
|
||||
required:
|
||||
- "challenge"
|
||||
properties:
|
||||
challenge:
|
||||
type: string
|
||||
example: XGYKUzSmTpM1KxxpekArviW0w0OU2pwwRAocgn8TkVQ
|
||||
registeredKeys:
|
||||
timeout:
|
||||
type: integer
|
||||
example: 60000
|
||||
rpId:
|
||||
type: string
|
||||
example: auth.example.com
|
||||
allowCredentials:
|
||||
type: array
|
||||
items:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/webauthn.CredentialDescriptor'
|
||||
extensions:
|
||||
type: object
|
||||
properties:
|
||||
appId:
|
||||
appid:
|
||||
type: string
|
||||
example: https://auth.example.com
|
||||
version:
|
||||
webauthn.Transports:
|
||||
type: object
|
||||
properties:
|
||||
transports:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: U2F_V2
|
||||
keyHandle:
|
||||
example:
|
||||
- "usb"
|
||||
- "nfc"
|
||||
enum:
|
||||
- "usb"
|
||||
- "nfc"
|
||||
- "ble"
|
||||
- "internal"
|
||||
webauthn.UserVerification:
|
||||
type: object
|
||||
properties:
|
||||
userVerification:
|
||||
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:
|
||||
authelia_auth:
|
||||
type: apiKey
|
||||
|
|
|
@ -98,6 +98,9 @@ log:
|
|||
##
|
||||
## Parameters used for TOTP generation.
|
||||
totp:
|
||||
## Disable TOTP.
|
||||
disable: false
|
||||
|
||||
## The issuer name displayed in the Authenticator application of your choice.
|
||||
issuer: authelia.com
|
||||
|
||||
|
@ -121,6 +124,28 @@ totp:
|
|||
skew: 1
|
||||
## 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
|
||||
##
|
||||
|
@ -576,7 +601,7 @@ storage:
|
|||
##
|
||||
## 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.
|
||||
notifier:
|
||||
## You can disable the notifier startup check by setting this to true.
|
||||
|
|
|
@ -14,6 +14,7 @@ full example of TOTP configuration below, as well as sections describing them.
|
|||
## Configuration
|
||||
```yaml
|
||||
totp:
|
||||
disable: false
|
||||
issuer: authelia.com
|
||||
algorithm: sha1
|
||||
digits: 6
|
||||
|
@ -23,6 +24,18 @@ totp:
|
|||
|
||||
## 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
|
||||
<div markdown="1">
|
||||
type: string
|
||||
|
|
|
@ -19,6 +19,7 @@ For example for version pre1, it is used for all versions between it and the ver
|
|||
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 |
|
||||
|:------------:|:--------------:|:----------------------------------------------------------:|
|
||||
|:--------------:|:----------------:|:-------------------------------------------------------------------------------------------------:|
|
||||
| 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 |
|
||||
|
|
109
docs/configuration/webauthn.md
Normal file
109
docs/configuration/webauthn.md
Normal 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.
|
|
@ -10,8 +10,8 @@ has_children: true
|
|||
|
||||
There are multiple supported options for the second factor.
|
||||
|
||||
* Time-based One-Time passwords with [Google Authenticator]
|
||||
* Security Keys with tokens like [Yubikey].
|
||||
* Time-based One-Time passwords with compatible authenticator applications.
|
||||
* Security Keys that support [FIDO2] [Webauthn] with devices like a [YubiKey].
|
||||
* Push notifications on your mobile using [Duo].
|
||||
|
||||
<p align="center">
|
||||
|
@ -20,5 +20,6 @@ There are multiple supported options for the second factor.
|
|||
|
||||
|
||||
[Duo]: https://duo.com/
|
||||
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
|
||||
[Google Authenticator]: https://google-authenticator.com/
|
||||
[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/
|
|
@ -8,35 +8,34 @@ grand_parent: Features
|
|||
|
||||
# Security Keys
|
||||
|
||||
**Authelia** supports hardware-based second factors leveraging security keys like
|
||||
**Authelia** supports hardware-based second factors leveraging [FIDO2] [Webauthn] compatible security keys like
|
||||
[YubiKey]'s.
|
||||
|
||||
Security keys are among the most secure second factor. This method is already
|
||||
supported by many major applications and platforms like Google, Facebook, Github,
|
||||
some banks, and much more...
|
||||
Security keys are among the most secure second factor. This method is already supported by many major applications and
|
||||
platforms like Google, Facebook, GitHub, some banks, and much more.
|
||||
|
||||
<p align="center">
|
||||
<img src="../../images/yubikey.jpg" width="150">
|
||||
</p>
|
||||
|
||||
Normally, the protocol requires your security key to be enrolled on each site before
|
||||
being able to authenticate with it. Since Authelia provides Single Sign-On, your users
|
||||
will need to enroll their device only once to get access to all your applications.
|
||||
Normally, the protocol requires your security key to be enrolled on each site before being able to authenticate with it.
|
||||
Since Authelia provides Single Sign-On, your users will need to enroll their device only once to get access to all your
|
||||
applications.
|
||||
|
||||
<p align="center">
|
||||
<img src="../../images/REGISTER-U2F.png" width="400">
|
||||
</p>
|
||||
|
||||
After having successfully passed the first factor, select *Security Key* method and
|
||||
click on *Register device* link. This will send you an email to verify your identity.
|
||||
After having successfully passed the first factor, select *Security Key* method and click on *Register device* link.
|
||||
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.*
|
||||
|
||||
Confirm your identity by clicking on **Register** and you'll be asked to
|
||||
touch the token of your security key to complete the enrollment.
|
||||
Confirm your identity by clicking on **Register** and you'll be asked to touch the token of your security key to
|
||||
complete the enrollment.
|
||||
|
||||
Upon successful enrollment, you can authenticate using your security key
|
||||
by simply touching the token again when requested:
|
||||
Upon successful enrollment, you can authenticate using your security key by simply touching the token again when
|
||||
requested:
|
||||
|
||||
<p align="center">
|
||||
<img src="../../images/2FA-U2F.png" width="400">
|
||||
|
@ -44,20 +43,32 @@ by simply touching the token again when requested:
|
|||
|
||||
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
|
||||
|
||||
### 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?
|
||||
|
||||
U2F protocol is a new protocol that is only supported by recent browsers
|
||||
and might even be enabled on some of them. Please be sure your browser
|
||||
supports U2F and that the feature is enabled to make the option
|
||||
available in **Authelia**.
|
||||
The [Webauthn] protocol is a new protocol that is only supported by modern browsers. Please ensure your browser is up to
|
||||
date, supports [Webauthn], and that the feature is not disabled if the option is not available to you 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] [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/
|
||||
|
|
|
@ -29,8 +29,8 @@ protect your apps.
|
|||
|
||||
Multiple 2-factor methods are available for satisfying every users.
|
||||
|
||||
* Time-based One-Time passwords with [Google Authenticator].
|
||||
* Security Keys with tokens like [Yubikey].
|
||||
* Time-based One-Time passwords with compatible authenticator applications.
|
||||
* Security Keys that support [FIDO2] [Webauthn] with devices like a [YubiKey].
|
||||
* Push notifications on your mobile using [Duo].
|
||||
|
||||
**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/
|
||||
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
|
||||
[Google Authenticator]: https://google-authenticator.com/
|
||||
[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/
|
||||
|
|
|
@ -90,9 +90,9 @@ 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:
|
||||
|
||||
| Table | Column | Rational |
|
||||
|:-----------------:|:--------:|:----------------------------------------------------------------------------------------------------:|
|
||||
|:-------------------:|:----------:|:------------------------------------------------------------------------------------------------------:|
|
||||
| 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
|
||||
|
||||
|
|
9
go.mod
9
go.mod
|
@ -6,6 +6,7 @@ require (
|
|||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
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/fasthttp/router v1.4.6
|
||||
github.com/fasthttp/session/v2 v2.4.7
|
||||
|
@ -30,7 +31,6 @@ require (
|
|||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tstranex/u2f v1.0.0
|
||||
github.com/valyala/fasthttp v1.33.0
|
||||
golang.org/x/text v0.3.7
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
|
@ -42,17 +42,20 @@ require (
|
|||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // 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/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // 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-redis/redis/v8 v8.11.4 // indirect
|
||||
github.com/gobuffalo/pop/v5 v5.3.3 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // 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/hashicorp/hcl 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/tinylib/msgp v1.1.6 // 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/gson v0.6.4 // 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/metric 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/net v0.0.0-20220111093109-d55c255bac03 // indirect
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
|
||||
|
@ -110,6 +114,7 @@ require (
|
|||
)
|
||||
|
||||
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/tidwall/gjson => github.com/tidwall/gjson v1.11.0
|
||||
)
|
||||
|
|
43
go.sum
43
go.sum
|
@ -1,4 +1,5 @@
|
|||
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.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
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/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/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/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs=
|
||||
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/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/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-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
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-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/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-sdk-go v1.23.19/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/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/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/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
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/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/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-20200629203442-efcf912fb354/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.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
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/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=
|
||||
|
@ -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.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
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/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
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/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/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.3.0/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.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/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 v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
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.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
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.14.1 h1:hLQYb23E8/fO+1u53d02A97a8UnsddcvYzq4ERRU4ds=
|
||||
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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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 v1.0.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/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/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-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
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/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/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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
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/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/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/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=
|
||||
|
@ -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/fasthttp v1.33.0 h1:mHBKd98J5NcXuBddgjvim1i3kWzlng1SzLhrnBOU9g8=
|
||||
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/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/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=
|
||||
|
@ -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/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/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.13.0/go.mod h1:dylGv2HKR0tiCV+wliJz1KHtDyuD8SPe69oV7VyK6WY=
|
||||
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-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-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-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-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-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-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-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-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-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-20180807140117-3d87b88a115f/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-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-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-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
|
|
|
@ -19,8 +19,8 @@ const (
|
|||
const (
|
||||
// TOTP Method using Time-Based One-Time Password applications like Google Authenticator.
|
||||
TOTP = "totp"
|
||||
// U2F Method using U2F devices like Yubikeys.
|
||||
U2F = "u2f"
|
||||
// Webauthn Method using Webauthn devices like YubiKeys.
|
||||
Webauthn = "webauthn"
|
||||
// Push Method using Duo application to receive push notifications.
|
||||
Push = "mobile_push"
|
||||
)
|
||||
|
@ -37,7 +37,7 @@ const (
|
|||
)
|
||||
|
||||
// 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.
|
||||
type CryptAlgo string
|
||||
|
|
|
@ -98,6 +98,9 @@ log:
|
|||
##
|
||||
## Parameters used for TOTP generation.
|
||||
totp:
|
||||
## Disable TOTP.
|
||||
disable: false
|
||||
|
||||
## The issuer name displayed in the Authenticator application of your choice.
|
||||
issuer: authelia.com
|
||||
|
||||
|
@ -121,6 +124,28 @@ totp:
|
|||
skew: 1
|
||||
## 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
|
||||
##
|
||||
|
@ -576,7 +601,7 @@ storage:
|
|||
##
|
||||
## 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.
|
||||
notifier:
|
||||
## You can disable the notifier startup check by setting this to true.
|
||||
|
|
|
@ -25,15 +25,14 @@ func StringToMailAddressFunc() mapstructure.DecodeHookFunc {
|
|||
}
|
||||
|
||||
var (
|
||||
mailAddress *mail.Address
|
||||
parsedAddress *mail.Address
|
||||
)
|
||||
|
||||
mailAddress, err = mail.ParseAddress(dataStr)
|
||||
if err != nil {
|
||||
if parsedAddress, err = mail.ParseAddress(dataStr); err != nil {
|
||||
return nil, fmt.Errorf("could not parse '%s' as a RFC5322 address: %w", dataStr, err)
|
||||
}
|
||||
|
||||
return *mailAddress, nil
|
||||
return *parsedAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,18 +22,18 @@ func TestShouldErrorSecretNotExist(t *testing.T) {
|
|||
dir, err := os.MkdirTemp("", "authelia-test-secret-not-exist")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", filepath.Join(dir, "jwt")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"DUO_API_SECRET_KEY_FILE", filepath.Join(dir, "duo")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", filepath.Join(dir, "session")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", filepath.Join(dir, "authentication")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"NOTIFIER_SMTP_PASSWORD_FILE", filepath.Join(dir, "notifier")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"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")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", filepath.Join(dir, "mysql")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_POSTGRES_PASSWORD_FILE", filepath.Join(dir, "postgres")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"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")))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE", filepath.Join(dir, "oidc-hmac")))
|
||||
testSetEnv(t, "JWT_SECRET_FILE", filepath.Join(dir, "jwt"))
|
||||
testSetEnv(t, "DUO_API_SECRET_KEY_FILE", filepath.Join(dir, "duo"))
|
||||
testSetEnv(t, "SESSION_SECRET_FILE", filepath.Join(dir, "session"))
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", filepath.Join(dir, "authentication"))
|
||||
testSetEnv(t, "NOTIFIER_SMTP_PASSWORD_FILE", filepath.Join(dir, "notifier"))
|
||||
testSetEnv(t, "SESSION_REDIS_PASSWORD_FILE", filepath.Join(dir, "redis"))
|
||||
testSetEnv(t, "SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", filepath.Join(dir, "redis-sentinel"))
|
||||
testSetEnv(t, "STORAGE_MYSQL_PASSWORD_FILE", filepath.Join(dir, "mysql"))
|
||||
testSetEnv(t, "STORAGE_POSTGRES_PASSWORD_FILE", filepath.Join(dir, "postgres"))
|
||||
testSetEnv(t, "SERVER_TLS_KEY_FILE", filepath.Join(dir, "tls"))
|
||||
testSetEnv(t, "IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE", filepath.Join(dir, "oidc-key"))
|
||||
testSetEnv(t, "IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE", filepath.Join(dir, "oidc-hmac"))
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
_, _, err = Load(val, NewEnvironmentSource(DefaultEnvPrefix, DefaultEnvDelimiter), NewSecretsSource(DefaultEnvPrefix, DefaultEnvDelimiter))
|
||||
|
@ -76,10 +76,10 @@ func TestLoadShouldReturnErrWithoutSources(t *testing.T) {
|
|||
func TestShouldHaveNotifier(t *testing.T) {
|
||||
testReset()
|
||||
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc"))
|
||||
testSetEnv(t, "SESSION_SECRET", "abc")
|
||||
testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "abc")
|
||||
testSetEnv(t, "JWT_SECRET", "abc")
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
_, 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) {
|
||||
testReset()
|
||||
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc"))
|
||||
testSetEnv(t, "SESSION_SECRET", "abc")
|
||||
testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "abc")
|
||||
testSetEnv(t, "JWT_SECRET", "abc")
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
_, _, 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) {
|
||||
testReset()
|
||||
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "an env session secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL", "a bad env"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "an env jwt secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"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, "SESSION_SECRET", "an env session secret")
|
||||
testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "an env storage mysql password")
|
||||
testSetEnv(t, "STORAGE_MYSQL", "a bad env")
|
||||
testSetEnv(t, "JWT_SECRET", "an env jwt secret")
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password")
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_URL", "an env authentication backend ldap password")
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
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) {
|
||||
testReset()
|
||||
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "an env session secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"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, "SESSION_SECRET", "an env session secret")
|
||||
testSetEnv(t, "SESSION_SECRET_FILE", "./test_resources/example_secret")
|
||||
testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "an env storage mysql password")
|
||||
testSetEnv(t, "JWT_SECRET_FILE", "./test_resources/example_secret")
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password")
|
||||
testSetEnv(t, "STORAGE_ENCRYPTION_KEY", "a_very_bad_encryption_key")
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
_, 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) {
|
||||
testReset()
|
||||
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"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, "SESSION_SECRET_FILE", "./test_resources/example_secret")
|
||||
testSetEnv(t, "STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret")
|
||||
testSetEnv(t, "JWT_SECRET_FILE", "./test_resources/example_secret")
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", "./test_resources/example_secret")
|
||||
testSetEnv(t, "STORAGE_ENCRYPTION_KEY_FILE", "./test_resources/example_secret")
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
_, 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) {
|
||||
testReset()
|
||||
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc"))
|
||||
testSetEnv(t, "SESSION_SECRET", "abc")
|
||||
testSetEnv(t, "STORAGE_MYSQL_PASSWORD", "abc")
|
||||
testSetEnv(t, "JWT_SECRET", "abc")
|
||||
testSetEnv(t, "AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc")
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
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))
|
||||
}
|
||||
|
||||
func testSetEnv(t *testing.T, key, value string) {
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+key, value))
|
||||
}
|
||||
|
||||
func testReset() {
|
||||
testUnsetEnvName("STORAGE_MYSQL")
|
||||
testUnsetEnvName("JWT_SECRET")
|
||||
|
|
|
@ -11,7 +11,7 @@ type Configuration struct {
|
|||
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
|
||||
AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"`
|
||||
Session SessionConfiguration `koanf:"session"`
|
||||
TOTP *TOTPConfiguration `koanf:"totp"`
|
||||
TOTP TOTPConfiguration `koanf:"totp"`
|
||||
DuoAPI *DuoAPIConfiguration `koanf:"duo_api"`
|
||||
AccessControl AccessControlConfiguration `koanf:"access_control"`
|
||||
NTP NTPConfiguration `koanf:"ntp"`
|
||||
|
@ -19,4 +19,5 @@ type Configuration struct {
|
|||
Storage StorageConfiguration `koanf:"storage"`
|
||||
Notifier *NotifierConfiguration `koanf:"notifier"`
|
||||
Server ServerConfiguration `koanf:"server"`
|
||||
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
|||
|
||||
// TOTPConfiguration represents the configuration related to TOTP options.
|
||||
type TOTPConfiguration struct {
|
||||
Disable bool `koanf:"disable"`
|
||||
Issuer string `koanf:"issuer"`
|
||||
Algorithm string `koanf:"algorithm"`
|
||||
Digits uint `koanf:"digits"`
|
||||
|
|
27
internal/configuration/schema/webauthn.go
Normal file
27
internal/configuration/schema/webauthn.go
Normal 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,
|
||||
}
|
|
@ -39,6 +39,8 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
|
|||
|
||||
ValidateTOTP(config, validator)
|
||||
|
||||
ValidateWebauthn(config, validator)
|
||||
|
||||
ValidateAuthenticationBackend(&config.AuthenticationBackend, validator)
|
||||
|
||||
ValidateAccessControl(config, validator)
|
||||
|
|
|
@ -3,6 +3,8 @@ package validator
|
|||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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.
|
||||
const (
|
||||
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 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 validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny}
|
||||
|
||||
|
@ -285,12 +296,20 @@ var ValidKeys = []string{
|
|||
"server.headers.csp_template",
|
||||
|
||||
// TOTP Keys.
|
||||
"totp.disable",
|
||||
"totp.issuer",
|
||||
"totp.algorithm",
|
||||
"totp.digits",
|
||||
"totp.period",
|
||||
"totp.skew",
|
||||
|
||||
// Webauthn Keys.
|
||||
"webauthn.disable",
|
||||
"webauthn.display_name",
|
||||
"webauthn.attestation_conveyance_preference",
|
||||
"webauthn.user_verification",
|
||||
"webauthn.timeout",
|
||||
|
||||
// DUO API Keys.
|
||||
"duo_api.hostname",
|
||||
"duo_api.enable_self_enrollment",
|
||||
|
|
|
@ -18,7 +18,7 @@ func ValidateNTP(config *schema.Configuration, validator *schema.StructValidator
|
|||
validator.Push(fmt.Errorf(errFmtNTPVersion, config.NTP.Version))
|
||||
}
|
||||
|
||||
if config.NTP.MaximumDesync == 0 {
|
||||
if config.NTP.MaximumDesync <= 0 {
|
||||
config.NTP.MaximumDesync = schema.DefaultNTPConfiguration.MaximumDesync
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ func newDefaultNTPConfig() schema.Configuration {
|
|||
}
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultNtpAddress(t *testing.T) {
|
||||
func TestShouldSetDefaultNTPValues(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultNTPConfig()
|
||||
|
||||
|
@ -23,21 +23,16 @@ func TestShouldSetDefaultNtpAddress(t *testing.T) {
|
|||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
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) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultNTPConfig()
|
||||
|
||||
ValidateNTP(&config, validator)
|
||||
|
||||
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()
|
||||
config.NTP.MaximumDesync = -1
|
||||
|
||||
ValidateNTP(&config, validator)
|
||||
|
||||
|
@ -45,16 +40,6 @@ func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) {
|
|||
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) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultNTPConfig()
|
||||
|
|
|
@ -8,11 +8,11 @@ import (
|
|||
|
||||
// ValidateRegulation validates and update regulator configuration.
|
||||
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.
|
||||
}
|
||||
|
||||
if config.Regulation.BanTime == 0 {
|
||||
if config.Regulation.BanTime <= 0 {
|
||||
config.Regulation.BanTime = schema.DefaultRegulationConfiguration.BanTime // 5 min.
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ func newDefaultRegulationConfig() schema.Configuration {
|
|||
return config
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultRegulationBanTime(t *testing.T) {
|
||||
func TestShouldSetDefaultRegulationTimeDurationsWhenUnset(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultRegulationConfig()
|
||||
|
||||
|
@ -25,12 +25,16 @@ func TestShouldSetDefaultRegulationBanTime(t *testing.T) {
|
|||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
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()
|
||||
config := newDefaultRegulationConfig()
|
||||
|
||||
config.Regulation.BanTime = -1
|
||||
config.Regulation.FindTime = -1
|
||||
|
||||
ValidateRegulation(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
|
|
|
@ -18,7 +18,7 @@ func newDefaultSessionConfig() schema.SessionConfiguration {
|
|||
return config
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultSessionName(t *testing.T) {
|
||||
func TestShouldSetDefaultSessionValues(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
|
@ -27,39 +27,25 @@ func TestShouldSetDefaultSessionName(t *testing.T) {
|
|||
assert.False(t, validator.HasWarnings())
|
||||
assert.False(t, validator.HasErrors())
|
||||
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()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
config.Expiration, config.Inactivity, config.RememberMeDuration = -1, -1, -1
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.False(t, validator.HasWarnings())
|
||||
assert.False(t, validator.HasErrors())
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
assert.Equal(t, schema.DefaultSessionConfiguration.RememberMeDuration, config.RememberMeDuration)
|
||||
}
|
||||
|
||||
func TestShouldHandleRedisConfigSuccessfully(t *testing.T) {
|
||||
|
|
|
@ -10,12 +10,6 @@ import (
|
|||
|
||||
// ValidateTOTP validates and update TOTP configuration.
|
||||
func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) {
|
||||
if config.TOTP == nil {
|
||||
config.TOTP = &schema.DefaultTOTPConfiguration
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if config.TOTP.Issuer == "" {
|
||||
config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
func TestShouldSetDefaultTOTPValues(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{},
|
||||
TOTP: schema.TOTPConfiguration{},
|
||||
}
|
||||
|
||||
ValidateTOTP(config, validator)
|
||||
|
@ -30,7 +30,7 @@ func TestShouldNormalizeTOTPAlgorithm(t *testing.T) {
|
|||
validator := schema.NewStructValidator()
|
||||
|
||||
config := &schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
TOTP: schema.TOTPConfiguration{
|
||||
Algorithm: "sha1",
|
||||
},
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
|
|||
validator := schema.NewStructValidator()
|
||||
|
||||
config := &schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
TOTP: schema.TOTPConfiguration{
|
||||
Algorithm: "sha3",
|
||||
},
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
|
|||
func TestShouldRaiseErrorWhenInvalidTOTPValues(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := &schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
TOTP: schema.TOTPConfiguration{
|
||||
Period: 5,
|
||||
Digits: 20,
|
||||
},
|
||||
|
|
34
internal/configuration/validator/webauthn.go
Normal file
34
internal/configuration/validator/webauthn.go
Normal 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))
|
||||
}
|
||||
}
|
98
internal/configuration/validator/webauthn_test.go
Normal file
98
internal/configuration/validator/webauthn_test.go
Normal 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'")
|
||||
}
|
|
@ -10,8 +10,8 @@ const (
|
|||
// ActionTOTPRegistration is the string representation of the action for which the token has been produced.
|
||||
ActionTOTPRegistration = "RegisterTOTPDevice"
|
||||
|
||||
// ActionU2FRegistration is the string representation of the action for which the token has been produced.
|
||||
ActionU2FRegistration = "RegisterU2FDevice"
|
||||
// ActionWebauthnRegistration is the string representation of the action for which the token has been produced.
|
||||
ActionWebauthnRegistration = "RegisterWebauthnDevice"
|
||||
|
||||
// ActionResetPassword is the string representation of the action for which the token has been produced.
|
||||
ActionResetPassword = "ResetPassword"
|
||||
|
|
|
@ -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")
|
|
@ -7,20 +7,27 @@ import (
|
|||
|
||||
// ConfigurationGet get the configuration accessible to authenticated users.
|
||||
func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
|
||||
body := configurationBody{}
|
||||
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
|
||||
body := configurationBody{
|
||||
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 {
|
||||
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)
|
||||
|
||||
err := ctx.SetJSONBody(body)
|
||||
if err != nil {
|
||||
if err := ctx.SetJSONBody(body); err != nil {
|
||||
ctx.Logger.Errorf("Unable to set configuration response in body: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,106 +28,171 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
|
|||
s.mock.Close()
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
|
||||
expectedBody := configurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: false,
|
||||
}
|
||||
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), expectedBody)
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldHaveAllConfiguredMethods() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
DuoAPI: &schema.DuoAPIConfiguration{},
|
||||
}
|
||||
expectedBody := configurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f", "mobile_push"},
|
||||
SecondFactorEnabled: false,
|
||||
}
|
||||
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), expectedBody)
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() {
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(
|
||||
&schema.Configuration{
|
||||
TOTP: schema.TOTPConfiguration{
|
||||
Disable: false,
|
||||
},
|
||||
Webauthn: schema.WebauthnConfiguration{
|
||||
Disable: false,
|
||||
},
|
||||
AccessControl: schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "bypass",
|
||||
DefaultPolicy: "deny",
|
||||
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: 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",
|
||||
},
|
||||
{
|
||||
Domains: []string{"def.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
},
|
||||
}})
|
||||
}}
|
||||
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
|
||||
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200OK(s.T(), configurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: true,
|
||||
AvailableMethods: []string{"totp", "webauthn", "mobile_push"},
|
||||
})
|
||||
}
|
||||
|
||||
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{},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
}
|
158
internal/handlers/handler_register_webauthn.go
Normal file
158
internal/handlers/handler_register_webauthn.go
Normal 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)
|
||||
}
|
|
@ -58,6 +58,16 @@ func SecondFactorTOTPPost(ctx *middlewares.AutheliaCtx) {
|
|||
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())
|
||||
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
|
|
|
@ -2,18 +2,17 @@ package handlers
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"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 HandlerSignTOTPSuite struct {
|
||||
|
@ -26,8 +25,6 @@ func (s *HandlerSignTOTPSuite) 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)
|
||||
}
|
||||
|
@ -56,6 +53,10 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
|
|||
|
||||
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
|
||||
|
||||
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() {
|
||||
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.StorageMock.
|
||||
EXPECT().
|
||||
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
|
||||
|
||||
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||
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.StorageMock.
|
||||
EXPECT().
|
||||
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
|
||||
|
||||
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||
Token: "abc",
|
||||
TargetURL: "https://mydomain.local",
|
||||
})
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||
|
||||
|
@ -135,7 +181,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
|
|||
|
||||
func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
|
||||
s.mock.StorageMock.EXPECT().
|
||||
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
|
||||
LoadTOTPConfiguration(s.mock.Ctx, "john").
|
||||
Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil)
|
||||
|
||||
s.mock.StorageMock.
|
||||
|
@ -149,6 +195,10 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
|
|||
RemoteIP: models.NewNullIPFromString("0.0.0.0"),
|
||||
}))
|
||||
|
||||
s.mock.StorageMock.
|
||||
EXPECT().
|
||||
UpdateTOTPConfigurationSignIn(s.mock.Ctx, gomock.Any(), gomock.Any())
|
||||
|
||||
s.mock.TOTPMock.EXPECT().
|
||||
Validate(gomock.Eq("abc"), gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")})).
|
||||
Return(true, nil)
|
||||
|
@ -187,6 +237,10 @@ func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFi
|
|||
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{
|
||||
Token: "abc",
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
204
internal/handlers/handler_sign_webauthn.go
Normal file
204
internal/handlers/handler_sign_webauthn.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -51,16 +51,16 @@ func TestMethodSetToU2F(t *testing.T) {
|
|||
},
|
||||
{
|
||||
db: models.UserInfo{
|
||||
Method: "u2f",
|
||||
HasU2F: true,
|
||||
Method: "webauthn",
|
||||
HasWebauthn: true,
|
||||
HasTOTP: true,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
db: models.UserInfo{
|
||||
Method: "u2f",
|
||||
HasU2F: true,
|
||||
Method: "webauthn",
|
||||
HasWebauthn: true,
|
||||
HasTOTP: false,
|
||||
},
|
||||
err: nil,
|
||||
|
@ -68,7 +68,7 @@ func TestMethodSetToU2F(t *testing.T) {
|
|||
{
|
||||
db: models.UserInfo{
|
||||
Method: "mobile_push",
|
||||
HasU2F: false,
|
||||
HasWebauthn: false,
|
||||
HasTOTP: false,
|
||||
},
|
||||
err: nil,
|
||||
|
@ -116,8 +116,8 @@ func TestMethodSetToU2F(t *testing.T) {
|
|||
assert.Equal(t, resp.api.Method, actualPreferences.Method)
|
||||
})
|
||||
|
||||
t.Run("registered u2f", func(t *testing.T) {
|
||||
assert.Equal(t, resp.api.HasU2F, actualPreferences.HasU2F)
|
||||
t.Run("registered webauthn", func(t *testing.T) {
|
||||
assert.Equal(t, resp.api.HasWebauthn, actualPreferences.HasWebauthn)
|
||||
})
|
||||
|
||||
t.Run("registered totp", func(t *testing.T) {
|
||||
|
@ -205,14 +205,14 @@ func (s *SaveSuite) TestShouldReturnError500WhenBadMethodProvided() {
|
|||
MethodPreferencePost(s.mock.Ctx)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}"))
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"webauthn\"}"))
|
||||
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"))
|
||||
|
||||
MethodPreferencePost(s.mock.Ctx)
|
||||
|
@ -223,9 +223,9 @@ func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
|
|||
}
|
||||
|
||||
func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() {
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}"))
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"webauthn\"}"))
|
||||
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)
|
||||
|
||||
MethodPreferencePost(s.mock.Ctx)
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/tstranex/u2f"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
)
|
||||
|
||||
|
@ -14,7 +12,6 @@ type authorizationMatching int
|
|||
// configurationBody the content returned by the configuration endpoint.
|
||||
type configurationBody struct {
|
||||
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.
|
||||
|
@ -23,9 +20,8 @@ type signTOTPRequestBody struct {
|
|||
TargetURL string `json:"targetURL"`
|
||||
}
|
||||
|
||||
// signU2FRequestBody model of the request body of U2F authentication endpoint.
|
||||
type signU2FRequestBody struct {
|
||||
SignResponse u2f.SignResponse `json:"signResponse"`
|
||||
// signWebauthnRequestBody model of the request body of Webauthn authentication endpoint.
|
||||
type signWebauthnRequestBody struct {
|
||||
TargetURL string `json:"targetURL"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
63
internal/handlers/webauthn.go
Normal file
63
internal/handlers/webauthn.go
Normal 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)
|
||||
}
|
167
internal/handlers/webauthn_test.go
Normal file
167
internal/handlers/webauthn_test.go
Normal 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")
|
||||
}
|
|
@ -5,8 +5,10 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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\",")
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -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 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 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 duo_api.go -mock_names API=MockAPI github.com/authelia/authelia/v4/internal/duo API
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStorage) LoadUserInfo(arg0 context.Context, arg1 string) (models.UserInfo, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStorage) SaveIdentityVerification(arg0 context.Context, arg1 models.IdentityVerification) error {
|
||||
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)
|
||||
}
|
||||
|
||||
// SaveU2FDevice mocks base method.
|
||||
func (m *MockStorage) SaveU2FDevice(arg0 context.Context, arg1 models.U2FDevice) error {
|
||||
// SaveWebauthnDevice mocks base method.
|
||||
func (m *MockStorage) SaveWebauthnDevice(arg0 context.Context, arg1 models.WebauthnDevice) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveU2FDevice", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "SaveWebauthnDevice", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveU2FDevice indicates an expected call of SaveU2FDevice.
|
||||
func (mr *MockStorageMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomock.Call {
|
||||
// SaveWebauthnDevice indicates an expected call of SaveWebauthnDevice.
|
||||
func (mr *MockStorageMockRecorder) SaveWebauthnDevice(arg0, arg1 interface{}) *gomock.Call {
|
||||
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.
|
||||
|
@ -457,3 +457,31 @@ func (mr *MockStorageMockRecorder) StartupCheck() *gomock.Call {
|
|||
mr.mock.ctrl.T.Helper()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
8
internal/models/const.go
Normal 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"
|
||||
)
|
|
@ -4,6 +4,7 @@ import (
|
|||
"image"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
)
|
||||
|
@ -11,6 +12,8 @@ import (
|
|||
// TOTPConfiguration represents a users TOTP configuration row in the database.
|
||||
type TOTPConfiguration struct {
|
||||
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:"-"`
|
||||
Issuer string `db:"issuer" json:"-"`
|
||||
Algorithm string `db:"algorithm" json:"-"`
|
||||
|
@ -38,6 +41,11 @@ func (c TOTPConfiguration) URI() (uri 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.
|
||||
func (c TOTPConfiguration) Key() (key *otp.Key, err error) {
|
||||
return otp.NewKeyFromURL(c.URI())
|
||||
|
|
|
@ -2,7 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
@ -26,6 +26,11 @@ func NewNullIPFromString(value string) (ip NullIP) {
|
|||
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.
|
||||
type IP struct {
|
||||
IP net.IP
|
||||
|
@ -34,16 +39,16 @@ type IP struct {
|
|||
// Value is the IP implementation of the databases/sql driver.Valuer.
|
||||
func (ip IP) Value() (value driver.Value, err error) {
|
||||
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.
|
||||
func (ip *IP) Scan(src interface{}) (err error) {
|
||||
if src == nil {
|
||||
return errors.New("cannot scan nil to type IP")
|
||||
return fmt.Errorf(errFmtScanNil, ip)
|
||||
}
|
||||
|
||||
var value string
|
||||
|
@ -54,7 +59,7 @@ func (ip *IP) Scan(src interface{}) (err error) {
|
|||
case []byte:
|
||||
value = string(v)
|
||||
default:
|
||||
return fmt.Errorf("invalid type %T for IP %v", src, src)
|
||||
return fmt.Errorf(errFmtScanInvalidType, ip, src, src)
|
||||
}
|
||||
|
||||
ip.IP = net.ParseIP(value)
|
||||
|
@ -70,10 +75,10 @@ type NullIP struct {
|
|||
// Value is the NullIP implementation of the databases/sql driver.Valuer.
|
||||
func (ip NullIP) Value() (value driver.Value, err error) {
|
||||
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.
|
||||
|
@ -91,7 +96,7 @@ func (ip *NullIP) Scan(src interface{}) (err error) {
|
|||
case []byte:
|
||||
value = string(v)
|
||||
default:
|
||||
return fmt.Errorf("invalid type %T for NullIP %v", src, src)
|
||||
return fmt.Errorf(errFmtScanInvalidType, ip, src, src)
|
||||
}
|
||||
|
||||
ip.IP = net.ParseIP(value)
|
||||
|
@ -99,6 +104,48 @@ func (ip *NullIP) Scan(src interface{}) (err error) {
|
|||
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.
|
||||
type StartupCheck interface {
|
||||
StartupCheck() (err error)
|
||||
|
|
109
internal/models/types_test.go
Normal file
109
internal/models/types_test.go
Normal 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())
|
||||
}
|
|
@ -11,8 +11,8 @@ type UserInfo struct {
|
|||
// True if a TOTP device has been registered.
|
||||
HasTOTP bool `db:"has_totp" json:"has_totp" valid:"required"`
|
||||
|
||||
// True if a security key has been registered.
|
||||
HasU2F bool `db:"has_u2f" json:"has_u2f" valid:"required"`
|
||||
// True if a Webauthn device has been registered.
|
||||
HasWebauthn bool `db:"has_webauthn" json:"has_webauthn" valid:"required"`
|
||||
|
||||
// True if a duo device has been configured as the preferred.
|
||||
HasDuo bool `db:"has_duo" json:"has_duo" valid:"required"`
|
||||
|
|
167
internal/models/webauthn.go
Normal file
167
internal/models/webauthn.go
Normal 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
|
||||
}
|
||||
}
|
|
@ -12,11 +12,8 @@ const (
|
|||
// AuthTypeTOTP is the string representing an auth log for second-factor authentication via TOTP.
|
||||
AuthTypeTOTP = "TOTP"
|
||||
|
||||
// AuthTypeU2F is the string representing an auth log for second-factor authentication via FIDO/CTAP1/U2F.
|
||||
AuthTypeU2F = "U2F"
|
||||
|
||||
// AuthTypeWebAuthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn.
|
||||
// TODO: Add WebAuthn.
|
||||
// AuthTypeWebauthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn.
|
||||
AuthTypeWebauthn = "Webauthn"
|
||||
|
||||
// AuthTypeDuo is the string representing an auth log for second-factor authentication via DUO.
|
||||
AuthTypeDuo = "Duo"
|
||||
|
|
|
@ -89,31 +89,34 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
|
|||
middlewares.RequireFirstFactor(handlers.UserInfoGet)))
|
||||
r.POST("/api/user/info/2fa_method", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.MethodPreferencePost)))
|
||||
|
||||
if !configuration.TOTP.Disable {
|
||||
// TOTP related endpoints.
|
||||
r.GET("/api/user/info/totp", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.UserTOTPGet)))
|
||||
|
||||
// TOTP related endpoints.
|
||||
r.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityStart)))
|
||||
r.POST("/api/secondfactor/totp/identity/finish", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPIdentityFinish)))
|
||||
r.POST("/api/secondfactor/totp", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorTOTPPost)))
|
||||
}
|
||||
|
||||
// U2F related endpoints.
|
||||
r.POST("/api/secondfactor/u2f/identity/start", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityStart)))
|
||||
r.POST("/api/secondfactor/u2f/identity/finish", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorU2FIdentityFinish)))
|
||||
if !configuration.Webauthn.Disable {
|
||||
// Webauthn Endpoints.
|
||||
r.POST("/api/secondfactor/webauthn/identity/start", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnIdentityStart)))
|
||||
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(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorU2FRegister)))
|
||||
|
||||
r.POST("/api/secondfactor/u2f/sign_request", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignGet)))
|
||||
|
||||
r.POST("/api/secondfactor/u2f/sign", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorU2FSignPost(&handlers.U2FVerifierImpl{}))))
|
||||
r.GET("/api/secondfactor/webauthn/assertion", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnAssertionGET)))
|
||||
r.POST("/api/secondfactor/webauthn/assertion", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.SecondFactorWebauthnAssertionPOST)))
|
||||
}
|
||||
|
||||
// Configure DUO api endpoint only if configuration exists.
|
||||
if configuration.DuoAPI != nil {
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"github.com/fasthttp/session/v2"
|
||||
"github.com/fasthttp/session/v2/providers/redis"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tstranex/u2f"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
|
@ -22,12 +22,6 @@ type ProviderConfig struct {
|
|||
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.
|
||||
type UserSession struct {
|
||||
Username string
|
||||
|
@ -43,12 +37,8 @@ type UserSession struct {
|
|||
FirstFactorAuthnTimestamp int64
|
||||
SecondFactorAuthnTimestamp int64
|
||||
|
||||
// The challenge generated in first step of U2F registration (after identity verification) or authentication.
|
||||
// This is used reused in the second phase to check that the challenge has been completed.
|
||||
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
|
||||
// Webauthn holds the session registration data for this session.
|
||||
Webauthn *webauthn.SessionData
|
||||
|
||||
// Represent an OIDC workflow session initiated by the client if not null.
|
||||
OIDCWorkflowSession *OIDCWorkflowSession
|
||||
|
|
|
@ -8,7 +8,7 @@ const (
|
|||
tableUserPreferences = "user_preferences"
|
||||
tableIdentityVerification = "identity_verification"
|
||||
tableTOTPConfigurations = "totp_configurations"
|
||||
tableU2FDevices = "u2f_devices"
|
||||
tableWebauthnDevices = "webauthn_devices"
|
||||
tableDuoDevices = "duo_devices"
|
||||
tableAuthenticationLogs = "authentication_logs"
|
||||
tableMigrations = "migrations"
|
||||
|
@ -25,6 +25,7 @@ const (
|
|||
const (
|
||||
tablePre1TOTPSecrets = "totp_secrets"
|
||||
tablePre1IdentityVerificationTokens = "identity_verification_tokens"
|
||||
tablePre1U2FDevices = "u2f_devices"
|
||||
|
||||
tablePre1Config = "config"
|
||||
|
||||
|
@ -40,9 +41,9 @@ const (
|
|||
var tablesPre1 = []string{
|
||||
tablePre1TOTPSecrets,
|
||||
tablePre1IdentityVerificationTokens,
|
||||
tablePre1U2FDevices,
|
||||
|
||||
tableUserPreferences,
|
||||
tableU2FDevices,
|
||||
tableAuthenticationLogs,
|
||||
}
|
||||
|
||||
|
@ -55,7 +56,7 @@ const (
|
|||
|
||||
const (
|
||||
// This is the latest schema version for the purpose of tests.
|
||||
testLatestVersion = 1
|
||||
testLatestVersion = 2
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -11,8 +11,8 @@ var (
|
|||
// ErrNoTOTPConfiguration error thrown when no TOTP configuration has been found in DB.
|
||||
ErrNoTOTPConfiguration = errors.New("no TOTP configuration for user")
|
||||
|
||||
// ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB.
|
||||
ErrNoU2FDeviceHandle = errors.New("no U2F device handle found")
|
||||
// ErrNoWebauthnDevice error thrown when no Webauthn device handle has been found in DB.
|
||||
ErrNoWebauthnDevice = errors.New("no Webauthn device found")
|
||||
|
||||
// ErrNoDuoDevice error thrown when no Duo device and method has been found in DB.
|
||||
ErrNoDuoDevice = errors.New("no Duo device and method saved")
|
||||
|
|
|
@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS authentication_logs (
|
|||
username VARCHAR(100) NOT NULL,
|
||||
auth_type VARCHAR(8) NOT NULL DEFAULT '1FA',
|
||||
remote_ip VARCHAR(39) NULL DEFAULT NULL,
|
||||
request_uri TEXT NOT NULL,
|
||||
request_uri TEXT,
|
||||
request_method VARCHAR(8) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
|
37
internal/storage/migrations/V0002.Webauthn.mysql.down.sql
Normal file
37
internal/storage/migrations/V0002.Webauthn.mysql.down.sql
Normal 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';
|
47
internal/storage/migrations/V0002.Webauthn.mysql.up.sql
Normal file
47
internal/storage/migrations/V0002.Webauthn.mysql.up.sql
Normal 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';
|
37
internal/storage/migrations/V0002.Webauthn.postgres.down.sql
Normal file
37
internal/storage/migrations/V0002.Webauthn.postgres.down.sql
Normal 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';
|
47
internal/storage/migrations/V0002.Webauthn.postgres.up.sql
Normal file
47
internal/storage/migrations/V0002.Webauthn.postgres.up.sql
Normal 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';
|
37
internal/storage/migrations/V0002.Webauthn.sqlite.down.sql
Normal file
37
internal/storage/migrations/V0002.Webauthn.sqlite.down.sql
Normal 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';
|
47
internal/storage/migrations/V0002.Webauthn.sqlite.up.sql
Normal file
47
internal/storage/migrations/V0002.Webauthn.sqlite.up.sql
Normal 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';
|
|
@ -22,13 +22,15 @@ type Provider interface {
|
|||
FindIdentityVerification(ctx context.Context, jti string) (found bool, 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)
|
||||
LoadTOTPConfiguration(ctx context.Context, username string) (config *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)
|
||||
LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error)
|
||||
LoadU2FDevices(ctx context.Context, limit, page int) (devices []models.U2FDevice, err error)
|
||||
SaveWebauthnDevice(ctx context.Context, device models.WebauthnDevice) (err error)
|
||||
UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt *time.Time, signCount uint32, cloneWarning bool) (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)
|
||||
DeletePreferredDuoDevice(ctx context.Context, username string) (err error)
|
||||
|
|
|
@ -44,13 +44,17 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
|
|||
|
||||
sqlUpdateTOTPConfigSecret: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecret, tableTOTPConfigurations),
|
||||
sqlUpdateTOTPConfigSecretByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecretByUsername, tableTOTPConfigurations),
|
||||
sqlUpdateTOTPConfigRecordSignIn: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignIn, tableTOTPConfigurations),
|
||||
sqlUpdateTOTPConfigRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignInByUsername, tableTOTPConfigurations),
|
||||
|
||||
sqlUpsertU2FDevice: fmt.Sprintf(queryFmtUpsertU2FDevice, tableU2FDevices),
|
||||
sqlSelectU2FDevice: fmt.Sprintf(queryFmtSelectU2FDevice, tableU2FDevices),
|
||||
sqlSelectU2FDevices: fmt.Sprintf(queryFmtSelectU2FDevices, tableU2FDevices),
|
||||
sqlUpsertWebauthnDevice: fmt.Sprintf(queryFmtUpsertWebauthnDevice, tableWebauthnDevices),
|
||||
sqlSelectWebauthnDevices: fmt.Sprintf(queryFmtSelectWebauthnDevices, tableWebauthnDevices),
|
||||
sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices),
|
||||
|
||||
sqlUpdateU2FDevicePublicKey: fmt.Sprintf(queryFmtUpdateU2FDevicePublicKey, tableU2FDevices),
|
||||
sqlUpdateU2FDevicePublicKeyByUsername: fmt.Sprintf(queryFmtUpdateUpdateU2FDevicePublicKeyByUsername, tableU2FDevices),
|
||||
sqlUpdateWebauthnDevicePublicKey: fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKey, tableWebauthnDevices),
|
||||
sqlUpdateWebauthnDevicePublicKeyByUsername: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDevicePublicKeyByUsername, tableWebauthnDevices),
|
||||
sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices),
|
||||
sqlUpdateWebauthnDeviceRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignInByUsername, tableWebauthnDevices),
|
||||
|
||||
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
|
||||
sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),
|
||||
|
@ -58,7 +62,7 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
|
|||
|
||||
sqlUpsertPreferred2FAMethod: fmt.Sprintf(queryFmtUpsertPreferred2FAMethod, 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),
|
||||
sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations),
|
||||
|
@ -102,14 +106,18 @@ type SQLProvider struct {
|
|||
|
||||
sqlUpdateTOTPConfigSecret string
|
||||
sqlUpdateTOTPConfigSecretByUsername string
|
||||
sqlUpdateTOTPConfigRecordSignIn string
|
||||
sqlUpdateTOTPConfigRecordSignInByUsername string
|
||||
|
||||
// Table: u2f_devices.
|
||||
sqlUpsertU2FDevice string
|
||||
sqlSelectU2FDevice string
|
||||
sqlSelectU2FDevices string
|
||||
// Table: webauthn_devices.
|
||||
sqlUpsertWebauthnDevice string
|
||||
sqlSelectWebauthnDevices string
|
||||
sqlSelectWebauthnDevicesByUsername string
|
||||
|
||||
sqlUpdateU2FDevicePublicKey string
|
||||
sqlUpdateU2FDevicePublicKeyByUsername string
|
||||
sqlUpdateWebauthnDevicePublicKey string
|
||||
sqlUpdateWebauthnDevicePublicKeyByUsername string
|
||||
sqlUpdateWebauthnDeviceRecordSignIn string
|
||||
sqlUpdateWebauthnDeviceRecordSignInByUsername string
|
||||
|
||||
// Table: duo_devices.
|
||||
sqlUpsertDuoDevice string
|
||||
|
@ -182,9 +190,11 @@ func (p *SQLProvider) StartupCheck() (err error) {
|
|||
|
||||
// SavePreferred2FAMethod save the preferred method for 2FA to the database.
|
||||
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.
|
||||
|
@ -228,7 +238,7 @@ func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification
|
|||
if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification,
|
||||
verification.JTI, verification.IssuedAt, verification.IssuedIP, verification.ExpiresAt,
|
||||
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
|
||||
|
@ -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.
|
||||
func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) {
|
||||
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,
|
||||
config.Username, config.Issuer, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil {
|
||||
return fmt.Errorf("error upserting TOTP configuration: %w", err)
|
||||
config.CreatedAt, config.LastUsedAt,
|
||||
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
|
||||
|
@ -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.
|
||||
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) {
|
||||
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
|
||||
|
@ -296,11 +317,11 @@ func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string
|
|||
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 {
|
||||
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
|
||||
|
@ -308,35 +329,20 @@ func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string
|
|||
|
||||
// LoadTOTPConfigurations load a set of TOTP configurations.
|
||||
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)
|
||||
if err != nil {
|
||||
configs = make([]models.TOTPConfiguration, 0, limit)
|
||||
|
||||
if err = p.db.SelectContext(ctx, &configs, p.sqlSelectTOTPConfigs, limit, limit*page); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return configs, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error selecting TOTP configurations: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
p.log.Errorf(logFmtErrClosingConn, err)
|
||||
for i, c := range configs {
|
||||
if configs[i].Secret, err = p.decrypt(c.Secret); err != nil {
|
||||
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
|
||||
|
@ -351,90 +357,89 @@ func (p *SQLProvider) updateTOTPConfigurationSecret(ctx context.Context, config
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// SaveU2FDevice saves a registered U2F device.
|
||||
func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) {
|
||||
// SaveWebauthnDevice saves a registered Webauthn device.
|
||||
func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device models.WebauthnDevice) (err error) {
|
||||
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 {
|
||||
return fmt.Errorf("error upserting U2F device: %v", err)
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpsertWebauthnDevice,
|
||||
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
|
||||
}
|
||||
|
||||
// LoadU2FDevice loads a U2F device registration for a given username.
|
||||
func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) {
|
||||
device = &models.U2FDevice{}
|
||||
// UpdateWebauthnDeviceSignIn updates a registered Webauthn devices sign in information.
|
||||
func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt *time.Time, signCount uint32, cloneWarning bool) (err error) {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadWebauthnDevices loads Webauthn device registrations.
|
||||
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []models.WebauthnDevice, err error) {
|
||||
devices = make([]models.WebauthnDevice, 0, limit)
|
||||
|
||||
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevices, limit, limit*page); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNoU2FDeviceHandle
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error selecting U2F device: %w", err)
|
||||
return nil, fmt.Errorf("error selecting Webauthn devices: %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)
|
||||
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", device.Username, err)
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
// LoadU2FDevices loads U2F device registrations.
|
||||
func (p *SQLProvider) LoadU2FDevices(ctx context.Context, limit, page int) (devices []models.U2FDevice, err error) {
|
||||
rows, err := p.db.QueryxContext(ctx, p.sqlSelectU2FDevices, limit, limit*page)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error selecting U2F devices: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
p.log.Errorf(logFmtErrClosingConn, 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
|
||||
}
|
||||
|
||||
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 {
|
||||
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:
|
||||
_, 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 {
|
||||
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
|
||||
|
@ -442,26 +447,32 @@ func (p *SQLProvider) updateU2FDevicePublicKey(ctx context.Context, device model
|
|||
|
||||
// SavePreferredDuoDevice saves a Duo device.
|
||||
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)
|
||||
return err
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpsertDuoDevice, device.Username, device.Device, device.Method); err != nil {
|
||||
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.
|
||||
func (p *SQLProvider) DeletePreferredDuoDevice(ctx context.Context, username string) (err error) {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlDeleteDuoDevice, username)
|
||||
return err
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlDeleteDuoDevice, username); err != nil {
|
||||
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.
|
||||
func (p *SQLProvider) LoadPreferredDuoDevice(ctx context.Context, username string) (device *models.DuoDevice, err error) {
|
||||
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 {
|
||||
return nil, ErrNoDuoDevice
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error selecting preferred duo device for user '%s': %w", username, err)
|
||||
}
|
||||
|
||||
return device, nil
|
||||
|
@ -472,7 +483,7 @@ func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt model
|
|||
if _, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt,
|
||||
attempt.Time, attempt.Successful, attempt.Banned, attempt.Username,
|
||||
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
|
||||
|
@ -480,31 +491,14 @@ func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt model
|
|||
|
||||
// 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) {
|
||||
rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page)
|
||||
if err != nil {
|
||||
attempts = make([]models.AuthenticationAttempt, 0, limit)
|
||||
|
||||
if err = p.db.SelectContext(ctx, &attempts, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNoAuthenticationLogs
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error selecting authentication logs: %w", 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 nil, fmt.Errorf("error selecting authentication logs for user '%s': %w", username, err)
|
||||
}
|
||||
|
||||
return attempts, nil
|
||||
|
|
|
@ -26,7 +26,7 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr
|
|||
|
||||
// Specific alterations to this provider.
|
||||
// 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.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations)
|
||||
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.sqlConsumeIdentityVerification = provider.db.Rebind(provider.sqlConsumeIdentityVerification)
|
||||
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.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
|
||||
provider.sqlUpdateTOTPConfigSecret = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecret)
|
||||
provider.sqlUpdateTOTPConfigSecretByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecretByUsername)
|
||||
provider.sqlSelectU2FDevice = provider.db.Rebind(provider.sqlSelectU2FDevice)
|
||||
provider.sqlSelectU2FDevices = provider.db.Rebind(provider.sqlSelectU2FDevices)
|
||||
provider.sqlUpdateU2FDevicePublicKey = provider.db.Rebind(provider.sqlUpdateU2FDevicePublicKey)
|
||||
provider.sqlUpdateU2FDevicePublicKeyByUsername = provider.db.Rebind(provider.sqlUpdateU2FDevicePublicKeyByUsername)
|
||||
provider.sqlSelectWebauthnDevices = provider.db.Rebind(provider.sqlSelectWebauthnDevices)
|
||||
provider.sqlSelectWebauthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByUsername)
|
||||
provider.sqlUpdateWebauthnDevicePublicKey = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKey)
|
||||
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.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice)
|
||||
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package storage
|
||||
|
||||
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/authelia/authelia/v4/internal/configuration/schema"
|
||||
|
@ -14,7 +18,7 @@ type SQLiteProvider struct {
|
|||
// NewSQLiteProvider constructs a SQLite provider.
|
||||
func NewSQLiteProvider(config *schema.Configuration) (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.
|
||||
|
@ -22,3 +26,27 @@ func NewSQLiteProvider(config *schema.Configuration) (provider *SQLiteProvider)
|
|||
|
||||
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
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ func (p *SQLProvider) SchemaEncryptionChangeKey(ctx context.Context, encryptionK
|
|||
return err
|
||||
}
|
||||
|
||||
if err = p.schemaEncryptionChangeKeyU2F(ctx, tx, key); err != nil {
|
||||
if err = p.schemaEncryptionChangeKeyWebauthn(ctx, tx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -79,11 +79,11 @@ func (p *SQLProvider) schemaEncryptionChangeKeyTOTP(ctx context.Context, tx *sql
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *SQLProvider) schemaEncryptionChangeKeyU2F(ctx context.Context, tx *sqlx.Tx, key [32]byte) (err error) {
|
||||
var devices []models.U2FDevice
|
||||
func (p *SQLProvider) schemaEncryptionChangeKeyWebauthn(ctx context.Context, tx *sqlx.Tx, key [32]byte) (err error) {
|
||||
var devices []models.WebauthnDevice
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
if err = p.updateU2FDevicePublicKey(ctx, device); err != nil {
|
||||
if err = p.updateWebauthnDevicePublicKey(ctx, device); err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
return fmt.Errorf("error selecting U2F devices: %w", err)
|
||||
|
|
|
@ -35,7 +35,7 @@ const (
|
|||
|
||||
const (
|
||||
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
|
||||
WHERE username = ?;`
|
||||
|
||||
|
@ -96,14 +96,24 @@ const (
|
|||
WHERE username = ?;`
|
||||
|
||||
queryFmtUpsertTOTPConfiguration = `
|
||||
REPLACE INTO %s (username, issuer, algorithm, digits, period, secret)
|
||||
VALUES (?, ?, ?, ?, ?, ?);`
|
||||
REPLACE INTO %s (created_at, last_used_at, username, issuer, algorithm, digits, period, secret)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
|
||||
queryFmtPostgresUpsertTOTPConfiguration = `
|
||||
INSERT INTO %s (username, issuer, algorithm, digits, period, secret)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
INSERT INTO %s (created_at, last_used_at, username, issuer, algorithm, digits, period, secret)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
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 = `
|
||||
DELETE FROM %s
|
||||
|
@ -111,36 +121,50 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
queryFmtSelectU2FDevice = `
|
||||
SELECT id, username, key_handle, public_key
|
||||
FROM %s
|
||||
WHERE username = ?;`
|
||||
|
||||
queryFmtSelectU2FDevices = `
|
||||
SELECT id, username, key_handle, public_key
|
||||
queryFmtSelectWebauthnDevices = `
|
||||
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
|
||||
FROM %s
|
||||
LIMIT ?
|
||||
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
|
||||
SET public_key = ?
|
||||
WHERE id = ?;`
|
||||
|
||||
queryFmtUpdateUpdateU2FDevicePublicKeyByUsername = `
|
||||
queryFmtUpdateUpdateWebauthnDevicePublicKeyByUsername = `
|
||||
UPDATE %s
|
||||
SET public_key = ?
|
||||
WHERE username = ?;`
|
||||
WHERE username = ? AND kid = ?;`
|
||||
|
||||
queryFmtUpsertU2FDevice = `
|
||||
REPLACE INTO %s (username, description, key_handle, public_key)
|
||||
VALUES (?, ?, ?, ?);`
|
||||
queryFmtUpdateWebauthnDeviceRecordSignIn = `
|
||||
UPDATE %s
|
||||
SET
|
||||
rpid = ?, last_used_at = ?, sign_count = ?,
|
||||
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
|
||||
WHERE id = ?;`
|
||||
|
||||
queryFmtPostgresUpsertU2FDevice = `
|
||||
INSERT INTO %s (username, description, key_handle, public_key)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
queryFmtUpdateWebauthnDeviceRecordSignInByUsername = `
|
||||
UPDATE %s
|
||||
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)
|
||||
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 (
|
||||
|
|
|
@ -29,7 +29,7 @@ func (p *SQLProvider) schemaMigratePre1To1(ctx context.Context) (err error) {
|
|||
tablePre1Config,
|
||||
tablePre1TOTPSecrets,
|
||||
tablePre1IdentityVerificationTokens,
|
||||
tableU2FDevices,
|
||||
tablePre1U2FDevices,
|
||||
tableUserPreferences,
|
||||
tableAuthenticationLogs,
|
||||
tableAlphaPreferences,
|
||||
|
@ -93,7 +93,7 @@ func (p *SQLProvider) schemaMigratePre1Rename(ctx context.Context, tables, table
|
|||
}
|
||||
|
||||
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;`,
|
||||
tableNew, table, tableNew)); err != nil {
|
||||
continue
|
||||
|
@ -132,7 +132,7 @@ func (p *SQLProvider) schemaMigratePre1To1Rollback(ctx context.Context, up bool)
|
|||
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;`,
|
||||
tableNew, table, tableNew)); err != nil {
|
||||
continue
|
||||
|
@ -236,7 +236,7 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(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 {
|
||||
return err
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ func (p *SQLProvider) schemaMigratePre1To1U2F(ctx context.Context) (err error) {
|
|||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -295,7 +295,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1(ctx context.Context) (err error) {
|
|||
tableMigrations,
|
||||
tableTOTPConfigurations,
|
||||
tableIdentityVerification,
|
||||
tableU2FDevices,
|
||||
tablePre1U2FDevices,
|
||||
tableDuoDevices,
|
||||
tableUserPreferences,
|
||||
tableAuthenticationLogs,
|
||||
|
@ -429,7 +429,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1TOTP(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 {
|
||||
return err
|
||||
}
|
||||
|
@ -460,7 +460,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1U2F(ctx context.Context) (err error) {
|
|||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ const (
|
|||
|
||||
var (
|
||||
storageLocalTmpConfig = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
TOTP: schema.TOTPConfiguration{
|
||||
Issuer: "Authelia",
|
||||
Period: 6,
|
||||
},
|
||||
|
|
|
@ -41,18 +41,17 @@ func (s *BackendProtectionScenario) AssertRequestStatusCode(method, url string,
|
|||
|
||||
func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
|
||||
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/u2f/register", AutheliaBaseURL), 403)
|
||||
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign_request", AutheliaBaseURL), 403)
|
||||
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/webauthn/assertion", AutheliaBaseURL), 403)
|
||||
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/webauthn/attestation", 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/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/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() {
|
||||
|
|
|
@ -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"})
|
||||
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().Regexp(pattern, output)
|
||||
s.Assert().Contains(output, "Schema Version: ")
|
||||
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: valid")
|
||||
}
|
||||
|
||||
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"})
|
||||
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().Regexp(pattern, output)
|
||||
s.Assert().Contains(output, "Schema Version: ")
|
||||
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"})
|
||||
s.Assert().NoError(err)
|
||||
|
|
|
@ -462,6 +462,7 @@ func (s *DuoPushSuite) TestDuoPushRedirectionURLSuite() {
|
|||
func (s *DuoPushSuite) TestAvailableMethodsScenario() {
|
||||
suite.Run(s.T(), NewAvailableMethodsScenario([]string{
|
||||
"TIME-BASED ONE-TIME PASSWORD",
|
||||
"SECURITY KEY - WEBAUTHN",
|
||||
"PUSH NOTIFICATION",
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -288,7 +288,7 @@ func (s *StandaloneSuite) TestResetPasswordScenario() {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
15
internal/totp/helpers_test.go
Normal file
15
internal/totp/helpers_test.go
Normal 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(""))
|
||||
}
|
|
@ -7,6 +7,6 @@ import (
|
|||
// Provider for TOTP functionality.
|
||||
type Provider interface {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
)
|
||||
|
||||
// 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{
|
||||
config: config,
|
||||
config: &config,
|
||||
}
|
||||
|
||||
if config.Skew != nil {
|
||||
|
@ -49,6 +49,7 @@ func (p TimeBased) GenerateCustom(username, algorithm string, digits, period, se
|
|||
}
|
||||
|
||||
config = &models.TOTPConfiguration{
|
||||
CreatedAt: time.Now(),
|
||||
Username: username,
|
||||
Issuer: p.config.Issuer,
|
||||
Algorithm: algorithm,
|
||||
|
|
89
internal/totp/totp_test.go
Normal file
89
internal/totp/totp_test.go
Normal 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)
|
||||
}
|
|
@ -23,8 +23,7 @@
|
|||
"react-i18next": "11.15.5",
|
||||
"react-loading": "2.0.3",
|
||||
"react-otp-input": "2.4.0",
|
||||
"react-router-dom": "6.2.2",
|
||||
"u2f-api": "1.2.1"
|
||||
"react-router-dom": "6.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cd .. && husky install .github",
|
||||
|
|
|
@ -51,7 +51,6 @@ specifiers:
|
|||
react-router-dom: 6.2.2
|
||||
react-test-renderer: 17.0.2
|
||||
typescript: 4.6.2
|
||||
u2f-api: 1.2.1
|
||||
vite: 2.8.6
|
||||
vite-plugin-eslint: 1.3.0
|
||||
vite-plugin-istanbul: 2.5.0
|
||||
|
@ -80,7 +79,6 @@ dependencies:
|
|||
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-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2
|
||||
u2f-api: 1.2.1
|
||||
|
||||
devDependencies:
|
||||
'@commitlint/cli': 16.2.1
|
||||
|
@ -7846,10 +7844,6 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/u2f-api/1.2.1:
|
||||
resolution: {integrity: sha512-4kfxen+mVxYkdJWHIS7c4HxsqNUs6fkrxlMLi7uQTVVYMVfTh73PhYoAmp1B0PRGELGDiKmXlssgqxcPvtpfSg==}
|
||||
dev: false
|
||||
|
||||
/unbox-primitive/1.0.1:
|
||||
resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==}
|
||||
dependencies:
|
||||
|
|
|
@ -6,13 +6,13 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
|||
|
||||
import NotificationBar from "@components/NotificationBar";
|
||||
import {
|
||||
FirstFactorRoute,
|
||||
ConsentRoute,
|
||||
IndexRoute,
|
||||
LogoutRoute,
|
||||
RegisterOneTimePasswordRoute,
|
||||
RegisterWebauthnRoute,
|
||||
ResetPasswordStep2Route,
|
||||
ResetPasswordStep1Route,
|
||||
RegisterSecurityKeyRoute,
|
||||
RegisterOneTimePasswordRoute,
|
||||
LogoutRoute,
|
||||
ConsentRoute,
|
||||
} from "@constants/Routes";
|
||||
import NotificationsContext from "@hooks/NotificationsContext";
|
||||
import { Notification } from "@models/Notifications";
|
||||
|
@ -20,7 +20,7 @@ import * as themes from "@themes/index";
|
|||
import { getBasePath } from "@utils/BasePath";
|
||||
import { getDuoSelfEnrollment, getRememberMe, getResetPassword, getTheme } from "@utils/Configuration";
|
||||
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 LoginPortal from "@views/LoginPortal/LoginPortal";
|
||||
import SignOut from "@views/LoginPortal/SignOut/SignOut";
|
||||
|
@ -67,12 +67,12 @@ const App: React.FC = () => {
|
|||
<Routes>
|
||||
<Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} />
|
||||
<Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} />
|
||||
<Route path={RegisterSecurityKeyRoute} element={<RegisterSecurityKey />} />
|
||||
<Route path={RegisterWebauthnRoute} element={<RegisterWebauthn />} />
|
||||
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
|
||||
<Route path={LogoutRoute} element={<SignOut />} />
|
||||
<Route path={ConsentRoute} element={<ConsentView />} />
|
||||
<Route
|
||||
path={`${FirstFactorRoute}*`}
|
||||
path={`${IndexRoute}*`}
|
||||
element={
|
||||
<LoginPortal
|
||||
duoSelfEnrollment={getDuoSelfEnrollment()}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
export const FirstFactorRoute: string = "/";
|
||||
export const IndexRoute: string = "/";
|
||||
export const AuthenticatedRoute: string = "/authenticated";
|
||||
export const ConsentRoute: string = "/consent";
|
||||
|
||||
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 SecondFactorPushSubRoute: string = "push-notification";
|
||||
|
||||
export const ResetPasswordStep1Route: string = "/reset-password/step1";
|
||||
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 LogoutRoute: string = "/logout";
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"Reset": "Reset",
|
||||
"Scan QR Code": "Scan QR Code",
|
||||
"Secret": "Secret",
|
||||
"Security Key - U2F": "Security Key - U2F",
|
||||
"Security Key - WebAuthN": "Security Key - WebAuthN",
|
||||
"Select a Device": "Select a Device",
|
||||
"Sign in": "Sign in",
|
||||
"Sign out": "Sign out",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"Reset": "Restablecer",
|
||||
"Scan QR Code": "Escanear Código QR",
|
||||
"Secret": "Secreto",
|
||||
"Security Key - U2F": "Llave de Seguridad - U2F",
|
||||
"Security Key - WebAuthN": "Llave de Seguridad - WebAuthN",
|
||||
"Select a Device": "Seleccionar Dispositivo",
|
||||
"Sign in": "Iniciar Sesión",
|
||||
"Sign out": "Cerrar Sesión",
|
||||
|
|
|
@ -2,5 +2,4 @@ import { SecondFactorMethod } from "@models/Methods";
|
|||
|
||||
export interface Configuration {
|
||||
available_methods: Set<SecondFactorMethod>;
|
||||
second_factor_enabled: boolean;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum SecondFactorMethod {
|
||||
TOTP = 1,
|
||||
U2F = 2,
|
||||
MobilePush = 3,
|
||||
Webauthn,
|
||||
MobilePush,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { SecondFactorMethod } from "@models/Methods";
|
|||
export interface UserInfo {
|
||||
display_name: string;
|
||||
method: SecondFactorMethod;
|
||||
has_u2f: boolean;
|
||||
has_webauthn: boolean;
|
||||
has_totp: boolean;
|
||||
has_duo: boolean;
|
||||
}
|
||||
|
|
129
web/src/models/Webauthn.ts
Normal file
129
web/src/models/Webauthn.ts
Normal 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;
|
||||
}
|
|
@ -11,12 +11,11 @@ export const FirstFactorPath = basePath + "/api/firstfactor";
|
|||
export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start";
|
||||
export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
|
||||
|
||||
export const InitiateU2FRegistrationPath = basePath + "/api/secondfactor/u2f/identity/start";
|
||||
export const CompleteU2FRegistrationStep1Path = basePath + "/api/secondfactor/u2f/identity/finish";
|
||||
export const CompleteU2FRegistrationStep2Path = basePath + "/api/secondfactor/u2f/register";
|
||||
export const WebauthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start";
|
||||
export const WebauthnIdentityFinishPath = basePath + "/api/secondfactor/webauthn/identity/finish";
|
||||
export const WebauthnAttestationPath = basePath + "/api/secondfactor/webauthn/attestation";
|
||||
|
||||
export const InitiateU2FSignInPath = basePath + "/api/secondfactor/u2f/sign_request";
|
||||
export const CompleteU2FSignInPath = basePath + "/api/secondfactor/u2f/sign";
|
||||
export const WebauthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion";
|
||||
|
||||
export const InitiateDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_devices";
|
||||
export const CompleteDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_device";
|
||||
|
@ -48,6 +47,12 @@ export interface Response<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;
|
||||
|
||||
function toErrorResponse<T>(resp: AxiosResponse<ServiceResponse<T>>): ErrorResponse | undefined {
|
||||
|
|
|
@ -5,7 +5,6 @@ import { toEnum, Method2FA } from "@services/UserInfo";
|
|||
|
||||
interface ConfigurationPayload {
|
||||
available_methods: Method2FA[];
|
||||
second_factor_enabled: boolean;
|
||||
}
|
||||
|
||||
export async function getConfiguration(): Promise<Configuration> {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user