authelia/internal/models/webauthn.go
James Elliott 8f05846e21
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.
2022-03-03 22:20:43 +11:00

168 lines
4.4 KiB
Go

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