refactor: migrate to jwt sessions instead of store sessions

This commit is contained in:
MTG2000
2022-06-09 12:28:34 +03:00
parent d8fd6e1432
commit 435fc7f844
14 changed files with 257 additions and 234 deletions

View File

@@ -27,11 +27,10 @@ function isHashUsed(hash) {
return prisma.generatedK1.findFirst({ where: { value: hash } })
}
function addHash(hash, sid) {
function addHash(hash) {
return prisma.generatedK1.create({
data: {
value: hash,
sid,
}
})
}
@@ -46,7 +45,7 @@ function removeHash(hash) {
function removeExpiredHashes() {
const now = new Date();
const lastHourDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours() - 1, now.getMinutes());
const lastHourDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes() - 10);
return prisma.generatedK1.deleteMany({
where: {
@@ -57,20 +56,21 @@ function removeExpiredHashes() {
})
}
async function generateAuthUrl(sid) {
async function generateAuthUrl() {
const hostname = CONSTS.LNURL_AUTH_HOST;
const secret = await generateSecret()
await addHash(createHash(secret), sid)
const secret = await generateSecret();
const hash = createHash(secret);
await addHash(hash)
const url = `${hostname}?tag=login&k1=${secret}`
return {
url,
encoded: lnurl.encode(url).toUpperCase(),
secret,
secretHash: hash,
}
}
async function getSidByK1(k1) {
const hash = createHash(k1)
async function getAuthTokenByHash(hash) {
const data = await prisma.generatedK1.findFirst({
where: {
value: hash,
@@ -79,6 +79,17 @@ async function getSidByK1(k1) {
return data.sid;
}
function associateTokenToHash(hash, token) {
return prisma.generatedK1.update({
where: {
value: hash
},
data: {
sid: token
}
})
}
async function verifySig(sig, k1, key) {
if (!lnurl.verifyAuthorizationSignature(sig, k1, key)) {
const message = 'Signature verification failed'
@@ -105,141 +116,12 @@ function createHash(data) {
// function setupAuthMiddelwares(app) {
// app.use(session({
// secret: "12345",
// resave: false,
// saveUninitialized: true,
// store: new SQLiteStore()
// }));
// passport.use(
// new lnurlAuth.Strategy(function (linkingPublicKey, done) {
// const user = { id: linkingPublicKey };
// console.log("Strategy Function");
// console.log(user);
// // let user = map.user.get(linkingPublicKey);
// // if (!user) {
// // user = { id: linkingPublicKey };
// // map.user.set(linkingPublicKey, user);
// // }
// done(null, user);
// })
// );
// app.use(passport.initialize());
// app.use(passport.session());
// app.use(passport.authenticate("lnurl-auth"));
// passport.serializeUser(function (user, done) {
// done(null, user.id);
// });
// passport.deserializeUser(function (id, done) {
// // done(null, map.user.get(id) || null);
// done(null, id || null);
// });
// return app;
// /*
// app.get(
// "/do-login",
// function (req, res, next) {
// next();
// },
// async function (req, res) {
// 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).
// let session;
// assert.ok(
// req.query.k1,
// new HttpError('Missing required parameter: "k1"', 400)
// );
// assert.ok(
// req.query.sig,
// new HttpError('Missing required parameter: "sig"', 400)
// );
// assert.ok(
// req.query.key,
// new HttpError('Missing required parameter: "key"', 400)
// );
// session = map.session.get(req.query.k1);
// assert.ok(
// session,
// new HttpError("Secret does not match any known session", 400)
// );
// const { k1, sig, key } = req.query;
// assert.ok(
// verifyAuthorizationSignature(sig, k1, key),
// new HttpError("Invalid signature", 400)
// );
// session.lnurlAuth = session.lnurlAuth || {};
// session.lnurlAuth.linkingPublicKey = req.query.key;
// const result = await session.save();
// console.log(result);
// 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) {
// k1 = req.session.lnurlAuth.k1 = generateSecret(32, "hex");
// map.session.set(k1, req.session);
// }
// const callbackUrl =
// "https://" +
// `${req.get("host")}/do-login?${querystring.stringify({
// k1,
// tag: "login",
// })}`;
// const encoded = lnurl.encode(callbackUrl).toUpperCase();
// const qrCode = await qrcode.toDataURL(encoded);
// return res.json({
// lnurl: encoded,
// qrCode: qrCode,
// });
// }
// );
// */
// // app.get("/logout", function (req, res, next) {
// // if (req.user) {
// // req.session.destroy();
// // return res.redirect("/");
// // }
// // next();
// // });
// // app.get("/me", function (req, res, next) {
// // res.json({ user: req.user ? req.user : null });
// // next();
// // });
// // app.get("/profile", function (req, res, next) {
// // if (!req.user) {
// // return res.redirect("/login");
// // }
// // res.render("profile", { user: req.user });
// // next();
// // });
// }
module.exports = {
generateAuthUrl: generateAuthUrl,
verifySig: verifySig,
removeHash: removeHash,
createHash: createHash,
removeExpiredHashes: removeExpiredHashes,
getSidByK1: getSidByK1
getAuthTokenByHash: getAuthTokenByHash,
associateTokenToHash: associateTokenToHash
}

View File

@@ -1,12 +1,26 @@
const { ApolloServer } = require("apollo-server-lambda");
const schema = require('./schema')
const cookie = require('cookie')
const jose = require('jose');
const { createExpressApp } = require("../../modules");
const { JWT_SECRET } = require("../../utils/consts");
const extractKey = async (cookieHeader) => {
const cookies = cookie.parse(cookieHeader ?? '');
const token = cookies.Authorization;
if (token) {
const { payload } = await jose.jwtVerify(token, Buffer.from(JWT_SECRET), {
algorithms: ['HS256'],
})
return payload.pubKey
}
return null;
}
const server = new ApolloServer({
schema,
context: async ({ event, context, express }) => {
const userPubKey = express.req.user?.id;
context: async ({ event }) => {
const userPubKey = await extractKey(event.headers.cookie ?? event.headers.Cookie)
return { userPubKey }
},
});
@@ -19,11 +33,6 @@ const apolloHandler = server.createHandler({
origin: ['http://localhost:3000', 'https://studio.apollographql.com'],
credentials: true,
}
},
expressAppFromMiddleware(middleware) {
const app = createExpressApp();
app.use(middleware)
return app;
}
});

View File

@@ -0,0 +1,87 @@
const serverless = require('serverless-http');
const { getAuthTokenByHash } = require('../../auth/services/lnurl.service');
const { createExpressApp } = require('../../modules');
const express = require('express');
const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const lnurlService = require('../../auth/services/lnurl.service');
const isLoggedInHandler = async (req, res) => {
// console.log(req.cookies);
try {
const login_session = req.cookies?.login_session;
// console.log(login_session);
if (login_session) {
const { payload } = await jose.jwtVerify(login_session, Buffer.from(JWT_SECRET), {
algorithms: ['HS256'],
});
const hash = payload.hash;
const token = await getAuthTokenByHash(hash);
lnurlService.removeHash(hash).catch();
lnurlService.removeExpiredHashes().catch();
res
.status(200)
.cookie('Authorization', token, {
maxAge: 3600000 * 24 * 30,
secure: true,
httpOnly: true,
})
.clearCookie('login_session', {
secure: true,
httpOnly: true,
})
.json({
logged_in: true
});
// console.log(payload);
} else {
res.json({
me: null
});
}
} catch (error) {
console.log(error);
res.json({
logged_in: false
})
}
// get session token
// check DB to see if this token has an accossiated jwt auth token to it
// if yes:
// set the auth token to cookie
// remove the session token
// remove the data row
}
express.Router().get('id', (req, res) => {
res.clearCookie('Au')
})
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.get('/is-logged-in', isLoggedInHandler);
}
else {
const router = express.Router();
router.get('/is-logged-in', isLoggedInHandler)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -2,18 +2,44 @@
const { prisma } = require('../../prisma');
const LnurlService = require('../../auth/services/lnurl.service')
const serverless = require('serverless-http');
const { getSidByK1 } = require('../../auth/services/lnurl.service');
const { getAuthTokenByHash, createHash, associateTokenToHash } = require('../../auth/services/lnurl.service');
const { sessionsStore, createExpressApp } = require('../../modules');
const express = require('express');
const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const router = express.Router();
router.get('/login', (req, res) => {
res.cookie('login_session', 'value', {
maxAge: 1000 * 60 * 2, // 2 mins
secure: true,
httpOnly: true,
})
})
const loginHandler = async (req, res) => {
const { tag, k1, sig, key } = req.query;
// Generate an auth URL
if (!sig || !key) {
const data = await LnurlService.generateAuthUrl();
const maxAge = 1000 * 60 * 3; //2 mins
const data = await LnurlService.generateAuthUrl(req.sessionID);
return res.status(200).json(data);
const jwt = await new jose.SignJWT({ hash: data.secretHash })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('5min')
.sign(Buffer.from(JWT_SECRET, 'utf-8'))
return res
.status(200)
.cookie('login_session', jwt, {
maxAge,
secure: true,
httpOnly: true,
})
.json(data);
}
else {
if (tag !== 'login')
@@ -41,26 +67,26 @@ const loginHandler = async (req, res) => {
})
}
// calc the hash of k1
const hash = createHash(k1);
// Update the session with the secret
const sid = await getSidByK1(k1);
const d = await new Promise((res, rej) => {
sessionsStore.get(sid, (err, d) => {
if (err) rej(err);
res(d)
})
});
// console.log(d);
await new Promise((res, rej) => {
sessionsStore.set(sid, { ...d, lnurlAuth: { linkingPublicKey: key } }, (err) => {
if (err) rej(err);
res()
})
});
// generate the auth jwt token
const hour = 3600000
const maxAge = 30 * 24 * hour;
const jwt = await new jose.SignJWT({ pubKey: key })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime(maxAge)
//TODO: Set audience, issuer
.sign(Buffer.from(JWT_SECRET, 'utf-8'))
LnurlService.removeHash(LnurlService.createHash(k1)).catch();
LnurlService.removeExpiredHashes().catch();
// associate the auth token with the hash in the db
console.log(hash);
await associateTokenToHash(hash, jwt);
// LnurlService.removeHash(LnurlService.createHash(k1)).catch();
// LnurlService.removeExpiredHashes().catch();
return res.status(200).json({ status: "OK" })

View File

@@ -3,11 +3,15 @@ const { createExpressApp } = require('../../modules');
const express = require('express');
const logoutHandler = (req, res, next) => {
if (req.user) {
req.session.destroy();
return res.redirect("/");
}
next();
res
.clearCookie('Authorization', {
secure: true,
httpOnly: true,
})
.redirect("/")
.end()
};
let app;

View File

@@ -1,52 +1,19 @@
const express = require('express');
const session = require("express-session");
const passport = require("passport");
const lnurlAuth = require("passport-lnurl-auth");
var cors = require('cors');
const { SESSION_SECRET } = require('../utils/consts');
const createGlobalModule = require('../utils/createGlobalModule');
const sessionsStore = require('./sessions-store');
const cookieParser = require('cookie-parser');
const createExpressApp = (router) => {
const app = express();
const routerBasePath = process.env.LOCAL ? `/dev` : `/.netlify/functions`
app.use(cookieParser());
app.use(cors({
origin: ['http://localhost:3000', 'https://studio.apollographql.com'],
credentials: true,
}))
app.use(session({
secret: SESSION_SECRET,
resave: false,
store: sessionsStore,
saveUninitialized: true,
cookie: {
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
}
}));
passport.use(new lnurlAuth.Strategy(function (linkingPublicKey, done) {
// The user has successfully authenticated using lnurl-auth.
// The linked public key is provided here.
// You can use this as a unique reference for the user similar to a username or email address.
const user = { id: linkingPublicKey };
done(null, user);
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(passport.authenticate("lnurl-auth"));
passport.serializeUser(function (user, done) {
done(null, user?.id);
});
passport.deserializeUser(function (id, done) {
done(null, { id } || null);
});
if (router)
app.use(routerBasePath, router);

View File

@@ -1,8 +1,6 @@
const createExpressApp = require("./express-app");
const sessionsStore = require("./sessions-store");
module.exports = {
createExpressApp,
sessionsStore,
}

View File

@@ -1,19 +0,0 @@
const createGlobalModule = require("../utils/createGlobalModule");
let sessionsStore;
const createSessionStore = () => {
const session = require("express-session");
var Store = require('connect-pg-simple')(session);
console.log("New Sessions Store");
return new Store({
createTableIfMissing: true,
tableName: "user_sessions",
})
}
sessionsStore = createGlobalModule('sessions-store', createSessionStore);
module.exports = sessionsStore;

6
api/utils/generateId.js Normal file
View File

@@ -0,0 +1,6 @@
const crypto = require('crypto');
const generateId = () => crypto.randomUUID({});
module.exports = generateId;

37
package-lock.json generated
View File

@@ -35,6 +35,7 @@
"connect-pg-simple": "^7.0.0",
"connect-sqlite3": "^0.9.13",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dayjs": "^1.11.1",
@@ -19814,6 +19815,26 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"dependencies": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -82812,6 +82833,22 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
},
"cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"requires": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"dependencies": {
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
}
}
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",

View File

@@ -30,6 +30,7 @@
"connect-pg-simple": "^7.0.0",
"connect-sqlite3": "^0.9.13",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dayjs": "^1.11.1",

View File

@@ -27,18 +27,18 @@ functions:
login:
handler: api/functions/login/login.handler
events:
- http:
path: login
method: post
- http:
path: login
method: get
logout:
handler: api/functions/logout/logout.handler
events:
- http:
path: logout
method: post
- http:
path: logout
method: get
is-logged-in:
handler: api/functions/is-logged-in/is-logged-in.handler
events:
- http:
path: is-logged-in
method: get

View File

@@ -9,6 +9,7 @@ import LoadingPage from "./Components/LoadingPage/LoadingPage";
import { useMeQuery } from "./graphql";
import { setUser } from "./redux/features/user.slice";
import ProtectedRoute from "./Components/ProtectedRoute/ProtectedRoute";
import { Helmet } from "react-helmet";
// Pages
const FeedPage = React.lazy(() => import("./features/Posts/pages/FeedPage/FeedPage"))
@@ -62,6 +63,19 @@ function App() {
return <div id="app" className='w-full'>
<Helmet>
<title >makers.bolt.fun</title>
<meta
name="description"
content="A lightning app directory made for and by the bitcoin community."
/>
<meta
property="og:title"
content="makers.bolt.fun"
/>
</Helmet>
<Navbar />
<Suspense fallback={<LoadingPage />}>
<Routes>

View File

@@ -57,7 +57,18 @@ export default function LoginPage() {
}, [])
const startPolling = () => {
meQuery.startPolling(3000)
// meQuery.startPolling(3000)
const interval = setInterval(() => {
fetch(CONSTS.apiEndpoint + '/is-logged-in', {
credentials: 'include'
}).then(data => data.json())
.then(data => {
if (data.logged_in) {
clearInterval(interval)
meQuery.refetch();
}
})
}, 2000)
}