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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: "+
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user