package auth import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "time" ) type tokenPayload struct { Subject string `json:"sub"` Type string `json:"typ,omitempty"` OpenID string `json:"openid,omitempty"` UnionID string `json:"unionid,omitempty"` Issued int64 `json:"iat"` Expires int64 `json:"exp"` } func signToken(userID string, secret string, ttl time.Duration, now time.Time) (string, int64, error) { if ttl <= 0 { ttl = 7 * 24 * time.Hour } return signTypedToken(userID, "access", secret, ttl, now) } func signRefreshToken(userID string, secret string, ttl time.Duration, now time.Time) (string, int64, error) { if ttl <= 0 { ttl = 30 * 24 * time.Hour } return signTypedToken(userID, "refresh", secret, ttl, now) } func signTypedToken(userID string, tokenType string, secret string, ttl time.Duration, now time.Time) (string, int64, error) { payload := tokenPayload{ Subject: userID, Type: tokenType, Issued: now.Unix(), Expires: now.Add(ttl).Unix(), } token, err := signPayload(payload, secret) return token, int64(ttl.Seconds()), err } func parseToken(ctx context.Context, token string, secret string, now time.Time) (string, bool) { if err := ctx.Err(); err != nil { return "", false } parts := strings.Split(strings.TrimSpace(token), ".") if len(parts) != 2 { return "", false } expected := signBytes([]byte(parts[0]), secret) actual, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil || !hmac.Equal(expected, actual) { return "", false } data, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return "", false } var payload tokenPayload if err := json.Unmarshal(data, &payload); err != nil { return "", false } if payload.Subject == "" || payload.Expires <= now.Unix() { return "", false } if payload.Type != "" && payload.Type != "access" { return "", false } return payload.Subject, true } func signWechatBindingToken(identity WechatIdentity, secret string, ttl time.Duration, now time.Time) (string, int64, error) { if ttl <= 0 { ttl = 5 * time.Minute } if strings.TrimSpace(identity.OpenID) == "" { return "", 0, fmt.Errorf("%w: wechat identity is incomplete", ErrInvalidCredentials) } payload := tokenPayload{ Type: "wechat_binding", OpenID: strings.TrimSpace(identity.OpenID), UnionID: strings.TrimSpace(identity.UnionID), Issued: now.Unix(), Expires: now.Add(ttl).Unix(), } token, err := signPayload(payload, secret) return token, int64(ttl.Seconds()), err } func parseWechatBindingToken(ctx context.Context, token string, secret string, now time.Time) (WechatIdentity, bool) { payload, ok := parsePayload(ctx, token, secret) if !ok { return WechatIdentity{}, false } if payload.Type != "wechat_binding" || payload.OpenID == "" || payload.Expires <= now.Unix() { return WechatIdentity{}, false } return WechatIdentity{OpenID: payload.OpenID, UnionID: payload.UnionID}, true } func signPayload(payload tokenPayload, secret string) (string, error) { data, err := json.Marshal(payload) if err != nil { return "", err } encodedPayload := base64.RawURLEncoding.EncodeToString(data) signature := signBytes([]byte(encodedPayload), secret) return encodedPayload + "." + base64.RawURLEncoding.EncodeToString(signature), nil } func parsePayload(ctx context.Context, token string, secret string) (tokenPayload, bool) { if err := ctx.Err(); err != nil { return tokenPayload{}, false } parts := strings.Split(strings.TrimSpace(token), ".") if len(parts) != 2 { return tokenPayload{}, false } expected := signBytes([]byte(parts[0]), secret) actual, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil || !hmac.Equal(expected, actual) { return tokenPayload{}, false } data, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return tokenPayload{}, false } var payload tokenPayload if err := json.Unmarshal(data, &payload); err != nil { return tokenPayload{}, false } return payload, true } func signState(secret string, ttl time.Duration, now time.Time) (string, int64, error) { stateID := newID() token, expiresIn, err := signToken(stateID, secret, ttl, now) if err != nil { return "", 0, err } return token, expiresIn, nil } func verifyState(ctx context.Context, state string, secret string, now time.Time) error { if strings.TrimSpace(state) == "" { return fmt.Errorf("%w: state is required", ErrInvalidCredentials) } if _, ok := parseToken(ctx, state, secret, now); !ok { return fmt.Errorf("%w: state is invalid", ErrInvalidCredentials) } return nil } func hashVerificationCode(code string, target string, secret string) string { value := strings.TrimSpace(target) + ":" + strings.TrimSpace(code) return base64.RawURLEncoding.EncodeToString(signBytes([]byte(value), secret)) } func signBytes(data []byte, secret string) []byte { secret = strings.TrimSpace(secret) if secret == "" { secret = "local-dev-change-me" } mac := hmac.New(sha256.New, []byte(secret)) _, _ = mac.Write(data) return mac.Sum(nil) } func bearerToken(header string) (string, bool) { header = strings.TrimSpace(header) if header == "" { return "", false } parts := strings.Fields(header) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { return "", false } return parts[1], true } func normalizeTokenErr(err error) error { if err == nil { return nil } if errors.Is(err, ErrInvalidCredentials) { return err } return fmt.Errorf("%w: %v", ErrInvalidCredentials, err) }