proxy: LSAT and L402 WWW-Authenticate headers

Old clients expect "L402 macaroon=..." in the first WWW-Authenticate, while
the protocol [1] says it should be "WWW-Authenticate: L402 macaroon=...",
so send both LSAT and L402. LSAT must be sent first, to maintain backward
compatibility with older clients.

[1] https://github.com/lightninglabs/L402/blob/master/protocol-specification.md
This commit is contained in:
Boris Nagaev
2024-04-16 12:44:14 -03:00
parent 728707ba5c
commit 709463fe5b
5 changed files with 95 additions and 55 deletions

View File

@@ -71,6 +71,14 @@ func (l *L402Authenticator) Accept(header *http.Header, serviceName string) bool
return true 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 // FreshChallengeHeader returns a header containing a challenge for the user to
// complete. // complete.
// //
@@ -95,11 +103,19 @@ func (l *L402Authenticator) FreshChallengeHeader(r *http.Request,
log.Errorf("Error serializing L402: %v", err) 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) base64.StdEncoding.EncodeToString(macBytes), paymentRequest)
header := r.Header 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 return header, nil
} }

View File

@@ -35,13 +35,14 @@ func (a MockAuthenticator) FreshChallengeHeader(r *http.Request,
_ string, _ int64) (http.Header, error) { _ string, _ int64) (http.Header, error) {
header := r.Header header := r.Header
header.Set( str := "macaroon=\"AGIAJEemVQUTEyNCR0exk7ek9" +
"WWW-Authenticate", "LSAT macaroon=\"AGIAJEemVQUTEyNCR0exk7ek9"+
"0Cg==\", invoice=\"lnbc1500n1pw5kjhmpp5fu6xhthlt2vucm" + "0Cg==\", invoice=\"lnbc1500n1pw5kjhmpp5fu6xhthlt2vucm" +
"zkx6c7wtlh2r625r30cyjsfqhu8rsx4xpz5lwqdpa2fjkzep6yptk" + "zkx6c7wtlh2r625r30cyjsfqhu8rsx4xpz5lwqdpa2fjkzep6yptk" +
"sct5yp5hxgrrv96hx6twvusycn3qv9jx7ur5d9hkugr5dusx6cqzp" + "sct5yp5hxgrrv96hx6twvusycn3qv9jx7ur5d9hkugr5dusx6cqzp" +
"gxqr23s79ruapxc4j5uskt4htly2salw4drq979d7rcela9wz02el" + "gxqr23s79ruapxc4j5uskt4htly2salw4drq979d7rcela9wz02el" +
"hypmdzmzlnxuknpgfyfm86pntt8vvkvffma5qc9n50h4mvqhngadq" + "hypmdzmzlnxuknpgfyfm86pntt8vvkvffma5qc9n50h4mvqhngadq" +
"y3ngqjcym5a\"") "y3ngqjcym5a\""
header.Set("WWW-Authenticate", lsatAuthScheme+" "+str)
header.Add("WWW-Authenticate", l402AuthScheme+" "+str)
return header, nil return header, nil
} }

View File

