diff --git a/tp/client.go b/tp/client.go index 22a4868..3cc0f7f 100644 --- a/tp/client.go +++ b/tp/client.go @@ -17,6 +17,13 @@ import ( "github.com/superfly/macaroon/bundle" ) +// ErrMissingUserURLCallback is returned when a third party asks for the user +// to be sent to a URL in their browser, but the client has no +// WithUserURLCallback to send them with. Callers that can fall back to an +// interactive flow can test for it with errors.Is, including on the joined +// error FetchDischargeTokens returns for a whole set of tickets. +var ErrMissingUserURLCallback = errors.New("missing user-url callback") + type ClientOption func(*Client) // WithHTTP specifies the HTTP client to use for requests to third parties. @@ -321,7 +328,7 @@ func (c *Client) doUserInteractive(ctx context.Context, ui *jsonUserInteractive) return "", errors.New("bad discharge response") } if c.userURLCallback == nil { - return "", errors.New("missing user-url callback") + return "", ErrMissingUserURLCallback } if err := c.openUserInteractiveURL(ctx, ui.UserURL); err != nil { diff --git a/tp/tp_test.go b/tp/tp_test.go index 3cbc4f9..ad8cfc0 100644 --- a/tp/tp_test.go +++ b/tp/tp_test.go @@ -2,6 +2,7 @@ package tp import ( "context" + "errors" "net/http" "net/http/httptest" "strings" @@ -202,6 +203,23 @@ func TestTP(t *testing.T) { assert.Equal(t, []string{"fp-cav", "dis-cav"}, cavs) }) + t.Run("user interactive response without a callback", func(t *testing.T) { + handleInit = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := CaveatsFromRequest(r) + assert.NoError(t, err) + + tp.RespondUserInteractive(w, r) + }) + + hdr := genFP(t, tp, myCaveat("fp-cav")) + + // the error comes back joined with whatever the other tickets did, so + // this is the check a caller has to be able to make to decide whether + // retrying with a callback is worth it. + _, err = NewClient(firstPartyLocation).FetchDischargeTokens(context.Background(), hdr) + assert.True(t, errors.Is(err, ErrMissingUserURLCallback), "got %v", err) + }) + t.Run("user interactive response", func(t *testing.T) { userSecret := ""