diff --git a/go.mod b/go.mod index deeb25ebb1..7de8df30d6 100644 --- a/go.mod +++ b/go.mod @@ -31,20 +31,20 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/gomarkdown/markdown v0.0.0-20260614204949-e08cff860f76 github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/infiniflow/infinity-go-sdk v0.0.0-00010101000000-000000000000 - github.com/iromli/go-itsdangerous v0.0.0-20220223194502-9c8bef8dac6a github.com/json-iterator/go v1.1.12 github.com/kaptinlin/jsonrepair v0.4.8 github.com/larksuite/oapi-sdk-go/v3 v3.9.9 github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 github.com/lib/pq v1.10.9 github.com/minio/minio-go/v7 v7.0.99 - github.com/mitchellh/mapstructure v1.5.0 github.com/nats-io/nats-server/v2 v2.14.3 github.com/nats-io/nats.go v1.52.0 github.com/nikolalohinski/gonja v1.5.3 github.com/open-dingtalk/dingtalk-stream-sdk-go v0.0.5 github.com/peterh/liner v1.2.2 + github.com/pkg/errors v0.9.1 github.com/pkoukk/tiktoken-go v0.1.8 github.com/redis/go-redis/v9 v9.18.0 github.com/signintech/gopdf v0.36.1 @@ -145,7 +145,6 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.17 // indirect github.com/googleapis/gax-go/v2 v2.23.0 // indirect github.com/goph/emperror v0.17.2 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -160,6 +159,7 @@ require ( github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/highwayhash v1.0.4 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nats-io/jwt/v2 v2.8.2 // indirect @@ -169,7 +169,6 @@ require ( github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.1 // indirect diff --git a/go.sum b/go.sum index 29df731181..d893f700d7 100644 --- a/go.sum +++ b/go.sum @@ -311,8 +311,6 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/infiniflow/infinity/go v0.0.0-20260723093510-ceb4bc518010 h1:TKHL99MrpO2d+q6zRUSPRWsxTXo+pojFr3teiK8HLWw= github.com/infiniflow/infinity/go v0.0.0-20260723093510-ceb4bc518010/go.mod h1:hw3z5AwNFsGy1cdrE0Mfjot2y9jqVHTxBufUx9VzZ+0= -github.com/iromli/go-itsdangerous v0.0.0-20220223194502-9c8bef8dac6a h1:Inib12UR9HAfBubrGNraPjKt/Cu8xPbTJbC50+0wP5U= -github.com/iromli/go-itsdangerous v0.0.0-20220223194502-9c8bef8dac6a/go.mod h1:8N0Hlye5Lzw+H/yHWpZMkT0QLA+iOHG7KLdvAm95DZg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= diff --git a/internal/utility/token.go b/internal/utility/token.go index 7807fb3162..171e72aebb 100644 --- a/internal/utility/token.go +++ b/internal/utility/token.go @@ -17,21 +17,28 @@ package utility import ( + "bytes" + "compress/zlib" + "crypto/hmac" "crypto/rand" "crypto/sha1" "encoding/base64" + "encoding/binary" "encoding/hex" + "encoding/json" "errors" "fmt" + "io" "strings" + "time" "github.com/google/uuid" - "github.com/iromli/go-itsdangerous" ) +const accessTokenExpireSeconds = 86400 + // ExtractAccessToken extract access token from authorization header // This is equivalent to: str(jwt.loads(authorization)) in Python -// Uses github.com/iromli/go-itsdangerous for itsdangerous compatibility func ExtractAccessToken(authorization, secretKey string) (string, error) { if authorization == "" { return "", errors.New("empty authorization") @@ -40,37 +47,19 @@ func ExtractAccessToken(authorization, secretKey string) (string, error) { // Strip "Bearer " prefix if present token := strings.TrimPrefix(authorization, "Bearer ") - // Create URLSafeTimedSerializer with correct configuration - // Matching Python itsdangerous configuration: - // - salt: "itsdangerous" - // - key_derivation: "django-concat" - // - digest_method: sha1 - algo := &itsdangerous.HMACAlgorithm{DigestMethod: sha1.New} - signer := itsdangerous.NewTimestampSignature( - secretKey, - "itsdangerous", - ".", - "django-concat", - sha1.New, - algo, - ) - - // Unsign the token (verifies signature and extracts payload) - encodedValue, err := signer.Unsign(token, 0) + encodedValue, err := unsignAccessToken(token, secretKey, accessTokenExpireSeconds) if err != nil { return "", fmt.Errorf("failed to decode token: %w", err) } - // Base64 decode the payload - jsonValue, err := urlSafeB64Decode(encodedValue) + jsonValue, err := decodePayload(encodedValue) if err != nil { return "", fmt.Errorf("failed to decode payload: %w", err) } - // Parse JSON string (remove surrounding quotes) - value := string(jsonValue) - if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { - value = value[1 : len(value)-1] + var value string + if err := json.Unmarshal(jsonValue, &value); err != nil { + return "", fmt.Errorf("failed to parse payload: %w", err) } return value, nil @@ -78,55 +67,129 @@ func ExtractAccessToken(authorization, secretKey string) (string, error) { // DumpAccessToken creates an authorization token from access token // This is equivalent to: jwt.dumps(access_token) in Python -// Uses github.com/iromli/go-itsdangerous for itsdangerous compatibility func DumpAccessToken(accessToken, secretKey string) (string, error) { if accessToken == "" { return "", errors.New("empty access token") } - // Create URLSafeTimedSerializer with correct configuration - // Matching Python itsdangerous configuration: - // - salt: "itsdangerous" - // - key_derivation: "django-concat" - // - digest_method: sha1 - algo := &itsdangerous.HMACAlgorithm{DigestMethod: sha1.New} - signer := itsdangerous.NewTimestampSignature( - secretKey, - "itsdangerous", - ".", - "django-concat", - sha1.New, - algo, - ) - - // Encode the access token as JSON string (add surrounding quotes) - jsonValue := fmt.Sprintf("\"%s\"", accessToken) - encodedValue := urlSafeB64Encode([]byte(jsonValue)) - - // Sign the token (creates signature) - token, err := signer.Sign(encodedValue) + jsonValue, err := json.Marshal(accessToken) if err != nil { - return "", fmt.Errorf("failed to sign token: %w", err) + return "", fmt.Errorf("failed to encode payload: %w", err) } - return token, nil + return signAccessToken(encodePayload(jsonValue), secretKey), nil } // urlSafeB64Decode URL-safe base64 decode func urlSafeB64Decode(s string) ([]byte, error) { - // Add padding if needed - padding := 4 - len(s)%4 - if padding != 4 { - s += strings.Repeat("=", padding) - } - return base64.URLEncoding.DecodeString(s) + return base64.RawURLEncoding.DecodeString(s) } // urlSafeB64Encode URL-safe base64 encode (without padding) func urlSafeB64Encode(data []byte) string { - encoded := base64.URLEncoding.EncodeToString(data) - // Remove padding - return strings.TrimRight(encoded, "=") + return base64.RawURLEncoding.EncodeToString(data) +} + +func encodePayload(jsonValue []byte) string { + var buf bytes.Buffer + zw := zlib.NewWriter(&buf) + _, _ = zw.Write(jsonValue) + _ = zw.Close() + + compressed := buf.Bytes() + if len(compressed) < len(jsonValue)-1 { + return "." + urlSafeB64Encode(compressed) + } + return urlSafeB64Encode(jsonValue) +} + +func decodePayload(payload string) ([]byte, error) { + decompress := false + if strings.HasPrefix(payload, ".") { + payload = payload[1:] + decompress = true + } + + jsonValue, err := urlSafeB64Decode(payload) + if err != nil { + return nil, err + } + if !decompress { + return jsonValue, nil + } + + zr, err := zlib.NewReader(bytes.NewReader(jsonValue)) + if err != nil { + return nil, err + } + defer zr.Close() + return io.ReadAll(zr) +} + +func signAccessToken(payload, secretKey string) string { + value := payload + "." + urlSafeB64Encode(intToBytes(time.Now().Unix())) + return value + "." + getAccessTokenSignature(value, secretKey) +} + +func unsignAccessToken(token, secretKey string, maxAge int64) (string, error) { + token = strings.TrimSpace(token) + + li := strings.LastIndex(token, ".") + if li < 0 { + return "", errors.New("signature missing") + } + value := token[:li] + sig := token[li+1:] + if !hmac.Equal([]byte(sig), []byte(getAccessTokenSignature(value, secretKey))) { + return "", errors.New("signature does not match") + } + + li = strings.LastIndex(value, ".") + if li < 0 { + return "", errors.New("timestamp missing") + } + payload := value[:li] + tsValue := value[li+1:] + tsBytes, err := urlSafeB64Decode(tsValue) + if err != nil { + return "", fmt.Errorf("malformed timestamp: %w", err) + } + timestamp := bytesToInt(tsBytes) + if maxAge > 0 { + age := time.Now().Unix() - timestamp + if age > maxAge { + return "", fmt.Errorf("signature age %d > %d seconds", age, maxAge) + } + if age < 0 { + return "", fmt.Errorf("signature age %d < 0 seconds", age) + } + } + return payload, nil +} + +func getAccessTokenSignature(value, secretKey string) string { + h := sha1.New() + h.Write([]byte("itsdangerous" + "signer" + secretKey)) + key := h.Sum(nil) + + mac := hmac.New(sha1.New, key) + mac.Write([]byte(value)) + return urlSafeB64Encode(mac.Sum(nil)) +} + +func intToBytes(num int64) []byte { + var buf [8]byte + binary.BigEndian.PutUint64(buf[:], uint64(num)) + return bytes.TrimLeft(buf[:], "\x00") +} + +func bytesToInt(data []byte) int64 { + if len(data) > 8 { + data = data[len(data)-8:] + } + var buf [8]byte + copy(buf[8-len(data):], data) + return int64(binary.BigEndian.Uint64(buf[:])) } // GenerateSecretKey generates a 32-byte hex string (equivalent to Python's secrets.token_hex(32))