package service import ( "encoding/base64" "fmt" "net/http" "opencatd-open/internal/store" "opencatd-open/pkg/config" "strconv" "strings" "time" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "gorm.io/gorm" ) type WebAuthnUser struct { User *store.User Credentials []webauthn.Credential } func (u *WebAuthnUser) WebAuthnID() []byte { return []byte(strconv.FormatUint(u.User.ID, 10)) } func (u *WebAuthnUser) WebAuthnName() string { return u.User.Username } func (u *WebAuthnUser) WebAuthnDisplayName() string { return u.User.Username } func (u *WebAuthnUser) WebAuthnCredentials() []webauthn.Credential { return u.Credentials } func (u *WebAuthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) { credentials := u.WebAuthnCredentials() descriptors = make([]protocol.CredentialDescriptor, len(credentials)) for i, credential := range credentials { descriptors[i] = credential.Descriptor() } return descriptors } type WebAuthnService struct { cfg *config.Config DB *gorm.DB WebAuthn *webauthn.WebAuthn } func NewWebAuthnService(cfg *config.Config, db *gorm.DB) (*WebAuthnService, error) { wconfig := &webauthn.Config{ RPDisplayName: cfg.AppName, RPID: cfg.RPID, RPOrigins: cfg.RPOrigins, AuthenticatorSelection: protocol.AuthenticatorSelection{ RequireResidentKey: protocol.ResidentKeyRequired(), ResidentKey: protocol.ResidentKeyRequirementRequired, UserVerification: protocol.VerificationPreferred, }, } wa, err := webauthn.New(wconfig) if err != nil { return nil, err } return &WebAuthnService{ cfg: cfg, DB: db, WebAuthn: wa, }, nil } func (s *WebAuthnService) GetUserWithCredentials(userID uint64) (*WebAuthnUser, error) { var user store.User if err := s.DB.First(&user, userID).Error; err != nil { return nil, err } var passkeys []store.Passkey if err := s.DB.Where("user_id = ?", userID).Find(&passkeys).Error; err != nil { return nil, err } credentials := make([]webauthn.Credential, len(passkeys)) for i, pk := range passkeys { credentialIDBytes, err := base64.StdEncoding.DecodeString(pk.CredentialID) if err != nil { return nil, fmt.Errorf("failed to decode CredentialID: %w", err) } publicKeyBytes, err := base64.StdEncoding.DecodeString(pk.PublicKey) if err != nil { return nil, fmt.Errorf("failed to decode PublicKey: %w", err) } aaguidBytes, err := base64.StdEncoding.DecodeString(pk.AAGUID) if err != nil { return nil, fmt.Errorf("failed to decode AAGUID: %w", err) } var transport []protocol.AuthenticatorTransport if pk.Transport != "" { transport = []protocol.AuthenticatorTransport{protocol.AuthenticatorTransport(pk.Transport)} } credentials[i] = webauthn.Credential{ ID: credentialIDBytes, PublicKey: publicKeyBytes, AttestationType: pk.AttestationType, Transport: transport, Flags: webauthn.CredentialFlags{ UserPresent: true, UserVerified: true, BackupEligible: pk.BackupEligible, BackupState: pk.BackupState, }, Authenticator: webauthn.Authenticator{ AAGUID: aaguidBytes, SignCount: uint32(pk.SignCount), }, } } return &WebAuthnUser{ User: &user, Credentials: credentials, }, nil } func (s *WebAuthnService) BeginRegistration(userID uint64) (*protocol.CredentialCreation, error) { user, err := s.GetUserWithCredentials(userID) if err != nil { return nil, err } options, _, err := s.WebAuthn.BeginRegistration(user) if err != nil { return nil, err } return options, nil } func (s *WebAuthnService) FinishRegistration(userID uint64, response *http.Request, deviceName string) (*store.Passkey, error) { user, err := s.GetUserWithCredentials(userID) if err != nil { return nil, err } credential, err := s.WebAuthn.FinishRegistration(user, webauthn.SessionData{}, response) if err != nil { return nil, err } var transport string if len(credential.Transport) > 0 { transport = string(credential.Transport[0]) } passkey := &store.Passkey{ UserID: userID, CredentialID: base64.StdEncoding.EncodeToString(credential.ID), PublicKey: base64.StdEncoding.EncodeToString(credential.PublicKey), AttestationType: string(credential.AttestationType), AAGUID: base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID), SignCount: uint64(credential.Authenticator.SignCount), Name: deviceName, DeviceType: strings.TrimSpace(fmt.Sprintf("%s", deviceName)), LastUsedAt: time.Now().Unix(), BackupEligible: credential.Flags.BackupEligible, BackupState: credential.Flags.BackupState, Transport: transport, } if err := s.DB.Create(passkey).Error; err != nil { return nil, err } return passkey, nil } func (s *WebAuthnService) BeginLogin() (*protocol.CredentialAssertion, error) { options, _, err := s.WebAuthn.BeginDiscoverableLogin() if err != nil { return nil, err } return options, nil } func (s *WebAuthnService) ListPasskeys(userID uint64) ([]store.Passkey, error) { var passkeys []store.Passkey if err := s.DB.Where("user_id = ?", userID).Find(&passkeys).Error; err != nil { return nil, err } return passkeys, nil } func (s *WebAuthnService) DeletePasskey(userID uint64, passkeyID uint64) error { return s.DB.Where("id = ? AND user_id = ?", passkeyID, userID).Delete(&store.Passkey{}).Error }