login working

This commit is contained in:
counterweight 2025-02-17 01:18:43 +01:00
parent 48b12b71c2
commit a35fa7d0dc
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
9 changed files with 333 additions and 7 deletions

View file

@ -26,9 +26,17 @@ class ExpiredError extends Error {
} }
} }
class ForbiddenError extends Error {
constructor(message) {
super(message);
this.name = 'ForbiddenError';
}
}
module.exports = { module.exports = {
AlreadyUsedError, AlreadyUsedError,
InvalidSignatureError, InvalidSignatureError,
NotFoundError, NotFoundError,
ExpiredError, ExpiredError,
ForbiddenError: ForbiddenError,
}; };

View file

@ -4,8 +4,10 @@ async function attachPublicKeyMiddleware(req, res, next) {
const publicKey = await sessionService.getPublicKeyRelatedToSession( const publicKey = await sessionService.getPublicKeyRelatedToSession(
req.cookies.sessionUuid req.cookies.sessionUuid
); );
req.cookies.publicKey = publicKey;
if (publicKey) {
req.cookies.publicKey = publicKey;
}
next(); next();
} }

View file

@ -0,0 +1,31 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const LoginChallengeCompleted = sequelize.define(
'LoginChallengeCompleted',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'login_challenge_completed',
}
);
module.exports = LoginChallengeCompleted;

View file

@ -0,0 +1,27 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const LoginChallengeCreated = sequelize.define(
'LoginChallengeCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'login_challenge_created',
}
);
module.exports = LoginChallengeCreated;

View file

@ -0,0 +1,72 @@
window.onload = function () {
if (!window.nostr) {
console.log('Nostr extension not present');
document.querySelector('#login-button').disabled = true;
document.querySelector('#no-extension-nudges').style.display = 'block';
} else {
console.log('Nostr extension present');
}
};
async function login() {
let challengeResponse;
try {
challengeResponse = await fetch('/api/login/nostr-challenge', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
console.log(`Something went wrong: ${error}`);
return;
}
const { challenge } = await challengeResponse.json();
let pubkey;
try {
pubkey = await window.nostr.getPublicKey();
} catch (error) {
document.querySelector('#rejected-nostr-nudges').style.display = 'block';
return;
}
const event = {
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
tags: [['challenge', challenge]],
content: 'Sign this challenge to authenticate',
pubkey: pubkey,
};
let signedEvent;
try {
signedEvent = await window.nostr.signEvent(event);
} catch (error) {
document.querySelector('#rejected-nostr-nudges').style.display = 'block';
return;
}
let verifyResponse;
try {
verifyResponse = await fetch('/api/login/nostr-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedEvent),
});
} catch (error) {
console.log(`Something went wrong: ${error}`);
return;
}
if (verifyResponse.status === 403) {
document.querySelector('#rejected-public-key').style.display = 'block';
}
if (verifyResponse.ok) {
document.querySelector('#sign-up-success').style.display = 'block';
setTimeout(() => {
window.location.href = '/home';
}, 1000);
}
}

View file

