Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion drivers/cloudreve_v4/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (d *CloudreveV4) login() error {
return errors.New("password not enabled")
}
if prepareLogin.WebauthnEnabled {
return errors.New("webauthn not support")
return errors.New("passkey not support")
}
for range 5 {
err = d.doLogin(siteConfig.LoginCaptcha)
Expand Down
1 change: 0 additions & 1 deletion internal/bootstrap/data/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ func InitialSettings() []model.SettingItem {
{Key: conf.FilenameCharMapping, Value: `{"/": "|"}`, Type: conf.TypeText, Group: model.GLOBAL},
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
{Key: conf.IgnoreDirectLinkParams, Value: "sign,openlist_ts,raw", Type: conf.TypeString, Group: model.GLOBAL},
{Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
{Key: conf.SharePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
{Key: conf.ShareArchivePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
{Key: conf.ShareForceProxy, Value: "true", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},
Expand Down
2 changes: 1 addition & 1 deletion internal/bootstrap/patch/v3_32_0/update_authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

// UpdateAuthnForOldVersion updates users' authn
// First published: bdfc159 fix: webauthn logspam (#6181) by itsHenry
// First published: bdfc159 fix: passkey logspam (#6181) by itsHenry
func UpdateAuthnForOldVersion() {
users, _, err := op.GetUsers(1, -1)
if err != nil {
Expand Down
1 change: 0 additions & 1 deletion internal/conf/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ const (
FilenameCharMapping = "filename_char_mapping"
ForwardDirectLinkParams = "forward_direct_link_params"
IgnoreDirectLinkParams = "ignore_direct_link_params"
WebauthnLoginEnabled = "webauthn_login_enabled"
SharePreview = "share_preview"
ShareArchivePreview = "share_archive_preview"
ShareForceProxy = "share_force_proxy"
Expand Down
29 changes: 24 additions & 5 deletions internal/db/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,18 @@ 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 {
func RegisterAuthn(u *model.User, credential *webauthn.Credential, residentKey, creatorIP, creatorUA string) error {
if u == nil {
return errors.New("user is nil")
}
exists := u.WebAuthnCredentials()
exists := u.WebAuthnCredentialRecords()
if credential != nil {
exists = append(exists, *credential)
exists = append(exists, model.WebAuthnCredentialRecord{
Credential: *credential,
ResidentKey: residentKey,
CreatorIP: creatorIP,
CreatorUA: creatorUA,
})
}
res, err := utils.Json.Marshal(exists)
if err != nil {
Expand All @@ -84,9 +89,9 @@ func RegisterAuthn(u *model.User, credential *webauthn.Credential) error {
}

func RemoveAuthn(u *model.User, id string) error {
exists := u.WebAuthnCredentials()
exists := u.WebAuthnCredentialRecords()
for i := 0; i < len(exists); i++ {
idEncoded := base64.StdEncoding.EncodeToString(exists[i].ID)
idEncoded := base64.StdEncoding.EncodeToString(exists[i].Credential.ID)
if idEncoded == id {
exists[len(exists)-1], exists[i] = exists[i], exists[len(exists)-1]
exists = exists[:len(exists)-1]
Expand All @@ -100,3 +105,17 @@ func RemoveAuthn(u *model.User, id string) error {
}
return UpdateAuthn(u.ID, string(res))
}

func HasLegacyAuthnCredentials() (bool, error) {
var users []model.User
err := db.Select("id", "authn").Where("authn <> '' AND authn <> '[]'").Find(&users).Error
if err != nil {
return false, err
}
for i := range users {
if users[i].HasLegacyWebAuthnCredential() {
return true, nil
}
}
return false, nil
}
59 changes: 56 additions & 3 deletions internal/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/OpenListTeam/go-cache"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -37,6 +38,13 @@ var (
DefaultMaxAuthRetries = 5
)

type WebAuthnCredentialRecord struct {
Credential webauthn.Credential `json:"credential"`
ResidentKey string `json:"residentKey,omitempty"`
CreatorIP string `json:"creatorIP,omitempty"`
CreatorUA string `json:"creatorUA,omitempty"`
}

type User struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key
Username string `json:"username" gorm:"unique" binding:"required"` // username
Expand Down Expand Up @@ -250,12 +258,57 @@ func (u *User) WebAuthnDisplayName() string {
}

func (u *User) WebAuthnCredentials() []webauthn.Credential {
var res []webauthn.Credential
err := json.Unmarshal([]byte(u.Authn), &res)
records := u.WebAuthnCredentialRecords()
res := make([]webauthn.Credential, 0, len(records))
for i := range records {
res = append(res, records[i].Credential)
}
return res
}

func (u *User) WebAuthnCredentialRecords() []WebAuthnCredentialRecord {
var records []WebAuthnCredentialRecord
err := json.Unmarshal([]byte(u.Authn), &records)
if err == nil {
recordParsed := false
for i := range records {
if len(records[i].Credential.ID) > 0 {
recordParsed = true
if records[i].ResidentKey == "" {
records[i].ResidentKey = string(protocol.ResidentKeyRequirementDiscouraged)
}
}
}
if recordParsed || u.Authn == "[]" || u.Authn == "" {
return records
}
}

var credentials []webauthn.Credential
err = json.Unmarshal([]byte(u.Authn), &credentials)
if err != nil {
fmt.Println(err)
return nil
}
return res

records = make([]WebAuthnCredentialRecord, 0, len(credentials))
for i := range credentials {
records = append(records, WebAuthnCredentialRecord{
Credential: credentials[i],
ResidentKey: string(protocol.ResidentKeyRequirementDiscouraged),
})
}
return records
}

func (u *User) HasLegacyWebAuthnCredential() bool {
records := u.WebAuthnCredentialRecords()
for i := range records {
if records[i].ResidentKey == string(protocol.ResidentKeyRequirementDiscouraged) {
return true
}
}
return false
}

func (u *User) WebAuthnIcon() string {
Expand Down
113 changes: 78 additions & 35 deletions server/handles/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,60 +11,82 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/gin-gonic/gin"
"github.com/go-webauthn/webauthn/protocol"
"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
}
authnInstance, err := authn.NewAuthnInstance(c)
if err != nil {
common.ErrorResp(c, err, 400)
return
}

allowCredentials := c.DefaultQuery("allowCredentials", "")
username := c.Query("username")
var (
options *protocol.CredentialAssertion
sessionData *webauthn.SessionData
options *protocol.CredentialAssertion
sessionData *webauthn.SessionData
requireUsername bool
)
if username := c.Query("username"); username != "" {
var user *model.User
user, err = db.GetUserByName(username)
if err == nil {
options, sessionData, err = authnInstance.BeginLogin(user)
switch allowCredentials {
case "yes":
requireUsername = true
if username != "" {
var user *model.User
user, err = db.GetUserByName(username)
if err == nil {
options, sessionData, err = authnInstance.BeginLogin(user)
}
}
} else { // client-side discoverable login
case "no":
options, sessionData, err = authnInstance.BeginDiscoverableLogin()
default:
if username != "" {
var user *model.User
user, err = db.GetUserByName(username)
if err == nil {
options, sessionData, err = authnInstance.BeginLogin(user)
}
} else { // client-side discoverable login
requireUsername, err = db.HasLegacyAuthnCredentials()
if err == nil && !requireUsername {
options, sessionData, err = authnInstance.BeginDiscoverableLogin()
}
}
}
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if requireUsername && username == "" {
common.SuccessResp(c, gin.H{"require_username": true})
return
}

val, err := json.Marshal(sessionData)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
common.SuccessResp(c, gin.H{
"options": options,
"session": val,
"options": options,
"session": val,
"require_username": requireUsername,
})
}

func FinishAuthnLogin(c *gin.Context) {
enabled := setting.GetBool(conf.WebauthnLoginEnabled)
if !enabled {
common.ErrorStrResp(c, "WebAuthn is not enabled", 403)
func LegacyAuthnStatus(c *gin.Context) {
hasLegacy, err := db.HasLegacyAuthnCredentials()
if err != nil {
common.ErrorResp(c, err, 400)
return
}
common.SuccessResp(c, gin.H{"has_legacy": hasLegacy})
}

func FinishAuthnLogin(c *gin.Context) {
authnInstance, err := authn.NewAuthnInstance(c)
if err != nil {
common.ErrorResp(c, err, 400)
Expand Down Expand Up @@ -120,19 +142,21 @@ func FinishAuthnLogin(c *gin.Context) {
}

func BeginAuthnRegistration(c *gin.Context) {
enabled := setting.GetBool(conf.WebauthnLoginEnabled)
if !enabled {
common.ErrorStrResp(c, "WebAuthn is not enabled", 403)
user := c.Request.Context().Value(conf.UserKey).(*model.User)
if user.HasLegacyWebAuthnCredential() && c.Query("upgrade") != "yes" {
common.ErrorStrResp(c, "legacy security key detected, please upgrade or delete it first", 400)
return
}
user := c.Request.Context().Value(conf.UserKey).(*model.User)

authnInstance, err := authn.NewAuthnInstance(c)
if err != nil {
common.ErrorResp(c, err, 400)
}

options, sessionData, err := authnInstance.BeginRegistration(user)
options, sessionData, err := authnInstance.BeginRegistration(
user,
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
)

if err != nil {
common.ErrorResp(c, err, 400)
Expand All @@ -150,11 +174,6 @@ func BeginAuthnRegistration(c *gin.Context) {
}

func FinishAuthnRegistration(c *gin.Context) {
enabled := setting.GetBool(conf.WebauthnLoginEnabled)
if !enabled {
common.ErrorStrResp(c, "WebAuthn is not enabled", 403)
return
}
user := c.Request.Context().Value(conf.UserKey).(*model.User)
sessionDataString := c.GetHeader("Session")

Expand Down Expand Up @@ -182,7 +201,13 @@ func FinishAuthnRegistration(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
err = db.RegisterAuthn(user, credential)
err = db.RegisterAuthn(
user,
credential,
string(protocol.ResidentKeyRequirementRequired),
c.ClientIP(),
c.Request.UserAgent(),
)
if err != nil {
common.ErrorResp(c, err, 400)
return
Expand Down Expand Up @@ -220,17 +245,35 @@ func DeleteAuthnLogin(c *gin.Context) {
}

func GetAuthnCredentials(c *gin.Context) {
type WebAuthnCredentials struct {
type PasskeyCredentials struct {
ID []byte `json:"id"`
FingerPrint string `json:"fingerprint"`
CreatorIP string `json:"creator_ip"`
CreatorUA string `json:"creator_ua"`
IsLegacy bool `json:"is_legacy"`
}
user := c.Request.Context().Value(conf.UserKey).(*model.User)
credentials := user.WebAuthnCredentials()
res := make([]WebAuthnCredentials, 0, len(credentials))
records := user.WebAuthnCredentialRecords()
res := make([]PasskeyCredentials, 0, len(credentials))
for _, v := range credentials {
credential := WebAuthnCredentials{
var creatorIP string
var creatorUA string
var isLegacy bool
for i := range records {
if string(records[i].Credential.ID) == string(v.ID) {
creatorIP = records[i].CreatorIP
creatorUA = records[i].CreatorUA
isLegacy = records[i].ResidentKey == string(protocol.ResidentKeyRequirementDiscouraged)
break
}
}
credential := PasskeyCredentials{
ID: v.ID,
FingerPrint: fmt.Sprintf("% X", v.Authenticator.AAGUID),
CreatorIP: creatorIP,
CreatorUA: creatorUA,
IsLegacy: isLegacy,
}
res = append(res, credential)
}
Expand Down
17 changes: 9 additions & 8 deletions server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func Init(e *gin.Engine) {

api := g.Group("/api")
auth := api.Group("", middlewares.Auth(false))
webauthn := api.Group("/authn", middlewares.Authn)
authn := api.Group("/authn", middlewares.Authn)

api.POST("/auth/login", handles.Login)
api.POST("/auth/login/hash", handles.LoginHash)
Expand All @@ -87,13 +87,14 @@ func Init(e *gin.Engine) {
api.GET("/auth/get_sso_id", handles.SSOLoginCallback)
api.GET("/auth/sso_get_token", handles.SSOLoginCallback)

// webauthn
api.GET("/authn/webauthn_begin_login", handles.BeginAuthnLogin)
api.POST("/authn/webauthn_finish_login", handles.FinishAuthnLogin)
webauthn.GET("/webauthn_begin_registration", handles.BeginAuthnRegistration)
webauthn.POST("/webauthn_finish_registration", handles.FinishAuthnRegistration)
webauthn.POST("/delete_authn", handles.DeleteAuthnLogin)
webauthn.GET("/getcredentials", handles.GetAuthnCredentials)
// passkey
api.GET("/authn/passkey_begin_login", handles.BeginAuthnLogin)
api.GET("/authn/passkey_legacy_status", handles.LegacyAuthnStatus)
api.POST("/authn/passkey_finish_login", handles.FinishAuthnLogin)
authn.GET("/passkey_begin_registration", handles.BeginAuthnRegistration)
authn.POST("/passkey_finish_registration", handles.FinishAuthnRegistration)
authn.POST("/delete_authn", handles.DeleteAuthnLogin)
authn.GET("/getcredentials", handles.GetAuthnCredentials)

// no need auth
public := api.Group("/public")
Expand Down