mirror of
https://github.com/lightninglabs/aperture.git
synced 2025-12-17 09:04:19 +01:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: "+
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user