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
}
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
}

View File

@@ -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"+
str := "macaroon=\"AGIAJEemVQUTEyNCR0exk7ek9" +
"0Cg==\", invoice=\"lnbc1500n1pw5kjhmpp5fu6xhthlt2vucm" +
"zkx6c7wtlh2r625r30cyjsfqhu8rsx4xpz5lwqdpa2fjkzep6yptk" +
"sct5yp5hxgrrv96hx6twvusycn3qv9jx7ur5d9hkugr5dusx6cqzp" +
"gxqr23s79ruapxc4j5uskt4htly2salw4drq979d7rcela9wz02el" +
"hypmdzmzlnxuknpgfyfm86pntt8vvkvffma5qc9n50h4mvqhngadq" +
"y3ngqjcym5a\"")
"y3ngqjcym5a\""
header.Set("WWW-Authenticate", lsatAuthScheme+" "+str)
header.Add("WWW-Authenticate", l402AuthScheme+" "+str)
return header, nil
}

View File

@@ -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: "+

View File

@@ -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,7 +73,7 @@ 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
@@ -83,7 +83,9 @@ var (
name: "no auth required happy path",
initialPreimage: nil,
interceptor: interceptor,
resetCb: func() { resetBackend(nil, "") },
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,
@@ -127,7 +129,9 @@ var (
name: "auth required, has token",
initialPreimage: &paidPreimage,
interceptor: interceptor,
resetCb: func() { resetBackend(nil, "") },
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
}

View File

@@ -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