refactor: restructure passkey with dedicated package

- Create internal/passkey/ package with challenge session management
- Add HTTP handler layer in internal/api/passkey.go
- Simplify Passkey model to use blob credential storage
- Register passkey routes in router
- Update frontend to match new API endpoints
- Add .env.example template
This commit is contained in:
Sakurasan
2026-09-01 22:34:44 +08:00
parent d28fca8ee6
commit 9733b3c20b
11 changed files with 546 additions and 268 deletions
-203
View File
@@ -1,203 +0,0 @@
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
}