mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
203 lines
7.2 KiB
Go
203 lines
7.2 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
"github.com/hanzoai/git/modules/timeutil"
|
|
"github.com/hanzoai/git/modules/util"
|
|
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
|
)
|
|
|
|
// ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error.
|
|
type ErrWebAuthnCredentialNotExist struct {
|
|
ID int64
|
|
CredentialID []byte
|
|
}
|
|
|
|
func (err ErrWebAuthnCredentialNotExist) Error() string {
|
|
if len(err.CredentialID) == 0 {
|
|
return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID)
|
|
}
|
|
return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %x]", err.CredentialID)
|
|
}
|
|
|
|
// Unwrap unwraps this as a ErrNotExist err
|
|
func (err ErrWebAuthnCredentialNotExist) Unwrap() error {
|
|
return util.ErrNotExist
|
|
}
|
|
|
|
// IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist.
|
|
func IsErrWebAuthnCredentialNotExist(err error) bool {
|
|
_, ok := err.(ErrWebAuthnCredentialNotExist)
|
|
return ok
|
|
}
|
|
|
|
// WebAuthnCredential represents the WebAuthn credential data for a public-key
|
|
// credential conformant to WebAuthn Level 1
|
|
type WebAuthnCredential struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
Name string
|
|
LowerName string `xorm:"unique(s)"`
|
|
UserID int64 `xorm:"INDEX unique(s)"`
|
|
CredentialID []byte `xorm:"INDEX VARBINARY(1024)"`
|
|
PublicKey []byte
|
|
AttestationType string
|
|
AAGUID []byte
|
|
SignCount uint32 `xorm:"BIGINT"`
|
|
CloneWarning bool
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(WebAuthnCredential))
|
|
}
|
|
|
|
// TableName returns a better table name for WebAuthnCredential
|
|
func (cred WebAuthnCredential) TableName() string {
|
|
return "webauthn_credential"
|
|
}
|
|
|
|
// UpdateSignCount will update the database value of SignCount
|
|
func (cred *WebAuthnCredential) UpdateSignCount(ctx context.Context) error {
|
|
_, err := db.GetEngine(ctx).ID(cred.ID).Cols("sign_count").Update(cred)
|
|
return err
|
|
}
|
|
|
|
// BeforeInsert will be invoked by XORM before updating a record
|
|
func (cred *WebAuthnCredential) BeforeInsert() {
|
|
cred.LowerName = strings.ToLower(cred.Name)
|
|
}
|
|
|
|
// BeforeUpdate will be invoked by XORM before updating a record
|
|
func (cred *WebAuthnCredential) BeforeUpdate() {
|
|
cred.LowerName = strings.ToLower(cred.Name)
|
|
}
|
|
|
|
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
|
func (cred *WebAuthnCredential) AfterLoad() {
|
|
cred.LowerName = strings.ToLower(cred.Name)
|
|
}
|
|
|
|
// WebAuthnCredentialList is a list of *WebAuthnCredential
|
|
type WebAuthnCredentialList []*WebAuthnCredential
|
|
|
|
// newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337
|
|
// to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags
|
|
func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags {
|
|
return webauthn.CredentialFlags{
|
|
UserPresent: flags.HasUserPresent(),
|
|
UserVerified: flags.HasUserVerified(),
|
|
BackupEligible: flags.HasBackupEligible(),
|
|
BackupState: flags.HasBackupState(),
|
|
}
|
|
}
|
|
|
|
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
|
|
func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential {
|
|
// TODO: at the moment, the forge doesn't store or check the flags
|
|
// so we need to use the default flags from the authenticator to make the login validation pass
|
|
// In the future, we should:
|
|
// 1. store the flags when registering the credential
|
|
// 2. provide the stored flags when converting the credentials (for login)
|
|
// 3. for old users, still use this fallback to the default flags
|
|
defAuthFlags := util.OptionalArg(defaultAuthFlags)
|
|
creds := make([]webauthn.Credential, 0, len(list))
|
|
for _, cred := range list {
|
|
creds = append(creds, webauthn.Credential{
|
|
ID: cred.CredentialID,
|
|
PublicKey: cred.PublicKey,
|
|
AttestationType: cred.AttestationType,
|
|
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
|
|
Authenticator: webauthn.Authenticator{
|
|
AAGUID: cred.AAGUID,
|
|
SignCount: cred.SignCount,
|
|
CloneWarning: cred.CloneWarning,
|
|
},
|
|
})
|
|
}
|
|
return creds
|
|
}
|
|
|
|
// GetWebAuthnCredentialsByUID returns all WebAuthn credentials of the given user
|
|
func GetWebAuthnCredentialsByUID(ctx context.Context, uid int64) (WebAuthnCredentialList, error) {
|
|
creds := make(WebAuthnCredentialList, 0)
|
|
return creds, db.GetEngine(ctx).Where("user_id = ?", uid).Find(&creds)
|
|
}
|
|
|
|
// ExistsWebAuthnCredentialsForUID returns if the given user has credentials
|
|
func ExistsWebAuthnCredentialsForUID(ctx context.Context, uid int64) (bool, error) {
|
|
return db.GetEngine(ctx).Where("user_id = ?", uid).Exist(&WebAuthnCredential{})
|
|
}
|
|
|
|
// GetWebAuthnCredentialByName returns WebAuthn credential by id
|
|
func GetWebAuthnCredentialByName(ctx context.Context, uid int64, name string) (*WebAuthnCredential, error) {
|
|
cred := new(WebAuthnCredential)
|
|
if found, err := db.GetEngine(ctx).Where("user_id = ? AND lower_name = ?", uid, strings.ToLower(name)).Get(cred); err != nil {
|
|
return nil, err
|
|
} else if !found {
|
|
return nil, ErrWebAuthnCredentialNotExist{}
|
|
}
|
|
return cred, nil
|
|
}
|
|
|
|
// GetWebAuthnCredentialByID returns WebAuthn credential by id
|
|
func GetWebAuthnCredentialByID(ctx context.Context, id int64) (*WebAuthnCredential, error) {
|
|
cred := new(WebAuthnCredential)
|
|
if found, err := db.GetEngine(ctx).ID(id).Get(cred); err != nil {
|
|
return nil, err
|
|
} else if !found {
|
|
return nil, ErrWebAuthnCredentialNotExist{ID: id}
|
|
}
|
|
return cred, nil
|
|
}
|
|
|
|
// HasWebAuthnRegistrationsByUID returns whether a given user has WebAuthn registrations
|
|
func HasWebAuthnRegistrationsByUID(ctx context.Context, uid int64) (bool, error) {
|
|
return db.GetEngine(ctx).Where("user_id = ?", uid).Exist(&WebAuthnCredential{})
|
|
}
|
|
|
|
// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID
|
|
func GetWebAuthnCredentialByCredID(ctx context.Context, userID int64, credID []byte) (*WebAuthnCredential, error) {
|
|
cred := new(WebAuthnCredential)
|
|
if found, err := db.GetEngine(ctx).Where("user_id = ? AND credential_id = ?", userID, credID).Get(cred); err != nil {
|
|
return nil, err
|
|
} else if !found {
|
|
return nil, ErrWebAuthnCredentialNotExist{CredentialID: credID}
|
|
}
|
|
return cred, nil
|
|
}
|
|
|
|
// CreateCredential will create a new WebAuthnCredential from the given Credential
|
|
func CreateCredential(ctx context.Context, userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) {
|
|
c := &WebAuthnCredential{
|
|
UserID: userID,
|
|
Name: name,
|
|
CredentialID: cred.ID,
|
|
PublicKey: cred.PublicKey,
|
|
AttestationType: cred.AttestationType,
|
|
AAGUID: cred.Authenticator.AAGUID,
|
|
SignCount: cred.Authenticator.SignCount,
|
|
CloneWarning: false,
|
|
}
|
|
|
|
if err := db.Insert(ctx, c); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// DeleteCredential will delete WebAuthnCredential
|
|
func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
|
|
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
|
|
return had > 0, err
|
|
}
|