diff --git a/auth/authenticator.go b/auth/authenticator.go index 373f7de..b52eab9 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -71,6 +71,14 @@ func (l *L402Authenticator) Accept(header *http.Header, serviceName string) bool return true } +const ( + // lsatAuthScheme is an outdated RFC 7235 auth-scheme used by aperture. + lsatAuthScheme = "LSAT" + + // l402AuthScheme is the current RFC 7235 auth-scheme used by aperture. + l402AuthScheme = "L402" +) + // FreshChallengeHeader returns a header containing a challenge for the user to // complete. // @@ -95,11 +103,19 @@ func (l *L402Authenticator) FreshChallengeHeader(r *http.Request, log.Errorf("Error serializing L402: %v", err) } - str := fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"", + str := fmt.Sprintf("macaroon=\"%s\", invoice=\"%s\"", base64.StdEncoding.EncodeToString(macBytes), paymentRequest) header := r.Header - header.Set("WWW-Authenticate", str) - log.Debugf("Created new challenge header: [%s]", str) + // Old loop software (via ClientInterceptor code of aperture) looks + // for "LSAT" in the first instance of WWW-Authenticate header, so + // legacy header must go first not to break backward compatibility. + lsatValue := lsatAuthScheme + " " + str + l402Value := l402AuthScheme + " " + str + header.Set("WWW-Authenticate", lsatValue) + log.Debugf("Created new challenge header: [%s]", lsatValue) + header.Add("WWW-Authenticate", l402Value) + log.Debugf("Created new challenge header: [%s]", l402Value) + return header, nil } diff --git a/auth/mock_authenticator.go b/auth/mock_authenticator.go index e52ff17..d5a9470 100644 --- a/auth/mock_authenticator.go +++ b/auth/mock_authenticator.go @@ -35,13 +35,14 @@ func (a MockAuthenticator) FreshChallengeHeader(r *http.Request, _ string, _ int64) (http.Header, error) { header := r.Header - header.Set( - "WWW-Authenticate", "LSAT macaroon=\"AGIAJEemVQUTEyNCR0exk7ek9"+ - "0Cg==\", invoice=\"lnbc1500n1pw5kjhmpp5fu6xhthlt2vucm"+ - "zkx6c7wtlh2r625r30cyjsfqhu8rsx4xpz5lwqdpa2fjkzep6yptk"+ - "sct5yp5hxgrrv96hx6twvusycn3qv9jx7ur5d9hkugr5dusx6cqzp"+ - "gxqr23s79ruapxc4j5uskt4htly2salw4drq979d7rcela9wz02el"+ - "hypmdzmzlnxuknpgfyfm86pntt8vvkvffma5qc9n50h4mvqhngadq"+ - "y3ngqjcym5a\"") + str := "macaroon=\"AGIAJEemVQUTEyNCR0exk7ek9" + + "0Cg==\", invoice=\"lnbc1500n1pw5kjhmpp5fu6xhthlt2vucm" + + "zkx6c7wtlh2r625r30cyjsfqhu8rsx4xpz5lwqdpa2fjkzep6yptk" + + "sct5yp5hxgrrv96hx6twvusycn3qv9jx7ur5d9hkugr5dusx6cqzp" + + "gxqr23s79ruapxc4j5uskt4htly2salw4drq979d7rcela9wz02el" + + "hypmdzmzlnxuknpgfyfm86pntt8vvkvffma5qc9n50h4mvqhngadq" + + "y3ngqjcym5a\"" + header.Set("WWW-Authenticate", lsatAuthScheme+" "+str) + header.Add("WWW-Authenticate", l402AuthScheme+" "+str) return header, nil } diff --git a/l402/client_interceptor.go b/l402/client_interceptor.go index b9fe03b..4ff6f0d 100644 --- a/l402/client_interceptor.go +++ b/l402/client_interceptor.go @@ -66,7 +66,7 @@ var ( // authHeaderRegex is the regular expression the payment challenge must // match for us to be able to parse the macaroon and invoice. authHeaderRegex = regexp.MustCompile( - "LSAT macaroon=\"(.*?)\", invoice=\"(.*?)\"", + "(LSAT|L402) macaroon=\"(.*?)\", invoice=\"(.*?)\"", ) // errPaymentFailedTerminally is signaled by the payment tracking method @@ -338,19 +338,26 @@ func (i *ClientInterceptor) payL402Token(ctx context.Context, md *metadata.MD) ( // First parse the authentication header that was stored in the // metadata. - authHeader := md.Get(AuthHeader) - if len(authHeader) == 0 { + authHeaders := md.Get(AuthHeader) + if len(authHeaders) == 0 { return nil, fmt.Errorf("auth header not found in response") } - matches := authHeaderRegex.FindStringSubmatch(authHeader[0]) - if len(matches) != 3 { + // Find the first WWW-Authenticate header, matching authHeaderRegex. + var matches []string + for _, authHeader := range authHeaders { + matches = authHeaderRegex.FindStringSubmatch(authHeader) + if len(matches) == 4 { + break + } + } + if len(matches) != 4 { return nil, fmt.Errorf("invalid auth header "+ - "format: %s", authHeader[0]) + "format: %s", authHeaders[0]) } // Decode the base64 macaroon and the invoice so we can store the // information in our store later. - macBase64, invoiceStr := matches[1], matches[2] + macBase64, invoiceStr := matches[2], matches[3] macBytes, err := base64.StdEncoding.DecodeString(macBase64) if err != nil { return nil, fmt.Errorf("base64 decode of macaroon failed: "+ diff --git a/l402/client_interceptor_test.go b/l402/client_interceptor_test.go index e80762b..5e6fd9e 100644 --- a/l402/client_interceptor_test.go +++ b/l402/client_interceptor_test.go @@ -23,7 +23,7 @@ type interceptTestCase struct { name string initialPreimage *lntypes.Preimage interceptor *ClientInterceptor - resetCb func() + resetCb func(addL402 bool) expectLndCall bool expectSecondLndCall bool sendPaymentCb func(*testing.T, test.PaymentChannelMessage) @@ -73,17 +73,19 @@ var ( testMacHex = hex.EncodeToString(testMacBytes) paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5} backendErr error - backendAuth = "" + backendAuth = []string{} callMD map[string]string numBackendCalls = 0 overallWg sync.WaitGroup backendWg sync.WaitGroup testCases = []interceptTestCase{{ - name: "no auth required happy path", - initialPreimage: nil, - interceptor: interceptor, - resetCb: func() { resetBackend(nil, "") }, + name: "no auth required happy path", + initialPreimage: nil, + interceptor: interceptor, + resetCb: func(addL402 bool) { + resetBackend(nil, []string{}) + }, expectLndCall: false, expectToken: false, expectBackendCalls: 1, @@ -93,10 +95,10 @@ var ( name: "auth required, no token yet", initialPreimage: nil, interceptor: interceptor, - resetCb: func() { + resetCb: func(addL402 bool) { resetBackend( status.New(GRPCErrCode, GRPCErrMessage).Err(), - makeAuthHeader(testMacBytes), + makeAuthHeaders(testMacBytes, addL402), ) }, expectLndCall: true, @@ -107,7 +109,7 @@ var ( // The next call to the "backend" shouldn't return an // error. - resetBackend(nil, "") + resetBackend(nil, []string{}) msg.Done <- lndclient.PaymentResult{ Preimage: paidPreimage, PaidAmt: 123, @@ -124,10 +126,12 @@ var ( expectMacaroonCall1: false, expectMacaroonCall2: true, }, { - name: "auth required, has token", - initialPreimage: &paidPreimage, - interceptor: interceptor, - resetCb: func() { resetBackend(nil, "") }, + name: "auth required, has token", + initialPreimage: &paidPreimage, + interceptor: interceptor, + resetCb: func(addL402 bool) { + resetBackend(nil, []string{}) + }, expectLndCall: false, expectToken: true, expectBackendCalls: 1, @@ -137,10 +141,10 @@ var ( name: "auth required, has pending token", initialPreimage: &zeroPreimage, interceptor: interceptor, - resetCb: func() { + resetCb: func(addL402 bool) { resetBackend( status.New(GRPCErrCode, GRPCErrMessage).Err(), - makeAuthHeader(testMacBytes), + makeAuthHeaders(testMacBytes, addL402), ) }, expectLndCall: true, @@ -154,7 +158,7 @@ var ( // The next call to the "backend" shouldn't return an // error. - resetBackend(nil, "") + resetBackend(nil, []string{}) msg.Updates <- lndclient.PaymentStatus{ State: lnrpc.Payment_SUCCEEDED, Preimage: paidPreimage, @@ -168,10 +172,10 @@ var ( name: "auth required, has pending but expired token", initialPreimage: &zeroPreimage, interceptor: interceptor, - resetCb: func() { + resetCb: func(addL402 bool) { resetBackend( status.New(GRPCErrCode, GRPCErrMessage).Err(), - makeAuthHeader(testMacBytes), + makeAuthHeaders(testMacBytes, addL402), ) }, expectLndCall: true, @@ -183,7 +187,7 @@ var ( // The next call to the "backend" shouldn't return an // error. - resetBackend(nil, "") + resetBackend(nil, []string{}) msg.Done <- lndclient.PaymentResult{ Preimage: paidPreimage, PaidAmt: 123, @@ -195,7 +199,7 @@ var ( // The next call to the "backend" shouldn't return an // error. - resetBackend(nil, "") + resetBackend(nil, []string{}) msg.Updates <- lndclient.PaymentStatus{ State: lnrpc.Payment_FAILED, } @@ -211,10 +215,10 @@ var ( &lnd.LndServices, store, testTimeout, 100, DefaultMaxRoutingFeeSats, false, ), - resetCb: func() { + resetCb: func(addL402 bool) { resetBackend( status.New(GRPCErrCode, GRPCErrMessage).Err(), - makeAuthHeader(testMacBytes), + makeAuthHeaders(testMacBytes, addL402), ) }, expectLndCall: false, @@ -230,7 +234,7 @@ var ( // resetBackend is used by the test cases to define the behaviour of the // simulated backend and reset its starting conditions. -func resetBackend(expectedErr error, expectedAuth string) { +func resetBackend(expectedErr error, expectedAuth []string) { backendErr = expectedErr backendAuth = expectedAuth callMD = nil @@ -252,9 +256,9 @@ func invoker(opts []grpc.CallOption) error { // Should we simulate an auth header response? trailer, ok := opt.(grpc.TrailerCallOption) - if ok && backendAuth != "" { + if ok && len(backendAuth) != 0 { trailer.TrailerAddr.Set( - AuthHeader, backendAuth, + AuthHeader, backendAuth..., ) } } @@ -284,8 +288,11 @@ func TestUnaryInterceptor(t *testing.T) { ctx, "", nil, nil, nil, unaryInvoker, nil, ) } - t.Run(tc.name, func(t *testing.T) { - testInterceptor(t, tc, intercept) + t.Run(tc.name+" with LSAT header only", func(t *testing.T) { + testInterceptor(t, tc, false, intercept) + }) + t.Run(tc.name+" with LSAT+L402 headers", func(t *testing.T) { + testInterceptor(t, tc, true, intercept) }) } } @@ -314,18 +321,21 @@ func TestStreamInterceptor(t *testing.T) { ) return err } - t.Run(tc.name, func(t *testing.T) { - testInterceptor(t, tc, intercept) + t.Run(tc.name+" with LSAT header only", func(t *testing.T) { + testInterceptor(t, tc, false, intercept) + }) + t.Run(tc.name+" with LSAT+L402 headers", func(t *testing.T) { + testInterceptor(t, tc, true, intercept) }) } } -func testInterceptor(t *testing.T, tc interceptTestCase, +func testInterceptor(t *testing.T, tc interceptTestCase, addL402 bool, intercept func() error) { // Initial condition and simulated backend call. store.token = makeToken(tc.initialPreimage) - tc.resetCb() + tc.resetCb(addL402) numBackendCalls = 0 backendWg.Add(1) overallWg.Add(1) @@ -434,13 +444,19 @@ func serializeMac(mac *macaroon.Macaroon) []byte { return macBytes } -func makeAuthHeader(macBytes []byte) string { +func makeAuthHeaders(macBytes []byte, addL402 bool) []string { // Testnet invoice over 500 sats. invoice := "lntb5u1p0pskpmpp5jzw9xvdast2g5lm5tswq6n64t2epe3f4xav43dyd" + "239qr8h3yllqdqqcqzpgsp5m8sfjqgugthk66q3tr4gsqr5rh740jrq9x4l0" + "kvj5e77nmwqvpnq9qy9qsq72afzu7sfuppzqg3q2pn49hlh66rv7w60h2rua" + "hx857g94s066yzxcjn4yccqc79779sd232v9ewluvu0tmusvht6r99rld8xs" + "k287cpyac79r" - return fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"", + str := fmt.Sprintf("macaroon=\"%s\", invoice=\"%s\"", base64.StdEncoding.EncodeToString(macBytes), invoice) + values := []string{"LSAT " + str} + if addL402 { + values = append(values, "L402 "+str) + } + + return values } diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 9bf4cbd..dbe326b 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -154,7 +154,7 @@ func runHTTPTest(t *testing.T, tc *testCase) { require.Equal(t, "402 Payment Required", resp.Status) authHeader := resp.Header.Get("Www-Authenticate") - require.Contains(t, authHeader, "LSAT") + require.Regexp(t, "(LSAT|L402)", authHeader) _ = resp.Body.Close() // Make sure that if we query an URL that is on the whitelist, we don't @@ -317,10 +317,10 @@ func runGRPCTest(t *testing.T, tc *testCase) { Header: map[string][]string{}, }, "", 0) capturedHeader := captureMetadata.Get("WWW-Authenticate") - require.Len(t, capturedHeader, 1) + require.Len(t, capturedHeader, 2) require.Equal( - t, expectedHeaderContent.Get("WWW-Authenticate"), - capturedHeader[0], + t, expectedHeaderContent.Values("WWW-Authenticate"), + capturedHeader, ) // Make sure that if we query an URL that is on the whitelist, we don't