diff --git a/auth/lnd_authenticator.go b/auth/authenticator.go similarity index 66% rename from auth/lnd_authenticator.go rename to auth/authenticator.go index e85e436..ec51780 100644 --- a/auth/lnd_authenticator.go +++ b/auth/authenticator.go @@ -1,7 +1,6 @@ package auth import ( - "context" "encoding/base64" "encoding/hex" "fmt" @@ -9,8 +8,6 @@ import ( "regexp" "github.com/lightninglabs/kirin/macaroons" - "github.com/lightninglabs/loop/lndclient" - "github.com/lightningnetwork/lnd/lnrpc" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery/checkers" ) @@ -20,31 +17,27 @@ var ( opWildcard = "*" ) -type LndAuthenticator struct { - client lnrpc.LightningClient +// LsatAuthenticator is an authenticator that uses the LSAT protocol to +// authenticate requests. +type LsatAuthenticator struct { + challenger Challenger macService *macaroons.Service } -// A compile time flag to ensure the LndAuthenticator satisfies the +// A compile time flag to ensure the LsatAuthenticator satisfies the // Authenticator interface. -var _ Authenticator = (*LndAuthenticator)(nil) +var _ Authenticator = (*LsatAuthenticator)(nil) -// NewLndAuthenticator creates a new authenticator that is connected to an lnd -// backend and can create new invoices if required. -func NewLndAuthenticator(cfg *Config) (*LndAuthenticator, error) { - client, err := lndclient.NewBasicClient( - cfg.LndHost, cfg.TlsPath, cfg.MacDir, cfg.Network, - ) - if err != nil { - return nil, err - } +// NewLsatAuthenticator creates a new authenticator that authenticates requests +// based on LSAT tokens. +func NewLsatAuthenticator(challenger Challenger) (*LsatAuthenticator, error) { macService, err := macaroons.NewService() if err != nil { return nil, err } - return &LndAuthenticator{ - client: client, + return &LsatAuthenticator{ + challenger: challenger, macService: macService, }, nil } @@ -53,7 +46,7 @@ func NewLndAuthenticator(cfg *Config) (*LndAuthenticator, error) { // to a given backend service. // // NOTE: This is part of the Authenticator interface. -func (l *LndAuthenticator) Accept(header *http.Header) bool { +func (l *LsatAuthenticator) Accept(header *http.Header) bool { authHeader := header.Get("Authorization") log.Debugf("Trying to authorize with header value [%s].", authHeader) if authHeader == "" { @@ -103,29 +96,23 @@ func (l *LndAuthenticator) Accept(header *http.Header) bool { // complete. // // NOTE: This is part of the Authenticator interface. -func (l *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( +func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) ( http.Header, error) { - // Obtain a new invoice from lnd first. We need to know the payment hash - // so we can add it as a caveat to the macaroon. - ctx := context.Background() - invoice := &lnrpc.Invoice{ - Memo: "LSAT", - Value: 1, - } - response, err := l.client.AddInvoice(ctx, invoice) + paymentRequest, paymentHash, err := l.challenger.NewChallenge() if err != nil { - log.Errorf("Error adding invoice: %v", err) + log.Errorf("Error creating new challenge: %v", err) return nil, err } - paymentHashHex := hex.EncodeToString(response.RHash) // Create a new macaroon and add the payment hash as a caveat. // The bakery requires at least one operation so we add an "allow all" // permission set for now. mac, err := l.macService.NewMacaroon( []bakery.Op{{Entity: opWildcard, Action: opWildcard}}, []string{ - checkers.Condition(macaroons.CondRHash, paymentHashHex), + checkers.Condition( + macaroons.CondRHash, paymentHash.String(), + ), }, ) if err != nil { @@ -135,8 +122,7 @@ func (l *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( str := "LSAT macaroon='%s' invoice='%s'" str = fmt.Sprintf( - str, base64.StdEncoding.EncodeToString(mac), - response.GetPaymentRequest(), + str, base64.StdEncoding.EncodeToString(mac), paymentRequest, ) header := r.Header header.Set("WWW-Authenticate", str) diff --git a/auth/config.go b/auth/config.go index 66aeb5e..a4eb713 100644 --- a/auth/config.go +++ b/auth/config.go @@ -8,17 +8,6 @@ import ( "github.com/lightninglabs/kirin/freebie" ) -type Config struct { - // LndHost is the hostname of the LND instance to connect to. - LndHost string `long:"lndhost" description:"Hostname of the LND instance to connect to"` - - TlsPath string `long:"tlspath"` - - MacDir string `long:"macdir"` - - Network string `long:"network"` -} - type Level string func (l Level) lower() string { diff --git a/auth/interface.go b/auth/interface.go index 5954fd0..00fa8ad 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -1,15 +1,26 @@ package auth -import "net/http" +import ( + "net/http" + + "github.com/lightningnetwork/lnd/lntypes" +) // Authenticator is the generic interface for validating client headers and // returning new challenge headers. type Authenticator interface { - // Accept returns whether or not the header successfully authenticates the user - // to a given backend service. + // Accept returns whether or not the header successfully authenticates + // the user to a given backend service. Accept(*http.Header) bool - // FreshChallengeHeader returns a header containing a challenge for the user to - // complete. + // FreshChallengeHeader returns a header containing a challenge for the + // user to complete. FreshChallengeHeader(r *http.Request) (http.Header, error) } + +// Challenger is an interface for generating new payment challenges. +type Challenger interface { + // NewChallenge creates a new LSAT payment challenge, returning a + // payment request (invoice) and the corresponding payment hash. + NewChallenge() (string, lntypes.Hash, error) +} diff --git a/challenger.go b/challenger.go new file mode 100644 index 0000000..4c79a62 --- /dev/null +++ b/challenger.go @@ -0,0 +1,74 @@ +package kirin + +import ( + "context" + "fmt" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/loop/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntypes" +) + +// InvoiceRequestGenerator is a function type that returns a new request for the +// lnrpc.AddInvoice call. +type InvoiceRequestGenerator func() (*lnrpc.Invoice, error) + +// LndChallenger is a challenger that uses an lnd backend to create new LSAT +// payment challenges. +type LndChallenger struct { + client lnrpc.LightningClient + genInvoiceReq InvoiceRequestGenerator +} + +// A compile time flag to ensure the LndChallenger satisfies the +// Challenger interface. +var _ auth.Challenger = (*LndChallenger)(nil) + +// NewLndChallenger creates a new challenger that uses the given connection +// details to connect to an lnd backend to create payment challenges. +func NewLndChallenger(cfg *authConfig, genInvoiceReq InvoiceRequestGenerator) ( + auth.Challenger, error) { + + if genInvoiceReq == nil { + return nil, fmt.Errorf("genInvoiceReq cannot be nil") + } + + client, err := lndclient.NewBasicClient( + cfg.LndHost, cfg.TlsPath, cfg.MacDir, cfg.Network, + ) + if err != nil { + return nil, err + } + return &LndChallenger{ + client: client, + genInvoiceReq: genInvoiceReq, + }, nil +} + +// NewChallenge creates a new LSAT payment challenge, returning a payment +// request (invoice) and the corresponding payment hash. +// +// NOTE: This is part of the Challenger interface. +func (l *LndChallenger) NewChallenge() (string, lntypes.Hash, error) { + // Obtain a new invoice from lnd first. We need to know the payment hash + // so we can add it as a caveat to the macaroon. + invoice, err := l.genInvoiceReq() + if err != nil { + log.Errorf("Error generating invoice request: %v", err) + return "", lntypes.ZeroHash, err + } + ctx := context.Background() + response, err := l.client.AddInvoice(ctx, invoice) + if err != nil { + log.Errorf("Error adding invoice: %v", err) + return "", lntypes.ZeroHash, err + } + paymentHash, err := lntypes.MakeHash(response.RHash) + if err != nil { + log.Errorf("Error parsing payment hash: %v", err) + return "", lntypes.ZeroHash, err + } + + return response.PaymentRequest, paymentHash, nil +} diff --git a/config.go b/config.go index a743233..368b57f 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,6 @@ package kirin import ( "github.com/btcsuite/btcutil" - "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" ) @@ -17,6 +16,17 @@ var ( defaultMaxLogFileSize = 10 ) +type authConfig struct { + // LndHost is the hostname of the LND instance to connect to. + LndHost string `long:"lndhost" description:"Hostname of the LND instance to connect to"` + + TlsPath string `long:"tlspath"` + + MacDir string `long:"macdir"` + + Network string `long:"network"` +} + type config struct { // ListenAddr is the listening address that we should use to allow Kirin // to listen for requests. @@ -26,7 +36,7 @@ type config struct { // is located. StaticRoot string `long:"staticroot" description:"The folder where the static content is located."` - Authenticator *auth.Config `long:"authenticator" description:"Configuration for the authenticator."` + Authenticator *authConfig `long:"authenticator" description:"Configuration for the authenticator."` // Services is a list of JSON objects in string format, which specify // each backend service to Kirin. diff --git a/kirin.go b/kirin.go index 3761bf3..47e396c 100644 --- a/kirin.go +++ b/kirin.go @@ -10,6 +10,7 @@ import ( "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/lnrpc" "gopkg.in/yaml.v2" ) @@ -37,17 +38,14 @@ func start() error { return fmt.Errorf("unable to set up logging: %v", err) } - // Create the auxiliary services the proxy needs to work. - authenticator, err := auth.NewLndAuthenticator(cfg.Authenticator) - if err != nil { - return err - } - servicesProxy, err := proxy.New( - authenticator, cfg.Services, cfg.StaticRoot, - ) - if err != nil { - return err + // Create the proxy and connect it to lnd. + genInvoiceReq := func() (*lnrpc.Invoice, error) { + return &lnrpc.Invoice{ + Memo: "LSAT", + Value: 1, + }, nil } + servicesProxy, err := createProxy(cfg, genInvoiceReq) server := &http.Server{ Addr: cfg.ListenAddr, Handler: http.HandlerFunc(servicesProxy.ServeHTTP), @@ -103,6 +101,23 @@ func setupLogging(cfg *config) error { return build.ParseAndSetDebugLevels(cfg.DebugLevel, logWriter) } +// createProxy creates the proxy with all the services it needs. +func createProxy(cfg *config, genInvoiceReq InvoiceRequestGenerator) ( + *proxy.Proxy, error) { + + challenger, err := NewLndChallenger( + cfg.Authenticator, genInvoiceReq, + ) + if err != nil { + return nil, err + } + authenticator, err := auth.NewLsatAuthenticator(challenger) + if err != nil { + return nil, err + } + return proxy.New(authenticator, cfg.Services, cfg.StaticRoot) +} + // cleanup closes the given server and shuts down the log rotator. func cleanup(server *http.Server) { err := server.Close()