passport-lnurl-auth icon indicating copy to clipboard operation
passport-lnurl-auth copied to clipboard

Does not support JWT or other serverless

Open martindmtrv opened this issue 2 years ago • 10 comments

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 avatar Mar 13 '22 22:03 martindmtrv

Hi! 👋

Firstly, thanks for your work on this project! 🙂

Today I used patch-package to patch [email protected] 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.

martindmtrv avatar Mar 13 '22 23:03 martindmtrv

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

Jared-Dahlke avatar Feb 26 '23 21:02 Jared-Dahlke

@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

martindmtrv avatar Feb 26 '23 22:02 martindmtrv

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

Jared-Dahlke avatar Feb 26 '23 22:02 Jared-Dahlke

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

martindmtrv avatar Feb 26 '23 22:02 martindmtrv

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

Jared-Dahlke avatar Feb 26 '23 22:02 Jared-Dahlke

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 avatar Feb 26 '23 22:02 Jared-Dahlke

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 avatar Feb 27 '23 15:02 Jared-Dahlke

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

Jared-Dahlke avatar Mar 01 '23 02:03 Jared-Dahlke

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.

chill117 avatar Apr 19 '24 15:04 chill117