@ -2,6 +2,7 @@ const express = require('express');
const invitesService = require('../services/invitesService'); const invitesService = require('../services/invitesService');
const nostrService = require('../services/nostrService'); const nostrService = require('../services/nostrService');
const loginService = require('../services/loginService');
const sessionService = require('../services/sessionService'); const sessionService = require('../services/sessionService');
const profileService = require('../services/profileService'); const profileService = require('../services/profileService');
const errors = require('../errors'); const errors = require('../errors');
@ -89,6 +90,84 @@ router.post('/signup/nostr-verify', async (req, res) => {
return res.status(200).json({ success: true }); return res.status(200).json({ success: true });
}); });
router.get('/login/nostr-challenge', async (req, res) => {
let loginChallenge;
try {
loginChallenge = await loginService.createLoginChallenge();
} catch (error) {
return res.status(500).json({
success: false,
message: 'Unexpected error.',
});
}
let relatedNostrChallenge;
try {
relatedNostrChallenge = await nostrService.getNostrChallenge(
loginChallenge.nostr_challenge_uuid
);
} catch (error) {
return res.status(500).json({
success: false,
message: `Unexpected error: ${error}`,
});
}
return res.status(200).json({ challenge: relatedNostrChallenge.challenge });
});
router.post('/login/nostr-verify', async (req, res) => {
const signedEvent = req.body;
const sessionUuid = req.cookies.sessionUuid;
let completedLoginChallenge;
try {
completedLoginChallenge =
await loginService.verifyLoginChallenge(signedEvent);
} catch (error) {
console.log('helo5');
console.log(error);
if (error instanceof errors.ExpiredError) {
return res.status(410).json({
success: false,
message: 'The challenge has expired, request a new one.',
});
}
if (error instanceof errors.AlreadyUsedError) {
return res.status(410).json({
success: false,
message: 'The challenge has been used, request a new one.',
});
}
if (error instanceof errors.InvalidSignatureError) {
return res.status(400).json({
success: false,
message: 'The challenge signature is not valid.',
});
}
if (error instanceof errors.ForbiddenError) {
console.log('helo?1');
return res.status(403).json({
success: false,
message: 'This public key is not authorized.',
});
}
return res.status(500).json({
success: false,
message: 'Unexpected error.',
});
}
console.log('helo?2');
console.log(completedLoginChallenge);
await sessionService.relateSessionToPublicKey(
sessionUuid,
completedLoginChallenge.public_key
);
return res.status(200).json({ success: true });
});
router.post( router.post(
'/set-contact-details', '/set-contact-details',
rejectIfNotAuthorizedMiddleware, rejectIfNotAuthorizedMiddleware,

View file

@ -0,0 +1,58 @@
const uuid = require('uuid');
const nostrService = require('./nostrService');
const invitesService = require('./invitesService');
const LoginChallengeCreated = require('../models/LoginChallengeCreated');
const LoginChallengeCompleted = require('../models/LoginChallengeCompleted');
const errors = require('../errors');
async function createLoginChallenge() {
const nostrChallenge = await nostrService.createNostrChallenge();
return await LoginChallengeCreated.create({
uuid: uuid.v7(),
nostr_challenge_uuid: nostrChallenge.uuid,
created_at: new Date().toISOString(),
});
}
async function verifyLoginChallenge(signedEvent) {
const challengeTag = signedEvent.tags.find((tag) => tag[0] === 'challenge');
const challenge = challengeTag[1];
if (await nostrService.hasNostrChallengeBeenCompleted(challenge)) {
throw new errors.AlreadyUsedError('This challenge has already been used.');
}
const completedNostrChallenge =
await nostrService.verifyNostrChallenge(signedEvent);
if (
!(await invitesService.isPublicKeySignedUp(
completedNostrChallenge.public_key
))
) {
console.log('helo4');
throw new errors.ForbiddenError(
`Public key ${completedNostrChallenge.public_key} is not authorized.`
);
}
const completedLoginChallenge = await LoginChallengeCompleted.create({
uuid: uuid.v7(),
nostr_challenge_completed_uuid: completedNostrChallenge.uuid,
public_key: completedNostrChallenge.public_key,
created_at: new Date().toISOString(),
});
console.log('helo3');
console.log(completedLoginChallenge);
return completedLoginChallenge;
}
module.exports = {
createLoginChallenge,
verifyLoginChallenge,
};

View file

@ -76,9 +76,13 @@ async function getPublicKeyRelatedToSession(sessionUuid) {
}, },
}); });
if (sessionRelatedToPublickey) {
return sessionRelatedToPublickey.public_key; return sessionRelatedToPublickey.public_key;
} }
return null;
}
module.exports = { module.exports = {
createSession, createSession,
isSessionValid, isSessionValid,

View file

@ -12,12 +12,57 @@
<h1>Bienvenido a la seca</h1> <h1>Bienvenido a la seca</h1>
<p>Usa Nostr para logearte</p> <p>Usa Nostr para logearte</p>
<form onsubmit="login();return false"> <form onsubmit="login();return false">
<button type="submit">Login con extensión de Nostr</button> <button id="login-button" type="submit">Login con extensión de Nostr</button>
</form> </form>
<div id="sign-up-success" style="display: none">
<p>¡Bien! Tu clave es parte de la seca.</p>
<p>Redirigiendo a la app...</p>
</div>
<div id="rejected-public-key" style="display: none">
<p> <p>
¿No tienes cuenta de Nostr? Ups, esa clave no está registrada en la seca. ¿Quizás estás usando un perfil equivocado?
<a href="https://start.njump.me/" target="_blank" rel="noopener noreferrer">Crea una gratis</a>.
</p> </p>
</div>
<div id="rejected-nostr-nudges" style="display: none">
<p>
Ups, parece que no has aceptado que usemos tus claves. Si te has
equivocado, puedes intentarlo de nuevo.
</p>
</div>
<div id="no-extension-nudges" style="display: none">
<p>
¡Atención! No se ha encontrado una extensión de Nostr en tu navegador.
Puedes usar:
</p>
<ul>
<li>
Firefox
<ul>
<li>
<a href="https://addons.mozilla.org/en-US/firefox/addon/alby/" target="_blank"
rel="noopener noreferrer">Alby</a>
</li>
<li>
<a href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/" target="_blank"
rel="noopener noreferrer">nos2x-fox</a>
</li>
</ul>
</li>
<li>
Chrome
<ul>
<li>
<a href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
target="_blank" rel="noopener noreferrer">Alby</a>
</li>
<li>
<a href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp" target="_blank"
rel="noopener noreferrer">nos2x</a>
</li>
</ul>
</li>
</ul>
</div>
</body> </body>
</html> </html>