From 8881df9f7a881f829dc0c78ed21faccef4c883ed Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 17 Sep 2026 13:38:54 +0200 Subject: [PATCH] fix(02-03): capture OAuth code from JSON redirect URLs Consent returns the callback URL in JSON, so replay can fill {{oauth:code}} from $.data.redirect_to before the token request. Co-authored-by: Cursor --- tide/capture_test.go | 20 ++++++++++++++++++++ tide/variables.go | 15 +++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/tide/capture_test.go b/tide/capture_test.go index d1fc16f..be030a4 100644 --- a/tide/capture_test.go +++ b/tide/capture_test.go @@ -79,6 +79,26 @@ func TestCaptureAndPlaceholderResolution(t *testing.T) { if !strings.Contains(string(escaped.Response.Body), "{{oauth:redirect}}") { t.Fatalf("php-escaped redirect not placeholder: %s", escaped.Response.Body) } + codeStep := Step{ + ID: "consent-code", + Response: Response{ + Body: Body(`{"data":{"redirect_to":"http://127.0.0.1:8424/oauth/callback?code=oauthCodeFromJSON"}}`), + }, + Capture: []CaptureRule{{ + From: "response.json.query", + Path: "$.data.redirect_to", + Name: "code", + As: "oauth:code", + Category: "oauth_code", + }}, + } + if err := CaptureStep(store, &codeStep); err != nil { + t.Fatal(err) + } + gotCode, ok := store.Get("oauth:code") + if !ok || gotCode != "oauthCodeFromJSON" { + t.Fatalf("json.query code: ok=%v val=%q", ok, gotCode) + } step2 := Step{ ID: "pkce", diff --git a/tide/variables.go b/tide/variables.go index 57c4bda..59a0e71 100644 --- a/tide/variables.go +++ b/tide/variables.go @@ -249,6 +249,21 @@ func extractCapture(rule CaptureRule, req Request, resp Response) (string, error return "", err } return scalarString(v), nil + case "response.json.query": + v, err := jsonPathValue([]byte(resp.Body), rule.Path) + if err != nil { + return "", err + } + raw := scalarString(v) + u, err := url.Parse(raw) + if err != nil { + return "", err + } + q := u.Query().Get(rule.Name) + if q == "" { + return "", fmt.Errorf("missing JSON URL query %s", rule.Name) + } + return q, nil case "response.header": v := headerValue(resp.Headers, rule.Name) if v == "" {