Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
288
service/user/passkey.go
Normal file
288
service/user/passkey.go
Normal file
@@ -0,0 +1,288 @@
|
||||
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.Sprint("%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.Sprint("%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.Sprint("%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.Sprint("%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
|
||||
}
|
||||
Reference in New Issue
Block a user