Fix go generated token expired in python - 2 (#906) (#17875)

This commit is contained in:
Wang Qi
2026-08-05 17:29:08 +08:00
committed by GitHub
parent 0227b2684e
commit eb9b621061
3 changed files with 124 additions and 64 deletions

7
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -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))