package service import ( "crypto/hmac" "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" "net" "strings" "time" "github.com/1Panel-dev/1Panel/core/app/auth" "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/init/session/psession" "github.com/1Panel-dev/1Panel/core/utils/encrypt" "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" "gorm.io/gorm" ) type AuthService struct{} type IAuthService interface { GetResponsePage() (string, error) VerifyCode(code string) (bool, error) LogOut(c *gin.Context) 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) 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) c.SetCookie(constant.CSRFTokenName, "", -1, "/", "", httpsSetting.Value == constant.StatusEnable, false) 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 } sessionUser := psession.SessionUser{ID: psession.SuperAdminSessionUserID, Name: userSetting.Value, Role: "ADMIN"} res, err := auth.GenerateSession(c, sessionUser) if err != nil { return nil, "", err } if entrance != "" { SetSecurityEntranceCookie(c, entrance) } return res, "", nil } func SetSecurityEntranceCookie(c *gin.Context, entrance string) { entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance)) sslEnabled := false if setting, err := settingRepo.Get(repo.WithByKey("SSL")); err == nil { sslEnabled = setting.Value == constant.StatusEnable } c.SetCookie("SecurityEntrance", entranceValue, 0, "/", "", sslEnabled, true) } 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) { 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" } if !passkeyIsFromTrustedProxy(c) { return "http" } if proto := passkeyForwardedProto(c.GetHeader("Forwarded")); proto != "" { return proto } if proto := passkeyXForwardedProto(c.GetHeader("X-Forwarded-Proto")); proto != "" { return proto } return "http" } func passkeyIsFromTrustedProxy(c *gin.Context) bool { remoteIP := passkeyRemoteIP(c.Request.RemoteAddr) if remoteIP == nil { return false } proxies, err := loadPasskeyTrustedProxies() if err != nil { global.LOG.Errorf("load passkey trusted proxies failed, err: %v", err) return false } for _, cidr := range proxies { if cidr.Contains(remoteIP) { return true } } return false } func passkeyRemoteIP(remoteAddr string) net.IP { if host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr)); err == nil { return net.ParseIP(host) } return net.ParseIP(strings.TrimSpace(remoteAddr)) } func loadPasskeyTrustedProxies() ([]*net.IPNet, error) { setting, err := settingRepo.Get(repo.WithByKey("PasskeyTrustedProxies")) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return parsePasskeyTrustedProxies("127.0.0.1\n::1") } return nil, err } return parsePasskeyTrustedProxies(setting.Value) } func parsePasskeyTrustedProxies(value string) ([]*net.IPNet, error) { lines := strings.Split(strings.TrimSpace(value), "\n") if len(lines) == 0 { return []*net.IPNet{}, nil } cidrs := make([]*net.IPNet, 0, len(lines)) for _, line := range lines { item := strings.TrimSpace(line) if item == "" { continue } if ip := net.ParseIP(item); ip != nil { if ip.To4() != nil { item += "/32" } else { item += "/128" } } _, ipNet, err := net.ParseCIDR(item) if err != nil { return nil, err } cidrs = append(cidrs, ipNet) } return cidrs, nil } func passkeyForwardedProto(value string) string { if value == "" { return "" } for _, item := range strings.Split(value, ",") { for _, part := range strings.Split(item, ";") { kv := strings.SplitN(strings.TrimSpace(part), "=", 2) if len(kv) != 2 { continue } if !strings.EqualFold(strings.TrimSpace(kv[0]), "proto") { continue } proto := strings.ToLower(strings.Trim(strings.TrimSpace(kv[1]), `"`)) if proto == "http" || proto == "https" { return proto } } } return "" } func passkeyXForwardedProto(value string) string { if value == "" { return "" } proto := strings.ToLower(strings.TrimSpace(strings.Split(value, ",")[0])) if proto == "http" || proto == "https" { return proto } return "" } 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, "[]") }