You've already forked openaccounting-server
forked from cybercinch/openaccounting-server
feat: implement secure file upload system with JWT authentication
- Add JWT-based secure file access for local storage with 1-hour expiry - Implement GORM repository methods for attachment CRUD operations - Add secure file serving endpoint with token validation - Update storage interface to support user context in URL generation - Add comprehensive security features including path traversal protection - Update documentation with security model and configuration examples - Add utility functions for hex/byte conversion and UUID validation - Configure secure file permissions (0600) for uploaded files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
83
core/storage/token.go
Normal file
83
core/storage/token.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/openaccounting/oa-server/core/util/id"
|
||||
)
|
||||
|
||||
// TokenService handles JWT token generation and validation for file access
|
||||
type TokenService struct {
|
||||
signingKey []byte
|
||||
}
|
||||
|
||||
// NewTokenService creates a new token service with a signing key
|
||||
func NewTokenService(signingKey string) *TokenService {
|
||||
if signingKey == "" {
|
||||
// Generate a random signing key if none provided
|
||||
// In production, this should be a consistent secret from config
|
||||
signingKey = id.String(id.New())
|
||||
}
|
||||
|
||||
return &TokenService{
|
||||
signingKey: []byte(signingKey),
|
||||
}
|
||||
}
|
||||
|
||||
// FileClaims represents the JWT claims for file access
|
||||
type FileClaims struct {
|
||||
FilePath string `json:"file_path"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
OrgID string `json:"org_id,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateFileToken creates a JWT token for accessing a specific file
|
||||
func (ts *TokenService) GenerateFileToken(filePath string, userID, orgID string, expiry time.Duration) (string, error) {
|
||||
now := time.Now()
|
||||
claims := FileClaims{
|
||||
FilePath: filePath,
|
||||
UserID: userID,
|
||||
OrgID: orgID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(expiry)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Issuer: "openaccounting-server",
|
||||
Subject: "file-access",
|
||||
ID: id.String(id.New()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(ts.signingKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ValidateFileToken validates a JWT token and returns the file claims
|
||||
func (ts *TokenService) ValidateFileToken(tokenString string) (*FileClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &FileClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// Verify the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return ts.signingKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse token: %w", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*FileClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
Reference in New Issue
Block a user