Tencent/WeKnora · error

unmarshal oidc state: %w

Error message

unmarshal oidc state: %w

What it means

The state token's payload decoded and its HMAC verified, but the JSON inside is not a valid OIDCStatePayload. json.Unmarshal failed, meaning the signed bytes are not the expected JSON object with redirect_uri and iat fields. Because the signature matched, this usually indicates a schema/version change in what was signed rather than an attack.

Source

Thrown at internal/utils/oidc_state.go:93

	if len(parts) != 2 {
		return nil, errors.New("invalid oidc state format")
	}
	payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return nil, fmt.Errorf("decode oidc state payload: %w", err)
	}
	sigBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, fmt.Errorf("decode oidc state signature: %w", err)
	}
	mac := hmac.New(sha256.New, []byte(oidcStateSigningKey()))
	mac.Write(payloadBytes)
	if !hmac.Equal(mac.Sum(nil), sigBytes) {
		return nil, errors.New("oidc state signature mismatch")
	}
	var payload OIDCStatePayload
	if err := json.Unmarshal(payloadBytes, &payload); err != nil {
		return nil, fmt.Errorf("unmarshal oidc state: %w", err)
	}
	if strings.TrimSpace(payload.RedirectURI) == "" {
		return nil, errors.New("state.redirect_uri is required")
	}
	if payload.IssuedAt == 0 {
		return nil, errors.New("state.iat is required")
	}
	issuedAt := time.Unix(payload.IssuedAt, 0)
	if time.Since(issuedAt) > oidcStateMaxAge || time.Until(issuedAt) > time.Minute {
		return nil, errors.New("oidc state expired or invalid timestamp")
	}
	return &payload, nil
}

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Ensure the same OIDCStatePayload struct/field names are used by SignOIDCState and all verifying instances (align deployments before rolling out schema changes)
  2. Check json tags on OIDCStatePayload; iat must be a number and redirect_uri a string in the signed JSON
  3. Clear in-flight states (old cookies) after a schema change so users get a fresh sign→verify cycle
  4. Inspect the wrapped UnmarshalTypeError/ SyntaxError to see which field or syntax failed

Example fix

// before: signing a plain string
mac.Write([]byte(redirectURI))
// after: sign the structured payload
payload, _ := json.Marshal(OIDCStatePayload{RedirectURI: redirectURI, IssuedAt: time.Now().Unix()})
mac.Write(payload)
Defensive patterns

Strategy: try-catch

Validate before calling

func payloadLooksLikeJSON(state string) bool {
    parts := strings.Split(state, ".")
    if len(parts) != 2 { return false }
    b, err := base64.RawURLEncoding.DecodeString(parts[0])
    if err != nil { return false }
    var probe map[string]json.RawMessage
    return json.Unmarshal(b, &probe) == nil && probe["redirect_uri"] != nil && probe["iat"] != nil
}

Type guard

func isCompatibleOIDCState(b []byte) bool {
    var p struct {
        RedirectURI string `json:"redirect_uri"`
        IssuedAt    int64  `json:"iat"`
    }
    return json.Unmarshal(b, &p) == nil && p.RedirectURI != "" && p.IssuedAt > 0
}

Try / catch

payload, err := utils.VerifyOIDCState(rawState)
if err != nil {
    if strings.Contains(err.Error(), "unmarshal oidc state") {
        // schema drift (rolling deploy / old cookie): issue a fresh state
        http.Redirect(w, r, startOIDCFlow(), http.StatusFound)
        return
    }
    http.Error(w, "invalid state", http.StatusBadRequest)
}

Prevention

When it happens

Trigger: VerifyOIDCState receiving a state signed by a different code version whose payload layout differs (e.g. signed a raw string or a different struct); an old deployment signing while a new one verifies during rolling deploys; key rotation reusing an unrelated blob as payload.

Common situations: Rolling deployments with divergent OIDCStatePayload schemas; manually crafted states in tests signed with the right key but wrong JSON shape; migrating state signing between libraries without keeping field names/types (iat as number vs string) stable.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/75f4c640afe67238. Report an issue: GitHub.