mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
45e5fda9d0
Adds a full Phase-52 RBAC implementation and multiple server/frontend fixes. Key changes: new auth/permissions.go with 28 granular permissions and DefaultRolePermissions, expanded 7-role hierarchy and helpers in auth/roles.go, JWT org context and GenerateOrgToken, requirePermission/requireOrgMembership middlewares (Go + Node.js), DB schema & adapter changes for role_permissions and is_server_admin, org role boundary checks and peer org scoping, and guards for last-admin demotion and self-demotion. Also: TCP EOF/connection-reset log filtering in signal/relay servers, improved startup banner port display, KEYS_PATH auto-detect warning, CSS hover/transition layout fixes, admin password race mitigation, ID-change ghost peer cleanup, added Tauri ACL schema files, and a new RBAC_PHASE52.md doc. Misc: numerous web-nodejs i18n, CSS, JS and route updates and an updated .github/copilot-instructions.md timestamp/summary.
132 lines
3.5 KiB
Go
132 lines
3.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Claims represents the JWT payload.
|
|
type Claims struct {
|
|
Sub string `json:"sub"` // Subject (username)
|
|
Role string `json:"role"` // User role (admin, operator, viewer)
|
|
OrgID string `json:"org_id,omitempty"` // Organization ID (empty = global/server-level user)
|
|
Iat int64 `json:"iat"` // Issued at (Unix)
|
|
Exp int64 `json:"exp"` // Expires at (Unix)
|
|
Jti string `json:"jti,omitempty"` // JWT ID (for revocation)
|
|
}
|
|
|
|
// JWTManager generates and validates HS256 JWT tokens.
|
|
type JWTManager struct {
|
|
secret []byte
|
|
expiry time.Duration
|
|
}
|
|
|
|
type jwtHeader struct {
|
|
Alg string `json:"alg"`
|
|
Typ string `json:"typ"`
|
|
}
|
|
|
|
var (
|
|
ErrInvalidToken = errors.New("auth: invalid token")
|
|
ErrTokenExpired = errors.New("auth: token expired")
|
|
)
|
|
|
|
// NewJWTManager creates a JWT manager with the given secret and token expiry duration.
|
|
func NewJWTManager(secret string, expiry time.Duration) *JWTManager {
|
|
return &JWTManager{
|
|
secret: []byte(secret),
|
|
expiry: expiry,
|
|
}
|
|
}
|
|
|
|
// Generate creates a new signed JWT token for the given subject and role.
|
|
func (m *JWTManager) Generate(subject, role string) (string, error) {
|
|
return m.GenerateWithTTL(subject, role, m.expiry)
|
|
}
|
|
|
|
// GenerateWithTTL creates a new signed JWT token with a custom time-to-live.
|
|
// This is used for short-lived tokens such as partial 2FA tokens (H4).
|
|
func (m *JWTManager) GenerateWithTTL(subject, role string, ttl time.Duration) (string, error) {
|
|
return m.GenerateOrgToken(subject, role, "", ttl)
|
|
}
|
|
|
|
// GenerateOrgToken creates a signed JWT token with an organization context.
|
|
// If orgID is empty, the token is a global/server-level token.
|
|
func (m *JWTManager) GenerateOrgToken(subject, role, orgID string, ttl time.Duration) (string, error) {
|
|
jti, err := GenerateRandomString(16)
|
|
if err != nil {
|
|
return "", fmt.Errorf("auth: generate jti: %w", err)
|
|
}
|
|
now := time.Now().Unix()
|
|
claims := Claims{
|
|
Sub: subject,
|
|
Role: role,
|
|
OrgID: orgID,
|
|
Iat: now,
|
|
Exp: now + int64(ttl.Seconds()),
|
|
Jti: jti,
|
|
}
|
|
|
|
hdr := b64URLEncode(mustJSON(jwtHeader{Alg: "HS256", Typ: "JWT"}))
|
|
pay := b64URLEncode(mustJSON(claims))
|
|
sig := m.sign(hdr + "." + pay)
|
|
|
|
return hdr + "." + pay + "." + sig, nil
|
|
}
|
|
|
|
// Validate parses and verifies a JWT token. Returns claims if valid.
|
|
func (m *JWTManager) Validate(token string) (*Claims, error) {
|
|
parts := strings.SplitN(token, ".", 3)
|
|
if len(parts) != 3 {
|
|
return nil, ErrInvalidToken
|
|
}
|
|
|
|
// Verify signature (constant-time comparison)
|
|
expected := m.sign(parts[0] + "." + parts[1])
|
|
if !hmac.Equal([]byte(parts[2]), []byte(expected)) {
|
|
return nil, ErrInvalidToken
|
|
}
|
|
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("auth: decode payload: %w", err)
|
|
}
|
|
|
|
var claims Claims
|
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
|
return nil, fmt.Errorf("auth: parse claims: %w", err)
|
|
}
|
|
|
|
if time.Now().Unix() > claims.Exp {
|
|
return nil, ErrTokenExpired
|
|
}
|
|
|
|
return &claims, nil
|
|
}
|
|
|
|
// Expiry returns the configured token expiry duration.
|
|
func (m *JWTManager) Expiry() time.Duration {
|
|
return m.expiry
|
|
}
|
|
|
|
func (m *JWTManager) sign(data string) string {
|
|
h := hmac.New(sha256.New, m.secret)
|
|
h.Write([]byte(data))
|
|
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func b64URLEncode(data []byte) string {
|
|
return base64.RawURLEncoding.EncodeToString(data)
|
|
}
|
|
|
|
func mustJSON(v any) []byte {
|
|
data, _ := json.Marshal(v)
|
|
return data
|
|
}
|