From 1aa024ed6bac6356de3f5da52c09745c2a6c33f8 Mon Sep 17 00:00:00 2001 From: itsHenry <2671230065@qq.com> Date: Mon, 14 Aug 2023 22:54:38 +0800 Subject: [PATCH] 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 --- go.mod | 7 + go.sum | 14 ++ internal/authn/authn.go | 25 ++++ internal/bootstrap/data/setting.go | 1 + internal/conf/const.go | 1 + internal/db/db.go | 2 +- internal/db/user.go | 41 ++++++ internal/model/user.go | 31 +++++ server/handles/webauthn.go | 217 +++++++++++++++++++++++++++++ server/middlewares/auth.go | 48 +++++++ server/router.go | 11 +- 11 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 internal/authn/authn.go create mode 100644 server/handles/webauthn.go diff --git a/go.mod b/go.mod index 6bdb9066..ec4f4363 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/gin-contrib/cors v1.4.0 github.com/gin-gonic/gin v1.9.1 github.com/go-resty/resty/v2 v2.7.0 + github.com/go-webauthn/webauthn v0.8.6 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 @@ -90,6 +91,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gaoyb7/115drive-webdav v0.1.8 // indirect github.com/geoffgarside/ber v1.1.0 // indirect @@ -100,10 +102,13 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-webauthn/x v0.1.4 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-tpm v0.9.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -132,6 +137,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect @@ -170,6 +176,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/u2takey/go-utils v0.3.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.etcd.io/bbolt v1.3.7 // indirect golang.org/x/arch v0.3.0 // indirect diff --git a/go.sum b/go.sum index f0d20d89..e03b6914 100644 --- a/go.sum +++ b/go.sum @@ -118,6 +118,8 @@ github.com/foxxorcat/mopan-sdk-go v0.1.3/go.mod h1:iWHA2JFhzmKR28ySp1ON0g6DjLaYt github.com/foxxorcat/weiyun-sdk-go v0.1.2 h1:waRWIBmjL9GCcndJ8HvOYrrVB4hhoPYzRrn3I/Cnzqw= github.com/foxxorcat/weiyun-sdk-go v0.1.2/go.mod h1:AKsLFuWhWlClpGrg1zxTdMejugZEZtmhIuElAk3W83s= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gaoyb7/115drive-webdav v0.1.8 h1:EJt4PSmcbvBY4KUh2zSo5p6fN9LZFNkIzuKejipubVw= @@ -152,12 +154,18 @@ github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPr github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-webauthn/webauthn v0.8.6 h1:bKMtL1qzd2WTFkf1mFTVbreYrwn7dsYmEPjTq6QN90E= +github.com/go-webauthn/webauthn v0.8.6/go.mod h1:emwVLMCI5yx9evTTvr0r+aOZCdWJqMfbRhF0MufyUog= +github.com/go-webauthn/x v0.1.4 h1:sGmIFhcY70l6k7JIDfnjVBiAAFEssga5lXIUXe0GtAs= +github.com/go-webauthn/x v0.1.4/go.mod h1:75Ug0oK6KYpANh5hDOanfDI+dvPWHk788naJVG/37H8= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -172,6 +180,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= +github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -271,6 +281,8 @@ github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -412,6 +424,8 @@ github.com/upyun/go-sdk/v3 v3.0.4/go.mod h1:P/SnuuwhrIgAVRd/ZpzDWqCsBAf/oHg7Uggb github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= github.com/winfsp/cgofuse v1.5.1-0.20221118130120-84c0898ad2e0 h1:j3un8DqYvvAOqKI5OPz+/RRVhDFipbPKI4t2Uk5RBJw= github.com/winfsp/cgofuse v1.5.1-0.20221118130120-84c0898ad2e0/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= diff --git a/internal/authn/authn.go b/internal/authn/authn.go new file mode 100644 index 00000000..df1d1fc6 --- /dev/null +++ b/internal/authn/authn.go @@ -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" + }) +} diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go index 4244d29d..4eeb7ce1 100644 --- a/internal/bootstrap/data/setting.go +++ b/internal/bootstrap/data/setting.go @@ -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}, diff --git a/internal/conf/const.go b/internal/conf/const.go index 206f8e1f..b131c661 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -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" diff --git a/internal/db/db.go b/internal/db/db.go index 4f1dcf11..c6a46107 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -29,5 +29,5 @@ func AutoMigrate(dst ...interface{}) error { } func GetDb() *gorm.DB { - return db; + return db } diff --git a/internal/db/user.go b/internal/db/user.go index 497f0905..82292666 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -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)) +} diff --git a/internal/model/user.go b/internal/model/user.go index b528bf59..5bc2850d 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -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" +} diff --git a/server/handles/webauthn.go b/server/handles/webauthn.go new file mode 100644 index 00000000..952cf480 --- /dev/null +++ b/server/handles/webauthn.go @@ -0,0 +1,217 @@ +package handles + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/alist-org/alist/v3/internal/authn" + "github.com/alist-org/alist/v3/internal/conf" + "github.com/alist-org/alist/v3/internal/db" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/internal/op" + "github.com/alist-org/alist/v3/internal/setting" + "github.com/alist-org/alist/v3/server/common" + "github.com/gin-gonic/gin" + "github.com/go-webauthn/webauthn/webauthn" +) + +func BeginAuthnLogin(c *gin.Context) { + enabled := setting.GetBool(conf.WebauthnLoginEnabled) + if !enabled { + common.ErrorStrResp(c, "WebAuthn is not enabled", 403) + return + } + username := c.Query("username") + if username == "" { + common.ErrorStrResp(c, "empty or no username provided", 400) + return + } + user, err := db.GetUserByName(username) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + authnInstance, err := authn.NewAuthnInstance(c.Request) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + + options, sessionData, err := authnInstance.BeginLogin(user) + + if err != nil { + common.ErrorResp(c, err, 400) + return + } + val, err := json.Marshal(sessionData) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + common.SuccessResp(c, gin.H{ + "options": options, + "session": val, + }) +} + +func FinishAuthnLogin(c *gin.Context) { + enabled := setting.GetBool(conf.WebauthnLoginEnabled) + if !enabled { + common.ErrorStrResp(c, "WebAuthn is not enabled", 403) + return + } + username := c.Query("username") + user, err := db.GetUserByName(username) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + + sessionDataString := c.GetHeader("session") + + authnInstance, err := authn.NewAuthnInstance(c.Request) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + sessionDataBytes, err := base64.StdEncoding.DecodeString(sessionDataString) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + + var sessionData webauthn.SessionData + if err := json.Unmarshal(sessionDataBytes, &sessionData); err != nil { + common.ErrorResp(c, err, 400) + return + } + + _, err = authnInstance.FinishLogin(user, sessionData, c.Request) + + if err != nil { + common.ErrorResp(c, err, 400) + return + } + + token, err := common.GenerateToken(user.Username) + if err != nil { + common.ErrorResp(c, err, 400, true) + return + } + common.SuccessResp(c, gin.H{"token": token}) +} + +func BeginAuthnRegistration(c *gin.Context) { + enabled := setting.GetBool(conf.WebauthnLoginEnabled) + if !enabled { + common.ErrorStrResp(c, "WebAuthn is not enabled", 403) + return + } + user := c.MustGet("user").(*model.User) + + authnInstance, err := authn.NewAuthnInstance(c.Request) + if err != nil { + common.ErrorResp(c, err, 400) + } + + options, sessionData, err := authnInstance.BeginRegistration(user) + + if err != nil { + common.ErrorResp(c, err, 400) + } + + val, err := json.Marshal(sessionData) + if err != nil { + common.ErrorResp(c, err, 400) + } + + common.SuccessResp(c, gin.H{ + "options": options, + "session": val, + }) +} + +func FinishAuthnRegistration(c *gin.Context) { + enabled := setting.GetBool(conf.WebauthnLoginEnabled) + if !enabled { + common.ErrorStrResp(c, "WebAuthn is not enabled", 403) + return + } + user := c.MustGet("user").(*model.User) + sessionDataString := c.GetHeader("Session") + + authnInstance, err := authn.NewAuthnInstance(c.Request) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + + sessionDataBytes, err := base64.StdEncoding.DecodeString(sessionDataString) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + + var sessionData webauthn.SessionData + if err := json.Unmarshal(sessionDataBytes, &sessionData); err != nil { + common.ErrorResp(c, err, 400) + return + } + + credential, err := authnInstance.FinishRegistration(user, sessionData, c.Request) + + if err != nil { + common.ErrorResp(c, err, 400) + return + } + err = db.RegisterAuthn(user, credential) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + err = op.DelUserCache(user.Username) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + common.SuccessResp(c, "Registered Successfully") +} + +func DeleteAuthnLogin(c *gin.Context) { + user := c.MustGet("user").(*model.User) + type DeleteAuthnReq struct { + ID string `json:"id"` + } + var req DeleteAuthnReq + err := c.ShouldBind(&req) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + err = db.RemoveAuthn(user, req.ID) + err = op.DelUserCache(user.Username) + if err != nil { + common.ErrorResp(c, err, 400) + return + } + common.SuccessResp(c, "Deleted Successfully") +} + +func GetAuthnCredentials(c *gin.Context) { + type WebAuthnCredentials struct { + ID []byte `json:"id"` + FingerPrint string `json:"fingerprint"` + } + user := c.MustGet("user").(*model.User) + credentials := user.WebAuthnCredentials() + res := make([]WebAuthnCredentials, 0, len(credentials)) + for _, v := range credentials { + credential := WebAuthnCredentials{ + ID: v.ID, + FingerPrint: fmt.Sprintf("% X", v.Authenticator.AAGUID), + } + res = append(res, credential) + } + common.SuccessResp(c, res) +} diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 71077378..14bba567 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -67,6 +67,54 @@ func Auth(c *gin.Context) { c.Next() } +func Authn(c *gin.Context) { + token := c.GetHeader("Authorization") + if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 { + admin, err := op.GetAdmin() + if err != nil { + common.ErrorResp(c, err, 500) + c.Abort() + return + } + c.Set("user", admin) + log.Debugf("use admin token: %+v", admin) + c.Next() + return + } + if token == "" { + guest, err := op.GetGuest() + if err != nil { + common.ErrorResp(c, err, 500) + c.Abort() + return + } + c.Set("user", guest) + log.Debugf("use empty token: %+v", guest) + c.Next() + return + } + userClaims, err := common.ParseToken(token) + if err != nil { + common.ErrorResp(c, err, 401) + c.Abort() + return + } + user, err := op.GetUserByName(userClaims.Username) + if err != nil { + common.ErrorResp(c, err, 401) + c.Abort() + return + } + if user.Disabled { + common.ErrorStrResp(c, "Current user is disabled, replace please", 401) + c.Abort() + return + } + c.Set("user", user) + log.Debugf("use login token: %+v", user) + c.Next() +} + func AuthAdmin(c *gin.Context) { user := c.MustGet("user").(*model.User) if !user.IsAdmin() { diff --git a/server/router.go b/server/router.go index dd4e4328..38034ce8 100644 --- a/server/router.go +++ b/server/router.go @@ -44,6 +44,7 @@ func Init(e *gin.Engine) { api := g.Group("/api") auth := api.Group("", middlewares.Auth) + webauthn := api.Group("/authn", middlewares.Authn) api.POST("/auth/login", handles.Login) api.POST("/auth/login/hash", handles.LoginHash) @@ -52,10 +53,18 @@ func Init(e *gin.Engine) { auth.POST("/auth/2fa/generate", handles.Generate2FA) auth.POST("/auth/2fa/verify", handles.Verify2FA) - // github auth + // auth api.GET("/auth/sso", handles.SSOLoginRedirect) api.GET("/auth/sso_callback", handles.SSOLoginCallback) + //webauthn + webauthn.GET("/webauthn_begin_registration", handles.BeginAuthnRegistration) + webauthn.POST("/webauthn_finish_registration", handles.FinishAuthnRegistration) + webauthn.GET("/webauthn_begin_login", handles.BeginAuthnLogin) + webauthn.POST("/webauthn_finish_login", handles.FinishAuthnLogin) + webauthn.POST("/delete_authn", handles.DeleteAuthnLogin) + webauthn.GET("/getcredentials", handles.GetAuthnCredentials) + // no need auth public := api.Group("/public") public.Any("/settings", handles.PublicSettings)