feat: support webauthn login (#4945)

* feat: support webauthn login

* manually merge

* fix: clear user cache after updating authn

* decrease db size of Authn

* change authn type to text

* simplify code structure

---------

Co-authored-by: Andy Hsu <i@nn.ci>
This commit is contained in:
itsHenry
2023-08-14 22:54:38 +08:00
committed by GitHub
parent 13e8d36e1a
commit 1aa024ed6b
11 changed files with 396 additions and 2 deletions

25
internal/authn/authn.go Normal file
View File

@ -0,0 +1,25 @@
package authn
import (
"net/http"
"net/url"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/server/common"
"github.com/go-webauthn/webauthn/webauthn"
)
func NewAuthnInstance(r *http.Request) (*webauthn.WebAuthn, error) {
siteUrl, err := url.Parse(common.GetApiUrl(r))
if err != nil {
return nil, err
}
return webauthn.New(&webauthn.Config{
RPDisplayName: setting.GetStr(conf.SiteTitle),
RPID: siteUrl.Hostname(),
//RPOrigin: siteUrl.String(),
RPOrigins: []string{siteUrl.String()},
// RPOrigin: "http://localhost:5173"
})
}

View File

@ -139,6 +139,7 @@ func InitialSettings() []model.SettingItem {
{Key: conf.OcrApi, Value: "https://api.nn.ci/ocr/file/json", Type: conf.TypeString, Group: model.GLOBAL},
{Key: conf.FilenameCharMapping, Value: `{"/": "|"}`, Type: conf.TypeText, Group: model.GLOBAL},
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
{Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
// aria2 settings
{Key: conf.Aria2Uri, Value: "http://localhost:6800/jsonrpc", Type: conf.TypeString, Group: model.ARIA2, Flag: model.PRIVATE},

View File

@ -41,6 +41,7 @@ const (
OcrApi = "ocr_api"
FilenameCharMapping = "filename_char_mapping"
ForwardDirectLinkParams = "forward_direct_link_params"
WebauthnLoginEnabled = "webauthn_login_enabled"
// index
SearchIndex = "search_index"

View File

@ -29,5 +29,5 @@ func AutoMigrate(dst ...interface{}) error {
}
func GetDb() *gorm.DB {
return db;
return db
}

View File

@ -1,7 +1,11 @@
package db
import (
"encoding/base64"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
)
@ -59,3 +63,40 @@ func GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err err
func DeleteUserById(id uint) error {
return errors.WithStack(db.Delete(&model.User{}, id).Error)
}
func UpdateAuthn(userID uint, authn string) error {
return db.Model(&model.User{ID: userID}).Update("authn", authn).Error
}
func RegisterAuthn(u *model.User, credential *webauthn.Credential) error {
if u == nil {
return errors.New("user is nil")
}
exists := u.WebAuthnCredentials()
if credential != nil {
exists = append(exists, *credential)
}
res, err := utils.Json.Marshal(exists)
if err != nil {
return err
}
return UpdateAuthn(u.ID, string(res))
}
func RemoveAuthn(u *model.User, id string) error {
exists := u.WebAuthnCredentials()
for i := 0; i < len(exists); i++ {
idEncoded := base64.StdEncoding.EncodeToString(exists[i].ID)
if idEncoded == id {
exists[len(exists)-1], exists[i] = exists[i], exists[len(exists)-1]
exists = exists[:len(exists)-1]
break
}
}
res, err := utils.Json.Marshal(exists)
if err != nil {
return err
}
return UpdateAuthn(u.ID, string(res))
}

View File

@ -1,11 +1,14 @@
package model
import (
"encoding/binary"
"encoding/json"
"fmt"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
)
@ -41,6 +44,7 @@ type User struct {
Permission int32 `json:"permission"`
OtpSecret string `json:"-"`
SsoID string `json:"sso_id"` // unique by sso platform
Authn string `gorm:"type:text" json:"-"`
}
func (u *User) IsGuest() bool {
@ -130,3 +134,30 @@ func HashPwd(static string, salt string) string {
func TwoHashPwd(password string, salt string) string {
return HashPwd(StaticHash(password), salt)
}
func (u *User) WebAuthnID() []byte {
bs := make([]byte, 8)
binary.LittleEndian.PutUint64(bs, uint64(u.ID))
return bs
}
func (u *User) WebAuthnName() string {
return u.Username
}
func (u *User) WebAuthnDisplayName() string {
return u.Username
}
func (u *User) WebAuthnCredentials() []webauthn.Credential {
var res []webauthn.Credential
err := json.Unmarshal([]byte(u.Authn), &res)
if err != nil {
fmt.Println(err)
}
return res
}
func (u *User) WebAuthnIcon() string {
return "https://alist.nn.ci/logo.svg"
}