mirror of https://github.com/cloudreve/Cloudreve
289 lines
8.6 KiB
Go
289 lines
8.6 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/gob"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
|
"github.com/gofrs/uuid"
|
|
"github.com/samber/lo"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func init() {
|
|
gob.Register(webauthn.SessionData{})
|
|
}
|
|
|
|
type authnUser struct {
|
|
hasher hashid.Encoder
|
|
u *ent.User
|
|
credentials []*ent.Passkey
|
|
}
|
|
|
|
func (a *authnUser) WebAuthnID() []byte {
|
|
return []byte(hashid.EncodeUserID(a.hasher, a.u.ID))
|
|
}
|
|
|
|
func (a *authnUser) WebAuthnName() string {
|
|
return a.u.Email
|
|
}
|
|
|
|
func (a *authnUser) WebAuthnDisplayName() string {
|
|
return a.u.Nick
|
|
}
|
|
|
|
func (a *authnUser) WebAuthnCredentials() []webauthn.Credential {
|
|
if a.credentials == nil {
|
|
return nil
|
|
}
|
|
|
|
return lo.Map(a.credentials, func(item *ent.Passkey, index int) webauthn.Credential {
|
|
return *item.Credential
|
|
})
|
|
}
|
|
|
|
const (
|
|
authnSessionKey = "authn_session_"
|
|
)
|
|
|
|
func PreparePasskeyLogin(c *gin.Context) (*PreparePasskeyLoginResponse, error) {
|
|
dep := dependency.FromContext(c)
|
|
webAuthn, err := dep.WebAuthn(c)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
|
}
|
|
|
|
options, sessionData, err := webAuthn.BeginDiscoverableLogin()
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
|
}
|
|
|
|
sessionID := uuid.Must(uuid.NewV4()).String()
|
|
if err := dep.KV().Set(fmt.Sprintf("%s%s", authnSessionKey, sessionID), *sessionData, 300); err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
|
}
|
|
|
|
return &PreparePasskeyLoginResponse{
|
|
Options: options,
|
|
SessionID: sessionID,
|
|
}, nil
|
|
}
|
|
|
|
type (
|
|
FinishPasskeyLoginParameterCtx struct{}
|
|
FinishPasskeyLoginService struct {
|
|
Response string `json:"response" binding:"required"`
|
|
SessionID string `json:"session_id" binding:"required"`
|
|
}
|
|
)
|
|
|
|
func (s *FinishPasskeyLoginService) FinishPasskeyLogin(c *gin.Context) (*ent.User, error) {
|
|
dep := dependency.FromContext(c)
|
|
kv := dep.KV()
|
|
userClient := dep.UserClient()
|
|
|
|
sessionDataRaw, ok := kv.Get(fmt.Sprintf("%s%s", authnSessionKey, s.SessionID))
|
|
if !ok {
|
|
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
|
}
|
|
|
|
_ = kv.Delete(authnSessionKey, s.Response)
|
|
|
|
webAuthn, err := dep.WebAuthn(c)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
|
}
|
|
|
|
sessionData := sessionDataRaw.(webauthn.SessionData)
|
|
pcc, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(s.Response))
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
|
|
}
|
|
|
|
var loginedUser *ent.User
|
|
discoverUserHandle := func(rawID, userHandle []byte) (user webauthn.User, err error) {
|
|
uid, err := dep.HashIDEncoder().Decode(string(userHandle), hashid.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx := context.WithValue(c, inventory.LoadUserPasskey{}, true)
|
|
ctx = context.WithValue(ctx, inventory.LoadUserGroup{}, true)
|
|
u, err := userClient.GetLoginUserByID(ctx, uid)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
|
|
}
|
|
|
|
if inventory.IsAnonymousUser(u) {
|
|
return nil, errors.New("anonymous user")
|
|
}
|
|
|
|
loginedUser = u
|
|
return &authnUser{u: u, hasher: dep.HashIDEncoder(), credentials: u.Edges.Passkey}, nil
|
|
}
|
|
|
|
credential, err := webAuthn.ValidateDiscoverableLogin(discoverUserHandle, sessionData, pcc)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to validate login", err)
|
|
}
|
|
|
|
// Find the credential just used
|
|
usedCredentialId := base64.StdEncoding.EncodeToString(credential.ID)
|
|
usedCredential, found := lo.Find(loginedUser.Edges.Passkey, func(item *ent.Passkey) bool {
|
|
return item.CredentialID == usedCredentialId
|
|
})
|
|
|
|
if !found {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Passkey login passed but credential used is unknown", nil)
|
|
}
|
|
|
|
// Update used at
|
|
if err := userClient.MarkPasskeyUsed(c, loginedUser.ID, usedCredential.CredentialID); err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update passkey", err)
|
|
}
|
|
|
|
return loginedUser, nil
|
|
}
|
|
|
|
func PreparePasskeyRegister(c *gin.Context) (*protocol.CredentialCreation, error) {
|
|
dep := dependency.FromContext(c)
|
|
userClient := dep.UserClient()
|
|
u := inventory.UserFromContext(c)
|
|
|
|
existingKeys, err := userClient.ListPasskeys(c, u.ID)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
|
|
}
|
|
|
|
webAuthn, err := dep.WebAuthn(c)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
|
}
|
|
|
|
authSelect := protocol.AuthenticatorSelection{
|
|
RequireResidentKey: protocol.ResidentKeyRequired(),
|
|
UserVerification: protocol.VerificationPreferred,
|
|
}
|
|
|
|
options, sessionData, err := webAuthn.BeginRegistration(
|
|
&authnUser{u: u, hasher: dep.HashIDEncoder()},
|
|
webauthn.WithAuthenticatorSelection(authSelect),
|
|
webauthn.WithExclusions(lo.Map(existingKeys, func(item *ent.Passkey, index int) protocol.CredentialDescriptor {
|
|
return protocol.CredentialDescriptor{
|
|
Type: protocol.PublicKeyCredentialType,
|
|
CredentialID: item.Credential.ID,
|
|
Transport: item.Credential.Transport,
|
|
AttestationType: item.Credential.AttestationType,
|
|
}
|
|
})),
|
|
)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
|
}
|
|
|
|
if err := dep.KV().Set(fmt.Sprintf("%s%d", authnSessionKey, u.ID), *sessionData, 300); err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
|
}
|
|
|
|
return options, nil
|
|
}
|
|
|
|
type (
|
|
FinishPasskeyRegisterParameterCtx struct{}
|
|
FinishPasskeyRegisterService struct {
|
|
Response string `json:"response" binding:"required"`
|
|
Name string `json:"name" binding:"required"`
|
|
UA string `json:"ua" binding:"required"`
|
|
}
|
|
)
|
|
|
|
func (s *FinishPasskeyRegisterService) FinishPasskeyRegister(c *gin.Context) (*Passkey, error) {
|
|
dep := dependency.FromContext(c)
|
|
kv := dep.KV()
|
|
u := inventory.UserFromContext(c)
|
|
|
|
sessionDataRaw, ok := kv.Get(fmt.Sprintf("%s%d", authnSessionKey, u.ID))
|
|
if !ok {
|
|
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
|
}
|
|
|
|
_ = kv.Delete(authnSessionKey, strconv.Itoa(u.ID))
|
|
|
|
webAuthn, err := dep.WebAuthn(c)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
|
}
|
|
|
|
sessionData := sessionDataRaw.(webauthn.SessionData)
|
|
pcc, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(s.Response))
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
|
|
}
|
|
|
|
credential, err := webAuthn.CreateCredential(&authnUser{u: u, hasher: dep.HashIDEncoder()}, sessionData, pcc)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to finish registration", err)
|
|
}
|
|
|
|
client := dep.UAParser().Parse(s.UA)
|
|
name := util.Replace(map[string]string{
|
|
"{os}": client.Os.Family,
|
|
"{browser}": client.UserAgent.Family,
|
|
}, s.Name)
|
|
|
|
passkey, err := dep.UserClient().AddPasskey(c, u.ID, name, credential)
|
|
if err != nil {
|
|
return nil, serializer.NewError(serializer.CodeDBError, "Failed to add passkey", err)
|
|
}
|
|
|
|
res := BuildPasskey(passkey)
|
|
return &res, nil
|
|
}
|
|
|
|
type (
|
|
DeletePasskeyService struct {
|
|
ID string `form:"id" binding:"required"`
|
|
}
|
|
DeletePasskeyParameterCtx struct{}
|
|
)
|
|
|
|
func (s *DeletePasskeyService) DeletePasskey(c *gin.Context) error {
|
|
dep := dependency.FromContext(c)
|
|
u := inventory.UserFromContext(c)
|
|
userClient := dep.UserClient()
|
|
|
|
existingKeys, err := userClient.ListPasskeys(c, u.ID)
|
|
if err != nil {
|
|
return serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
|
|
}
|
|
|
|
var existing *ent.Passkey
|
|
for _, key := range existingKeys {
|
|
if key.CredentialID == s.ID {
|
|
existing = key
|
|
break
|
|
}
|
|
}
|
|
|
|
if existing == nil {
|
|
return serializer.NewError(serializer.CodeNotFound, "Passkey not found", nil)
|
|
}
|
|
|
|
if err := userClient.RemovePasskey(c, u.ID, s.ID); err != nil {
|
|
return serializer.NewError(serializer.CodeDBError, "Failed to delete passkey", err)
|
|
}
|
|
|
|
return nil
|
|
}
|