@@ -66,7 +66,7 @@ var (
// authHeaderRegex is the regular expression the payment challenge must // authHeaderRegex is the regular expression the payment challenge must
// match for us to be able to parse the macaroon and invoice. // match for us to be able to parse the macaroon and invoice.
authHeaderRegex = regexp.MustCompile( authHeaderRegex = regexp.MustCompile(
"LSAT macaroon=\"(.*?)\", invoice=\"(.*?)\"", "(LSAT|L402) macaroon=\"(.*?)\", invoice=\"(.*?)\"",
) )
// errPaymentFailedTerminally is signaled by the payment tracking method // 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 // First parse the authentication header that was stored in the
// metadata. // metadata.
authHeader := md.Get(AuthHeader) authHeaders := md.Get(AuthHeader)
if len(authHeader) == 0 { if len(authHeaders) == 0 {
return nil, fmt.Errorf("auth header not found in response") return nil, fmt.Errorf("auth header not found in response")
} }
matches := authHeaderRegex.FindStringSubmatch(authHeader[0]) // Find the first WWW-Authenticate header, matching authHeaderRegex.
if len(matches) != 3 { 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 "+ 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 // Decode the base64 macaroon and the invoice so we can store the
// information in our store later. // information in our store later.
macBase64, invoiceStr := matches[1], matches[2] macBase64, invoiceStr := matches[2], matches[3]
macBytes, err := base64.StdEncoding.DecodeString(macBase64) macBytes, err := base64.StdEncoding.DecodeString(macBase64)
if err != nil { if err != nil {
return nil, fmt.Errorf("base64 decode of macaroon failed: "+ return nil, fmt.Errorf("base64 decode of macaroon failed: "+

View File

@@ -23,7 +23,7 @@ type interceptTestCase struct {
name string name string
initialPreimage *lntypes.Preimage initialPreimage *lntypes.Preimage
interceptor *ClientInterceptor interceptor *ClientInterceptor
resetCb func() resetCb func(addL402 bool)
expectLndCall bool expectLndCall bool
expectSecondLndCall bool expectSecondLndCall bool
sendPaymentCb func(*testing.T, test.PaymentChannelMessage) sendPaymentCb func(*testing.T, test.PaymentChannelMessage)
@@ -73,7 +73,7 @@ var (
testMacHex = hex.EncodeToString(testMacBytes) testMacHex = hex.EncodeToString(testMacBytes)
paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5} paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5}
backendErr error backendErr error
backendAuth = "" backendAuth = []string{}
callMD map[string]string callMD map[string]string
numBackendCalls = 0 numBackendCalls = 0
overallWg sync.WaitGroup overallWg sync.WaitGroup
@@ -83,7 +83,9 @@ var (
name: "no auth required happy path", name: "no auth required happy path",
initialPreimage: nil, initialPreimage: nil,
interceptor: interceptor, interceptor: interceptor,
resetCb: func() { resetBackend(nil, "") }, resetCb: func(addL402 bool) {
resetBackend(nil, []string{})
},
expectLndCall: false, expectLndCall: false,
expectToken: false, expectToken: false,
expectBackendCalls: 1, expectBackendCalls: 1,
@@ -93,10 +95,10 @@ var (
name: "auth required, no token yet", name: "auth required, no token yet",
initialPreimage: nil, initialPreimage: nil,
interceptor: interceptor, interceptor: interceptor,
resetCb: func() { resetCb: func(addL402 bool) {
resetBackend( resetBackend(
status.New(GRPCErrCode, GRPCErrMessage).Err(), status.New(GRPCErrCode, GRPCErrMessage).Err(),
makeAuthHeader(testMacBytes), makeAuthHeaders(testMacBytes, addL402),
) )
}, },
expectLndCall: true, expectLndCall: true,
@@ -107,7 +109,7 @@ var (
// The next call to the "backend" shouldn't return an // The next call to the "backend" shouldn't return an
// error. // error.
resetBackend(nil, "") resetBackend(nil, []string{})
msg.Done <- lndclient.PaymentResult{ msg.Done <- lndclient.PaymentResult{
Preimage: paidPreimage, Preimage: paidPreimage,
PaidAmt: 123, PaidAmt: 123,
@@ -127,7 +129,9 @@ var (
name: "auth required, has token", name: "auth required, has token",
initialPreimage: &paidPreimage, initialPreimage: &paidPreimage,
interceptor: interceptor, interceptor: interceptor,
resetCb: func() { resetBackend(nil, "") }, resetCb: func(addL402 bool) {
resetBackend(nil, []string{})
},
expectLndCall: false, expectLndCall: false,
expectToken: true, expectToken: true,
expectBackendCalls: 1, expectBackendCalls: 1,
@@ -137,10 +141,10 @@ var (
name: "auth required, has pending token", name: "auth required, has pending token",
initialPreimage: &zeroPreimage, initialPreimage: &zeroPreimage,
interceptor: interceptor, interceptor: interceptor,
resetCb: func() { resetCb: func(addL402 bool) {
resetBackend( resetBackend(
status.New(GRPCErrCode, GRPCErrMessage).Err(), status.New(GRPCErrCode, GRPCErrMessage).Err(),
makeAuthHeader(testMacBytes), makeAuthHeaders(testMacBytes, addL402),
) )
}, },
expectLndCall: true, expectLndCall: true,
@@ -154,7 +158,7 @@ var (
// The next call to the "backend" shouldn't return an // The next call to the "backend" shouldn't return an
// error. // error.
resetBackend(nil, "") resetBackend(nil, []string{})
msg.Updates <- lndclient.PaymentStatus{ msg.Updates <- lndclient.PaymentStatus{
State: lnrpc.Payment_SUCCEEDED, State: lnrpc.Payment_SUCCEEDED,
Preimage: paidPreimage, Preimage: paidPreimage,
@@ -168,10 +172,10 @@ var (
name: "auth required, has pending but expired token", name: "auth required, has pending but expired token",
initialPreimage: &zeroPreimage, initialPreimage: &zeroPreimage,
interceptor: interceptor, interceptor: interceptor,
resetCb: func() { resetCb: func(addL402 bool) {
resetBackend( resetBackend(
status.New(GRPCErrCode, GRPCErrMessage).Err(), status.New(GRPCErrCode, GRPCErrMessage).Err(),
makeAuthHeader(testMacBytes), makeAuthHeaders(testMacBytes, addL402),
) )
}, },
expectLndCall: true, expectLndCall: true,
@@ -183,7 +187,7 @@ var (
// The next call to the "backend" shouldn't return an // The next call to the "backend" shouldn't return an
// error. // error.
resetBackend(nil, "") resetBackend(nil, []string{})
msg.Done <- lndclient.PaymentResult{ msg.Done <- lndclient.PaymentResult{
Preimage: paidPreimage, Preimage: paidPreimage,
PaidAmt: 123, PaidAmt: 123,
@@ -195,7 +199,7 @@ var (
// The next call to the "backend" shouldn't return an // The next call to the "backend" shouldn't return an
// error. // error.
resetBackend(nil, "") resetBackend(nil, []string{})
msg.Updates <- lndclient.PaymentStatus{ msg.Updates <- lndclient.PaymentStatus{
State: lnrpc.Payment_FAILED, State: lnrpc.Payment_FAILED,
} }
@@ -211,10 +215,10 @@ var (
&lnd.LndServices, store, testTimeout, 100, &lnd.LndServices, store, testTimeout, 100,
DefaultMaxRoutingFeeSats, false, DefaultMaxRoutingFeeSats, false,
), ),
resetCb: func() { resetCb: func(addL402 bool) {
resetBackend( resetBackend(
status.New(GRPCErrCode, GRPCErrMessage).Err(), status.New(GRPCErrCode, GRPCErrMessage).Err(),
makeAuthHeader(testMacBytes), makeAuthHeaders(testMacBytes, addL402),
) )
}, },
expectLndCall: false, expectLndCall: false,
@@ -230,7 +234,7 @@ var (
// resetBackend is used by the test cases to define the behaviour of the // resetBackend is used by the test cases to define the behaviour of the
// simulated backend and reset its starting conditions. // simulated backend and reset its starting conditions.
func resetBackend(expectedErr error, expectedAuth string) { func resetBackend(expectedErr error, expectedAuth []string) {
backendErr = expectedErr backendErr = expectedErr
backendAuth = expectedAuth backendAuth = expectedAuth
callMD = nil callMD = nil
@@ -252,9 +256,9 @@ func invoker(opts []grpc.CallOption) error {
// Should we simulate an auth header response? // Should we simulate an auth header response?
trailer, ok := opt.(grpc.TrailerCallOption) trailer, ok := opt.(grpc.TrailerCallOption)
if ok && backendAuth != "" { if ok && len(backendAuth) != 0 {
trailer.TrailerAddr.Set( trailer.TrailerAddr.Set(
AuthHeader, backendAuth, AuthHeader, backendAuth...,
) )
} }
} }
@@ -284,8 +288,11 @@ func TestUnaryInterceptor(t *testing.T) {
ctx, "", nil, nil, nil, unaryInvoker, nil, ctx, "", nil, nil, nil, unaryInvoker, nil,
) )
} }
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name+" with LSAT header only", func(t *testing.T) {
testInterceptor(t, tc, intercept) 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 return err
} }
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name+" with LSAT header only", func(t *testing.T) {
testInterceptor(t, tc, intercept) 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) { intercept func() error) {
// Initial condition and simulated backend call. // Initial condition and simulated backend call.
store.token = makeToken(tc.initialPreimage) store.token = makeToken(tc.initialPreimage)
tc.resetCb() tc.resetCb(addL402)
numBackendCalls = 0 numBackendCalls = 0
backendWg.Add(1) backendWg.Add(1)
overallWg.Add(1) overallWg.Add(1)
@@ -434,13 +444,19 @@ func serializeMac(mac *macaroon.Macaroon) []byte {
return macBytes return macBytes
} }
func makeAuthHeader(macBytes []byte) string { func makeAuthHeaders(macBytes []byte, addL402 bool) []string {
// Testnet invoice over 500 sats. // Testnet invoice over 500 sats.
invoice := "lntb5u1p0pskpmpp5jzw9xvdast2g5lm5tswq6n64t2epe3f4xav43dyd" + invoice := "lntb5u1p0pskpmpp5jzw9xvdast2g5lm5tswq6n64t2epe3f4xav43dyd" +
"239qr8h3yllqdqqcqzpgsp5m8sfjqgugthk66q3tr4gsqr5rh740jrq9x4l0" + "239qr8h3yllqdqqcqzpgsp5m8sfjqgugthk66q3tr4gsqr5rh740jrq9x4l0" +
"kvj5e77nmwqvpnq9qy9qsq72afzu7sfuppzqg3q2pn49hlh66rv7w60h2rua" + "kvj5e77nmwqvpnq9qy9qsq72afzu7sfuppzqg3q2pn49hlh66rv7w60h2rua" +
"hx857g94s066yzxcjn4yccqc79779sd232v9ewluvu0tmusvht6r99rld8xs" + "hx857g94s066yzxcjn4yccqc79779sd232v9ewluvu0tmusvht6r99rld8xs" +
"k287cpyac79r" "k287cpyac79r"
return fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"", str := fmt.Sprintf("macaroon=\"%s\", invoice=\"%s\"",
base64.StdEncoding.EncodeToString(macBytes), invoice) base64.StdEncoding.EncodeToString(macBytes), invoice)
values := []string{"LSAT " + str}
if addL402 {
values = append(values, "L402 "+str)
}
return values
} }

View File

@@ -154,7 +154,7 @@ func runHTTPTest(t *testing.T, tc *testCase) {
require.Equal(t, "402 Payment Required", resp.Status) require.Equal(t, "402 Payment Required", resp.Status)
authHeader := resp.Header.Get("Www-Authenticate") authHeader := resp.Header.Get("Www-Authenticate")
require.Contains(t, authHeader, "LSAT") require.Regexp(t, "(LSAT|L402)", authHeader)
_ = resp.Body.Close() _ = resp.Body.Close()
// Make sure that if we query an URL that is on the whitelist, we don't // 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{}, Header: map[string][]string{},
}, "", 0) }, "", 0)
capturedHeader := captureMetadata.Get("WWW-Authenticate") capturedHeader := captureMetadata.Get("WWW-Authenticate")
require.Len(t, capturedHeader, 1) require.Len(t, capturedHeader, 2)
require.Equal( require.Equal(
t, expectedHeaderContent.Get("WWW-Authenticate"), t, expectedHeaderContent.Values("WWW-Authenticate"),
capturedHeader[0], capturedHeader,
) )
// Make sure that if we query an URL that is on the whitelist, we don't // Make sure that if we query an URL that is on the whitelist, we don't