authelia/internal/model/webauthn.go

298 lines
8.1 KiB
Go

package model
import (
"database/sql"
"encoding/base64"
"encoding/hex"
"strings"
"time"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
)
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, ","),
}
aaguid, err := uuid.Parse(hex.EncodeToString(credential.Authenticator.AAGUID))
if err == nil && aaguid.ID() != 0 {
device.AAGUID = uuid.NullUUID{Valid: true, UUID: 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 sql.NullTime `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.NullUUID `db:"aaguid"`
SignCount uint32 `db:"sign_count"`
CloneWarning bool `db:"clone_warning"`
}
// UpdateSignInInfo adjusts the values of the WebAuthnDevice after a sign in.
func (d *WebAuthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
d.LastUsedAt = sql.NullTime{Time: now, Valid: true}
d.SignCount = signCount
if d.RPID != "" {
return
}
switch d.AttestationType {
case attestationTypeFIDOU2F:
d.RPID = config.RPOrigin
default:
d.RPID = config.RPID
}
}
// LastUsed provides LastUsedAt as a *time.Time instead of sql.NullTime.
func (d *WebAuthnDevice) LastUsed() *time.Time {
if d.LastUsedAt.Valid {
value := time.Unix(d.LastUsedAt.Time.Unix(), int64(d.LastUsedAt.Time.Nanosecond()))
return &value
}
return nil
}
// ToData converts this WebAuthnDevice into the data format for exporting etc.
func (d *WebAuthnDevice) ToData() WebAuthnDeviceData {
return WebAuthnDeviceData{
CreatedAt: d.CreatedAt,
LastUsedAt: d.LastUsed(),
RPID: d.RPID,
Username: d.Username,
Description: d.Description,
KID: d.KID.String(),
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
AttestationType: d.AttestationType,
Transport: d.Transport,
AAGUID: d.AAGUID.UUID.String(),
SignCount: d.SignCount,
CloneWarning: d.CloneWarning,
}
}
// MarshalYAML marshals this model into YAML.
func (d *WebAuthnDevice) MarshalYAML() (any, error) {
return d.ToData(), nil
}
// UnmarshalYAML unmarshalls YAML into this model.
func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
o := &WebAuthnDeviceData{}
if err = value.Decode(o); err != nil {
return err
}
if d.PublicKey, err = base64.StdEncoding.DecodeString(o.PublicKey); err != nil {
return err
}
var aaguid uuid.UUID
if aaguid, err = uuid.Parse(o.AAGUID); err != nil {
return err
}
if aaguid.ID() != 0 {
d.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
}
var kid []byte
if kid, err = base64.StdEncoding.DecodeString(o.KID); err != nil {
return err
}
d.KID = NewBase64(kid)
d.CreatedAt = o.CreatedAt
d.RPID = o.RPID
d.Username = o.Username
d.Description = o.Description
d.AttestationType = o.AttestationType
d.Transport = o.Transport
d.SignCount = o.SignCount
d.CloneWarning = o.CloneWarning
if o.LastUsedAt != nil {
d.LastUsedAt = sql.NullTime{Valid: true, Time: *o.LastUsedAt}
}
return nil
}
// WebAuthnDeviceData represents a WebAuthn Device in the database storage.
type WebAuthnDeviceData struct {
CreatedAt time.Time `yaml:"created_at"`
LastUsedAt *time.Time `yaml:"last_used_at"`
RPID string `yaml:"rpid"`
Username string `yaml:"username"`
Description string `yaml:"description"`
KID string `yaml:"kid"`
PublicKey string `yaml:"public_key"`
AttestationType string `yaml:"attestation_type"`
Transport string `yaml:"transport"`
AAGUID string `yaml:"aaguid"`
SignCount uint32 `yaml:"sign_count"`
CloneWarning bool `yaml:"clone_warning"`
}
// WebAuthnDeviceExport represents a WebAuthnDevice export file.
type WebAuthnDeviceExport struct {
WebAuthnDevices []WebAuthnDevice `yaml:"webauthn_devices"`
}
// WebAuthnDeviceDataExport represents a WebAuthnDevice export file.
type WebAuthnDeviceDataExport struct {
WebAuthnDevices []WebAuthnDeviceData `yaml:"webauthn_devices"`
}
// ToData converts this WebAuthnDeviceExport into a WebAuthnDeviceDataExport.
func (export WebAuthnDeviceExport) ToData() WebAuthnDeviceDataExport {
data := WebAuthnDeviceDataExport{
WebAuthnDevices: make([]WebAuthnDeviceData, len(export.WebAuthnDevices)),
}
for i, device := range export.WebAuthnDevices {
data.WebAuthnDevices[i] = device.ToData()
}
return data
}
// MarshalYAML marshals this model into YAML.
func (export WebAuthnDeviceExport) MarshalYAML() (any, error) {
return export.ToData(), nil
}