chill117 / passport-lnurl-auth

Passport strategy that uses lnurl-auth
MIT License
42 stars 11 forks source link

Does not support JWT or other serverless #7

Open martindmtrv opened 2 years ago

martindmtrv commented 2 years ago

Probably not really a bug persay rather a lacking feature. Just putting here for visibility so nobody else tries to get it working with JWTs / serverless; need some kinda session store (since the library tries to save session after the request is done)

Still playing around with it now will see if there's something I can add in to workaround this

martindmtrv commented 2 years ago

Hi! 👋

Firstly, thanks for your work on this project! 🙂

Today I used patch-package to patch passport-lnurl-auth@1.4.2 for the project I'm working on.

I added a small patch to support serverless applications. Essentially just adding in functions the user can specify to use a DB to verify the login attempts. Specify the flag 'serverless' to be true and specify two async functions recordLoginSuccess and checkLoginSuccess.

When the lightning wallet scans the QR code if it is a successful login attempt; the recordLoginSuccess() function is called allowing this attempt to be stored into a DB and checked later.

When the site refreshes it calls checkLoginSuccess() to see if any login attempt was successful and will returns the node key of the successful login (again user defined DB operations).

Here is the diff that solved my problem:

diff --git a/node_modules/passport-lnurl-auth/lib/middleware.js b/node_modules/passport-lnurl-auth/lib/middleware.js
index abe7f04..4c6519b 100644
--- a/node_modules/passport-lnurl-auth/lib/middleware.js
+++ b/node_modules/passport-lnurl-auth/lib/middleware.js
@@ -34,6 +34,10 @@ const Middleware = function(options) {
        // The URI schema prefix used before the encoded LNURL.
        // e.g. "lightning:" or "LIGHTNING:" or "" (empty-string)
        uriSchemaPrefix: 'LIGHTNING:',
+       
+       serverless: false,
+       recordLoginSuccess: (k1, lnurlAuth) => {},
+       checkLoginSuccess: async (k1) => { return "" },
    });
    options.qrcode = _.defaults(options.qrcode || {}, {
        errorCorrectionLevel: 'L',
@@ -43,7 +47,7 @@ const Middleware = function(options) {
    if (!options.callbackUrl) {
        throw new Error('Missing required middleware option: "callbackUrl"');
    }
-   return function(req, res, next) {
+   return async function(req, res, next) {
        if (req.query.k1 || req.query.key || req.query.sig) {
            // Check signature against provided linking public key.
            // This request could originate from a mobile app (ie. not their browser).
@@ -59,15 +63,19 @@ const Middleware = function(options) {
                    throw new HttpError('Missing required parameter: "key"', 400);
                }
                session = map.session.get(req.query.k1);
-               if (!session) {
+               if (!options.serverless && !session) {
                    throw new HttpError('Secret does not match any known session', 400);
                }
                const { k1, sig, key } = req.query;
                if (!verifyAuthorizationSignature(sig, k1, key)) {
                    throw new HttpError('Invalid signature', 400);
                }
-               session.lnurlAuth = session.lnurlAuth || {};
-               session.lnurlAuth.linkingPublicKey = req.query.key;
+
+               if (!options.serverless) {
+                   session.lnurlAuth = session.lnurlAuth || {};
+                   session.lnurlAuth.linkingPublicKey = req.query.key;
+               }
+               
            } catch (error) {
                if (!error.status) {
                    debug.error(error);
@@ -80,6 +88,14 @@ const Middleware = function(options) {
                });
            }
            // Signature check passed.
+
+           if (options.serverless) {
+               // call this func should be some db operation on your end
+               await options.recordLoginSuccess(req.query.k1, req.query.key);
+               res.status(200).json({ status: 'OK' });
+               return;
+           }
+
            return session.save(function(error) {
                if (error) {
                    debug.error(error);
@@ -91,13 +107,31 @@ const Middleware = function(options) {
                res.status(200).json({ status: 'OK' });
            });
        }
+
        req.session = req.session || {};
        req.session.lnurlAuth = req.session.lnurlAuth || {};
        let k1 = req.session.lnurlAuth.k1 || null;
-       if (!k1) {
+
+       if (k1) {
+           let pubKey = await options.checkLoginSuccess(k1);
+           // we had a successful login attempt
+           if (pubKey != "") {
+               req.session.lnurlAuth.linkingPublicKey = pubKey;
+               await req.session.save();
+               res.redirect("/");
+               return;
+           }
+
+       } else  {
            k1 = req.session.lnurlAuth.k1 = generateSecret(32, 'hex');
            map.session.set(k1, req.session);
        }
+
+       // save current config into session (we do this now to make sure its sent to client "stateless")
+       if (options.serverless) {
+           await req.session.save();
+       }
+       
        // Show login page.
        return getLoginPageHtml(k1, options).then(html => {
            res.set({

This issue body was partially generated by patch-package.

Jared-Dahlke commented 1 year ago

@martindmtrv i'm trying to make this work in Next.js in pages/api. You didn't ever get this working by chance did you?

martindmtrv commented 1 year ago

@Jared-Dahlke

Yeah I was able to get it working with this patch applied. To give a sense of the newly added parameters here

  new LnurlAuth.Middleware({
    callbackUrl: ...,
    cancelUrl: ...,
    // setting this to true allows us to define the callbacks below
    serverless: true,
    // this method is called after signature verification successful by a wallet app
    recordLoginSuccess: async (k1: string, pubkey: string) => {
      // connect to your DB and store the k1 string and pubkey 
      await dbConnect();
      const attempt = new LoginAttempt({ k1, nodePubKey: pubkey });
      await attempt.save();
    },
    // this method gets called by the client login page polls to check if the login was successful
    checkLoginSuccess: async (k1: string) => {
      await dbConnect();
      const attempt = await LoginAttempt.findOne({ k1 });

      // existence check, if there is no entry in your DB then we did not succesfully login
      if (attempt) {
        const pubKey = attempt.nodePubKey;
        await attempt.delete();
        return pubKey;
      }

      return "";
    },
  })(req, res);

In essense the new callbacks allow you to define some DB operation to store the login information which gets accessed later, since by default this library holds these values in memory. Not the cleanest solution but it does get the job done

Jared-Dahlke commented 1 year ago

@martindmtrv , anyway you could post a Next.js repo showing how it's done? I think the internet would benefit greatly from having a template of how to do lightning login in Next.js standalone (pages/api not a custom server), for the frontend devs who are working in the 21st century.

I'm getting req.session.save is not a function when i try it.

martindmtrv commented 1 year ago

Likely you are missing iron-session with your nextjs setup. You need somehow to store session data for req.session.save() to work. As for writing up a repo, not sure I should do it with this given that it is a bit of a hack and not a great role model solution. I believe I have seen some lightning login templates before like this one: https://github.com/reneaaron/lapp-template

Jared-Dahlke commented 1 year ago

yeah i tried with iron-session. I've seen that lnapp-template , it uses a separate server so is not applicable.

Jared-Dahlke commented 1 year ago

we should try to get somebody to put up some sats to get a Next.js lnurl-auth template built. I think it would be a huge win for lightning development. I'm just a little too left curve to get it working. Could be added to https://github.com/vercel/next.js/tree/canary/examples

Jared-Dahlke commented 1 year ago

I just wanted to also point out that, to get this to work in Next.js, I've had to change res.set to this in middleware.js: image

Jared-Dahlke commented 1 year ago

fyi we figured out how to do this without using a custom server using NextAuth thanks to @reneaaron

https://github.com/Jared-Dahlke/nextauth-lnurl-template

chill117 commented 4 months ago

The latest release (v1.6.0) has a new option ("store") which allows this module to be used in a "serverless" environment. The previous default behavior is unchanged - an in-memory store is used to get/save session object reference using the k1 value. It is now possible to provide your own store get/save functions with whatever database you prefer. See lib/stores/memory.js for implementation details.