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:
217
server/handles/webauthn.go
Normal file
217
server/handles/webauthn.go
Normal file
@ -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)
|
||||
}
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user