You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

720 lines
20 KiB
Go

package service
import (
"crypto/hmac"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/buserr"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
"github.com/1Panel-dev/1Panel/core/utils/mfa"
"github.com/1Panel-dev/1Panel/core/utils/passkey"
"github.com/gin-gonic/gin"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
)
type AuthService struct{}
type IAuthService interface {
GetResponsePage() (string, error)
VerifyCode(code string) (bool, error)
Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error)
LogOut(c *gin.Context) error
MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, string, error)
PasskeyBeginLogin(c *gin.Context, entrance string) (*dto.PasskeyBeginResponse, string, error)
PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*dto.UserLoginInfo, string, error)
PasskeyBeginRegister(c *gin.Context, name string) (*dto.PasskeyBeginResponse, string, error)
PasskeyFinishRegister(c *gin.Context, sessionID string) (string, error)
PasskeyList() ([]dto.PasskeyInfo, error)
PasskeyDelete(id string) error
PasskeyStatus(c *gin.Context) bool
GetSecurityEntrance() string
IsLogin(c *gin.Context) bool
}
func NewIAuthService() IAuthService {
return &AuthService{}
}
func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error) {
nameSetting, err := settingRepo.Get(repo.WithByKey("UserName"))
if err != nil {
return nil, "", buserr.New("ErrRecordNotFound")
}
if nameSetting.Value != info.Name {
return nil, "ErrAuth", buserr.New("ErrAuth")
}
if err = checkPassword(info.Password); err != nil {
return nil, "ErrAuth", err
}
entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
if err != nil {
return nil, "", err
}
if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance {
return nil, "ErrEntrance", buserr.New("ErrEntrance")
}
mfa, err := settingRepo.Get(repo.WithByKey("MFAStatus"))
if err != nil {
return nil, "", err
}
if err = settingRepo.Update("Language", info.Language); err != nil {
return nil, "", err
}
if mfa.Value == constant.StatusEnable {
return &dto.UserLoginInfo{Name: nameSetting.Value, MfaStatus: mfa.Value}, "", nil
}
res, err := u.generateSession(c, info.Name)
if err != nil {
return nil, "", err
}
if entrance != "" {
entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance))
c.SetCookie("SecurityEntrance", entranceValue, 0, "", "", false, true)
}
return res, "", nil
}
func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, string, error) {
nameSetting, err := settingRepo.Get(repo.WithByKey("UserName"))
if err != nil {
return nil, "", buserr.New("ErrRecordNotFound")
}
if nameSetting.Value != info.Name {
return nil, "ErrAuth", nil
}
if err = checkPassword(info.Password); err != nil {
return nil, "ErrAuth", err
}
entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
if err != nil {
return nil, "", err
}
if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance {
return nil, "", buserr.New("ErrEntrance")
}
mfaSecret, err := settingRepo.Get(repo.WithByKey("MFASecret"))
if err != nil {
return nil, "", err
}
mfaInterval, err := settingRepo.Get(repo.WithByKey("MFAInterval"))
if err != nil {
return nil, "", err
}
success := mfa.ValidCode(info.Code, mfaInterval.Value, mfaSecret.Value)
if !success {
return nil, "ErrAuth", nil
}
res, err := u.generateSession(c, info.Name)
if err != nil {
return nil, "", err
}
if entrance != "" {
entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance))
c.SetCookie("SecurityEntrance", entranceValue, 0, "", "", false, true)
}
return res, "", nil
}
func (u *AuthService) generateSession(c *gin.Context, name string) (*dto.UserLoginInfo, error) {
setting, err := settingRepo.Get(repo.WithByKey("SessionTimeout"))
if err != nil {
return nil, err
}
httpsSetting, err := settingRepo.Get(repo.WithByKey("SSL"))
if err != nil {
return nil, err
}
lifeTime, err := strconv.Atoi(setting.Value)
if err != nil {
return nil, err
}
sessionUser, err := global.SESSION.Get(c)
if err != nil {
err := global.SESSION.Set(c, sessionUser, httpsSetting.Value == constant.StatusEnable, lifeTime)
if err != nil {
return nil, err
}
return &dto.UserLoginInfo{Name: name}, nil
}
if err := global.SESSION.Set(c, sessionUser, httpsSetting.Value == constant.StatusEnable, lifeTime); err != nil {
return nil, err
}
return &dto.UserLoginInfo{Name: name}, nil
}
func (u *AuthService) LogOut(c *gin.Context) error {
httpsSetting, err := settingRepo.Get(repo.WithByKey("SSL"))
if err != nil {
return err
}
sID, _ := c.Cookie(constant.SessionName)
if sID != "" {
c.SetCookie(constant.SessionName, sID, -1, "", "", httpsSetting.Value == constant.StatusEnable, true)
err := global.SESSION.Delete(c)
if err != nil {
return err
}
}
return nil
}
func (u *AuthService) VerifyCode(code string) (bool, error) {
setting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
if err != nil {
return false, err
}
return hmac.Equal([]byte(setting.Value), []byte(code)), nil
}
func (u *AuthService) GetResponsePage() (string, error) {
pageCode, err := settingRepo.Get(repo.WithByKey("NoAuthSetting"))
if err != nil {
return "", err
}
return pageCode.Value, nil
}
func (u *AuthService) GetSecurityEntrance() string {
status, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
if err != nil {
return ""
}
if len(status.Value) == 0 {
return ""
}
return status.Value
}
func (u *AuthService) IsLogin(c *gin.Context) bool {
_, err := global.SESSION.Get(c)
return err == nil
}
func (u *AuthService) PasskeyStatus(c *gin.Context) bool {
enabled, err := u.passkeyEnabled(c)
if err != nil {
global.LOG.Errorf("passkey enabled check failed, err: %v", err)
enabled = false
}
configured, err := u.passkeyConfigured()
if err != nil {
global.LOG.Errorf("passkey config check failed, err: %v", err)
configured = false
}
return enabled && configured
}
func (u *AuthService) PasskeyBeginLogin(c *gin.Context, entrance string) (*dto.PasskeyBeginResponse, string, error) {
if err := u.checkEntrance(entrance); err != nil {
return nil, "ErrEntrance", err
}
config, msgKey, err := u.passkeyConfig(c)
if err != nil {
return nil, msgKey, err
}
records, err := loadPasskeyCredentialRecords()
if err != nil {
return nil, "", err
}
if len(records) == 0 {
return nil, "ErrPasskeyNotConfigured", buserr.New("ErrPasskeyNotConfigured")
}
user, err := u.passkeyUser(records, true)
if err != nil {
return nil, "", err
}
wa, err := webauthn.New(config)
if err != nil {
return nil, "", err
}
assertion, sessionData, err := wa.BeginLogin(user)
if err != nil {
return nil, "", err
}
passkeySessions := passkey.GetPasskeySessionStore()
sessionID := passkeySessions.Set(passkey.PasskeySessionKindLogin, "", *sessionData)
return &dto.PasskeyBeginResponse{SessionID: sessionID, PublicKey: assertion.Response}, "", nil
}
func (u *AuthService) PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*dto.UserLoginInfo, string, error) {
if sessionID == "" {
return nil, "ErrPasskeySession", buserr.New("ErrPasskeySession")
}
if err := u.checkEntrance(entrance); err != nil {
return nil, "ErrEntrance", err
}
config, msgKey, err := u.passkeyConfig(c)
if err != nil {
return nil, msgKey, err
}
passkeySessions := passkey.GetPasskeySessionStore()
session, ok := passkeySessions.Get(sessionID)
if !ok || session.Kind != passkey.PasskeySessionKindLogin {
return nil, "ErrPasskeySession", buserr.New("ErrPasskeySession")
}
passkeySessions.Delete(sessionID)
records, err := loadPasskeyCredentialRecords()
if err != nil {
return nil, "", err
}
if len(records) == 0 {
return nil, "ErrPasskeyNotConfigured", buserr.New("ErrPasskeyNotConfigured")
}
user, err := u.passkeyUser(records, true)
if err != nil {
return nil, "", err
}
wa, err := webauthn.New(config)
if err != nil {
return nil, "", err
}
credential, err := wa.FinishLogin(user, session.Session, c.Request)
if err != nil {
return nil, "ErrAuth", err
}
if err := updatePasskeyCredentialRecord(records, credential); err != nil {
return nil, "ErrAuth", err
}
if err := savePasskeyCredentialRecords(records); err != nil {
return nil, "", err
}
userSetting, err := settingRepo.Get(repo.WithByKey("UserName"))
if err != nil {
return nil, "", err
}
res, err := u.generateSession(c, userSetting.Value)
if err != nil {
return nil, "", err
}
if entrance != "" {
entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance))
c.SetCookie("SecurityEntrance", entranceValue, 0, "", "", false, true)
}
return res, "", nil
}
func (u *AuthService) PasskeyBeginRegister(c *gin.Context, name string) (*dto.PasskeyBeginResponse, string, error) {
config, msgKey, err := u.passkeyConfig(c)
if err != nil {
return nil, msgKey, err
}
records, err := loadPasskeyCredentialRecords()
if err != nil {
return nil, "", err
}
if len(records) >= passkey.PasskeyMaxCredentials {
return nil, "ErrPasskeyLimit", buserr.New("ErrPasskeyLimit")
}
user, err := u.passkeyUser(records, true)
if err != nil {
return nil, "", err
}
wa, err := webauthn.New(config)
if err != nil {
return nil, "", err
}
exclusions := make([]protocol.CredentialDescriptor, len(user.Credentials))
for i, credential := range user.Credentials {
exclusions[i] = credential.Descriptor()
}
creation, sessionData, err := wa.BeginRegistration(user, webauthn.WithExclusions(exclusions))
if err != nil {
return nil, "", err
}
passkeySessions := passkey.GetPasskeySessionStore()
sessionID := passkeySessions.Set(passkey.PasskeySessionKindRegister, strings.TrimSpace(name), *sessionData)
return &dto.PasskeyBeginResponse{SessionID: sessionID, PublicKey: creation.Response}, "", nil
}
func (u *AuthService) PasskeyFinishRegister(c *gin.Context, sessionID string) (string, error) {
if sessionID == "" {
return "ErrPasskeySession", buserr.New("ErrPasskeySession")
}
config, msgKey, err := u.passkeyConfig(c)
if err != nil {
return msgKey, err
}
passkeySessions := passkey.GetPasskeySessionStore()
session, ok := passkeySessions.Get(sessionID)
if !ok || session.Kind != passkey.PasskeySessionKindRegister {
return "ErrPasskeySession", buserr.New("ErrPasskeySession")
}
passkeySessions.Delete(sessionID)
records, err := loadPasskeyCredentialRecords()
if err != nil {
return "", err
}
if len(records) >= passkey.PasskeyMaxCredentials {
return "ErrPasskeyLimit", buserr.New("ErrPasskeyLimit")
}
user, err := u.passkeyUser(records, true)
if err != nil {
return "", err
}
wa, err := webauthn.New(config)
if err != nil {
return "", err
}
credential, err := wa.FinishRegistration(user, session.Session, c.Request)
if err != nil {
return "ErrPasskeyVerify", err
}
if passkeyCredentialExists(records, credential.ID) {
return "ErrPasskeyDuplicate", buserr.New("ErrPasskeyDuplicate")
}
displayName := strings.TrimSpace(session.Name)
if displayName == "" {
displayName = fmt.Sprintf("%s-%s", passkey.PasskeyCredentialNameDefault, time.Now().Format("20060102150405"))
}
records = append(records, passkey.PasskeyCredentialRecord{
ID: base64.RawURLEncoding.EncodeToString(credential.ID),
Name: displayName,
CreatedAt: time.Now().Format(constant.DateTimeLayout),
LastUsedAt: "",
FlagsValue: credentialFlagsValue(credential.Flags),
Credential: *credential,
})
if err := savePasskeyCredentialRecords(records); err != nil {
return "", err
}
return "", nil
}
func (u *AuthService) PasskeyList() ([]dto.PasskeyInfo, error) {
records, err := loadPasskeyCredentialRecords()
if err != nil {
return nil, err
}
list := make([]dto.PasskeyInfo, 0, len(records))
for _, record := range records {
list = append(list, dto.PasskeyInfo{
ID: record.ID,
Name: record.Name,
CreatedAt: record.CreatedAt,
LastUsedAt: record.LastUsedAt,
})
}
return list, nil
}
func (u *AuthService) PasskeyDelete(id string) error {
records, err := loadPasskeyCredentialRecords()
if err != nil {
return err
}
index := -1
for i, record := range records {
if record.ID == id {
index = i
break
}
}
if index == -1 {
return buserr.New("ErrRecordNotFound")
}
records = append(records[:index], records[index+1:]...)
return savePasskeyCredentialRecords(records)
}
func (u *AuthService) passkeyEnabled(c *gin.Context) (bool, error) {
sslSetting, err := settingRepo.Get(repo.WithByKey("SSL"))
if err != nil {
return false, err
}
if sslSetting.Value == constant.StatusDisable {
return false, nil
}
return strings.EqualFold(passkeyRequestScheme(c), "https"), nil
}
func (u *AuthService) passkeyConfigured() (bool, error) {
bindDomain, err := settingRepo.Get(repo.WithByKey("BindDomain"))
if err != nil {
return false, err
}
if strings.TrimSpace(bindDomain.Value) == "" {
return false, nil
}
records, err := loadPasskeyCredentialRecords()
if err != nil {
return false, err
}
return len(records) > 0, nil
}
func (u *AuthService) passkeyUser(records []passkey.PasskeyCredentialRecord, allowCreate bool) (*passkey.PasskeyUser, error) {
userID, err := u.passkeyUserID(allowCreate)
if err != nil {
return nil, err
}
nameSetting, err := settingRepo.Get(repo.WithByKey("UserName"))
if err != nil {
return nil, err
}
credentials := make([]webauthn.Credential, len(records))
for i, record := range records {
credentials[i] = record.Credential
}
return &passkey.PasskeyUser{
ID: userID,
Name: nameSetting.Value,
DisplayName: nameSetting.Value,
Credentials: credentials,
}, nil
}
func (u *AuthService) passkeyUserID(allowCreate bool) ([]byte, error) {
setting, err := settingRepo.Get(repo.WithByKey(passkey.PasskeyUserIDSettingKey))
if err != nil {
return nil, err
}
if setting.Value == "" {
if !allowCreate {
return nil, buserr.New("ErrPasskeyNotConfigured")
}
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
return nil, err
}
encoded := base64.RawURLEncoding.EncodeToString(raw)
if err := settingRepo.Update(passkey.PasskeyUserIDSettingKey, encoded); err != nil {
return nil, err
}
return raw, nil
}
raw, err := base64.RawURLEncoding.DecodeString(setting.Value)
if err != nil {
return nil, err
}
return raw, nil
}
func (u *AuthService) passkeyConfig(c *gin.Context) (*webauthn.Config, string, error) {
enabled, err := u.passkeyEnabled(c)
if err != nil {
return nil, "", err
}
if !enabled {
return nil, "ErrPasskeyDisabled", buserr.New("ErrPasskeyDisabled")
}
origin, rpID, err := passkeyOriginAndRPID(c)
if err != nil {
return nil, "", err
}
panelName, err := settingRepo.Get(repo.WithByKey("PanelName"))
if err != nil {
return nil, "", err
}
return &webauthn.Config{
RPID: rpID,
RPDisplayName: panelName.Value,
RPOrigins: []string{origin},
AuthenticatorSelection: protocol.AuthenticatorSelection{
UserVerification: protocol.VerificationRequired,
},
}, "", nil
}
func (u *AuthService) checkEntrance(entrance string) error {
entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance"))
if err != nil {
return err
}
if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance {
return buserr.New("ErrEntrance")
}
return nil
}
func loadPasskeyCredentialRecords() ([]passkey.PasskeyCredentialRecord, error) {
setting, err := settingRepo.Get(repo.WithByKey(passkey.PasskeyCredentialSettingKey))
if err != nil {
return nil, err
}
if setting.Value == "" {
return []passkey.PasskeyCredentialRecord{}, nil
}
decrypted, err := encrypt.StringDecrypt(setting.Value)
if err != nil {
return nil, err
}
var records []passkey.PasskeyCredentialRecord
if err := json.Unmarshal([]byte(decrypted), &records); err != nil {
return nil, err
}
for i := range records {
records[i].Credential.Flags = webauthn.NewCredentialFlags(protocol.AuthenticatorFlags(records[i].FlagsValue))
}
return records, nil
}
func savePasskeyCredentialRecords(records []passkey.PasskeyCredentialRecord) error {
if len(records) == 0 {
return settingRepo.Update(passkey.PasskeyCredentialSettingKey, "")
}
for i := range records {
records[i].FlagsValue = credentialFlagsValue(records[i].Credential.Flags)
}
raw, err := json.Marshal(records)
if err != nil {
return err
}
encrypted, err := encrypt.StringEncrypt(string(raw))
if err != nil {
return err
}
return settingRepo.Update(passkey.PasskeyCredentialSettingKey, encrypted)
}
func passkeyCredentialExists(records []passkey.PasskeyCredentialRecord, credentialID []byte) bool {
encoded := base64.RawURLEncoding.EncodeToString(credentialID)
for _, record := range records {
if record.ID == encoded {
return true
}
}
return false
}
func updatePasskeyCredentialRecord(records []passkey.PasskeyCredentialRecord, credential *webauthn.Credential) error {
encoded := base64.RawURLEncoding.EncodeToString(credential.ID)
for i := range records {
if records[i].ID == encoded {
records[i].Credential = *credential
records[i].FlagsValue = credentialFlagsValue(credential.Flags)
records[i].LastUsedAt = time.Now().Format(constant.DateTimeLayout)
return nil
}
}
return buserr.New("ErrPasskeyNotConfigured")
}
func credentialFlagsValue(flags webauthn.CredentialFlags) uint8 {
var value protocol.AuthenticatorFlags
if flags.UserPresent {
value |= protocol.FlagUserPresent
}
if flags.UserVerified {
value |= protocol.FlagUserVerified
}
if flags.BackupEligible {
value |= protocol.FlagBackupEligible
}
if flags.BackupState {
value |= protocol.FlagBackupState
}
return uint8(value)
}
func passkeyOriginAndRPID(c *gin.Context) (string, string, error) {
host := passkeyRequestHost(c)
if host == "" {
return "", "", fmt.Errorf("missing request host")
}
scheme := passkeyRequestScheme(c)
origin := fmt.Sprintf("%s://%s", scheme, host)
bindDomain, err := settingRepo.Get(repo.WithByKey("BindDomain"))
if err != nil {
return "", "", err
}
bindDomainValue := strings.TrimSpace(bindDomain.Value)
if bindDomainValue == "" {
return "", "", buserr.New("ErrPasskeyNotConfigured")
}
hostDomain := stripHostPort(host)
bindDomainValue = stripHostPort(bindDomainValue)
if hostDomain == "" || !strings.EqualFold(hostDomain, bindDomainValue) {
return "", "", buserr.New("ErrPasskeyDisabled")
}
return origin, bindDomainValue, nil
}
func passkeyRequestHost(c *gin.Context) string {
host := c.Request.Host
if strings.Contains(host, ",") {
host = strings.TrimSpace(strings.Split(host, ",")[0])
}
return strings.TrimSpace(host)
}
func passkeyRequestScheme(c *gin.Context) string {
if c.Request.TLS != nil {
return "https"
}
return "http"
}
func stripHostPort(hostport string) string {
if hostport == "" {
return hostport
}
hostport = strings.TrimSpace(hostport)
if host, _, err := net.SplitHostPort(hostport); err == nil {
return strings.Trim(host, "[]")
}
return strings.Trim(hostport, "[]")
}
func checkPassword(password string) error {
priKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PRIVATE_KEY"))
privateKey, err := encrypt.ParseRSAPrivateKey(priKey.Value)
if err != nil {
return err
}
loginPassword, err := encrypt.DecryptPassword(password, privateKey)
if err != nil {
return err
}
passwordSetting, err := settingRepo.Get(repo.WithByKey("Password"))
if err != nil {
return err
}
existPassword, err := encrypt.StringDecrypt(passwordSetting.Value)
if err != nil {
return err
}
if !hmac.Equal([]byte(loginPassword), []byte(existPassword)) {
return buserr.New("ErrAuth")
}
return nil
}