This commit is contained in:
counterweight 2025-02-13 00:02:40 +01:00
parent 768efaf3a2
commit fb9832fabb
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
8 changed files with 306 additions and 92 deletions

View file

@ -1,7 +0,0 @@
npub -> publicKey
exports.createInvitedNpub = createPublicKeyInvite;
exports.isNpubInvited = isPublicKeyInvited;
invitedNpubService -> PublicKeyInvitedService

34
src/errors.js Normal file
View file

@ -0,0 +1,34 @@
class ChallengedUsedError extends Error {
constructor(message) {
super(message);
this.name = "ChallengeUsedError";
}
}
class InvalidSignatureError extends Error {
constructor(message) {
super(message);
this.name = "InvalidSignatureError";
}
}
class AppInvitedUsedError extends Error {
constructor(message) {
super(message);
this.name = "AppInvitedUsedError";
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = "AppInvitedUsedError";
}
}
module.exports = {
ChallengedUsedError,
InvalidSignatureError,
AppInvitedUsedError,
NotFoundError
};

View file

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

View file

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

View file

@ -1,25 +1,40 @@
async function acceptInvite() {
const publicKey = await window.nostr.getPublicKey();
// check if there is nostr extension
if (!window.nostr) {
console.log("No Nostr extension found.");
return { success: false, error: "No Nostr extension detected." };
}
try {
const response = await fetch('/api/sign-public-key-up', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
publicKey
})
const challengeResponse = await fetch("/api/signup/nostr-challenge");
if (!challengeResponse.ok) throw new Error("Failed to fetch challenge");
const { challenge } = await challengeResponse.json();
const pubkey = await window.nostr.getPublicKey();
const event = {
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
tags: [["challenge", challenge]],
content: "Sign this challenge to authenticate",
pubkey: pubkey
};
const signedEvent = await window.nostr.signEvent(event);
const verifyResponse = await fetch("/api/signup/nostr-verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(signedEvent),
});
if (response.ok) {
const data = await response.json();
console.log('invited-npub record created successfully:', data);
} else {
const error = await response.json();
console.error('Failed to create invited-npub record:', error);
}
} catch (error) {
console.error('An error occurred:', error);
if (verifyResponse.status === 200) {
}
} catch (error) { }
}

View file

@ -5,6 +5,9 @@ const crypto = require("crypto");
const invitesService = require('../services/invitesService');
const sessionService = require('../services/sessionService');
const nostrService = require('../services/nostrService');
const { error } = require('console');
const { TimeoutError } = require('sequelize');
const errors = require('../errors');
const router = express.Router();
@ -58,54 +61,40 @@ router.post('/sign-public-key-up', async (req, res) => {
});
router.get('/nostr-challenge', async (req, res) => {
const nostrChallenge = await nostrService.createNostrChallenge();
res.json({ 'challenge': nostrChallenge.challenge });
router.get('/signup/nostr-challenge', async (req, res) => {
const inviteUuid = req.cookies.inviteUuid;
const signUpChallenge = await invitesService.createSignUpChallenge(
inviteUuid
)
const relatedNostrChallenge = await nostrService.getNostrChallenge(
signUpChallenge.nostr_challenge_uuid
)
res.status(200).json({ 'challenge': relatedNostrChallenge.challenge });
});
router.post("/nostr-verify", async (req, res) => {
router.post("/signup/nostr-verify", async (req, res) => {
const signedEvent = req.body;
if (!signedEvent || !signedEvent.tags) {
return res.status(400).json({ success: false, error: "Invalid event format" });
try {
console.log(`Starting nostr-verify with event: ${signedEvent}`);
const completedSignUpChallenge = await invitesService.verifySignUpChallenge(signedEvent);
console.log(`Finished nostr-verify`);
} catch (error) {
if (error instanceof TimeoutError) {
console.error('The challenge is outdated.');
}
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
if (!challengeTag) {
return res.status(400).json({ success: false, error: "No challenge tag found" });
if (error instanceof errors.ChallengedUsedError) {
console.error('The challenge was already used, request a new one.');
}
const challenge = challengeTag[1];
if (!(await nostrService.isNostrChallengeFresh(challenge))) {
return res.status(410).json({ success: false, error: "Challenge expired, request new one." })
if (error instanceof errors.InvalidSignatureError) {
console.error('Signature is not valid.')
}
if (await nostrService.hasNostrChallengeBeenCompleted(challenge)) {
return res.status(410).json({ success: false, error: "Challenge already used, request new one." })
}
const isSignatureValid = verifyEvent(signedEvent);
if (!isSignatureValid) {
return res.status(400).json({ success: false, error: "Invalid signature" });
}
if (!invitesService.isPublicKeyInvited(signedEvent.pubkey)) {
return res.status(400).json(
{
success: false,
error: "Valid signature, but npub is not invited to app."
}
)
}
await nostrService.completeNostrChallenge(
challenge,
signedEvent
)
return res.json({ success: true, signedEvent });
return res.status(200).json({ success: true });
});
module.exports = router;

View file

@ -1,7 +1,15 @@
const uuid = require('uuid');
const nostrService = require('./nostrService');
const AppInviteCreated = require('../models/AppInviteCreated');
const PublicKeySignedUp = require('../models/PublicKeySignedUp');
const SignUpChallengeCreated = require('../models/SignUpChallengeCreated');
const SignUpChallengeCompleted = require('../models/SignUpChallengeCompleted');
const errors = require('../errors');
const NostrChallengeCompleted = require('../models/NostrChallengeCompleted');
const { sortEvents } = require('nostr-tools');
async function appInviteExists(inviteUuid) {
const invite = await AppInviteCreated.findOne({ where: { uuid: inviteUuid } });
@ -16,14 +24,14 @@ async function getAppInvite(inviteUuid) {
return invite;
}
async function isAppInviteSpent(inviteUuid) {
const invitedNpub = await PublicKeySignedUp.findOne({
async function isAppInviteSpent(appInviteUuid) {
const signUpChallengeCompleted = await SignUpChallengeCompleted.findOne({
where: {
app_invite_uuid: inviteUuid
app_invite_uuid: appInviteUuid
}
})
if (invitedNpub) {
if (signUpChallengeCompleted) {
return true;
}
return false;
@ -38,6 +46,79 @@ async function createAppInvite(inviterNpub) {
);
}
async function createSignUpChallenge(appInviteUuid) {
if (!(await appInviteExists(appInviteUuid))) {
throw new errors.NotFoundError("Invite doesn't exist.")
}
if (await isAppInviteSpent(appInviteUuid)) {
throw new errors.AppInvitedUsedError("Invite has already been used.")
}
const nostrChallenge = await nostrService.createNostrChallenge()
return await SignUpChallengeCreated.create({
'uuid': uuid.v7(),
nostr_challenge_uuid: nostrChallenge.uuid,
app_invite_uuid: appInviteUuid,
created_at: new Date().toISOString()
}
)
}
async function hasSignUpChallengeBeenCompleted(nostrChallengeCompletedUuid) {
const signUpChallengeCompleted = await SignUpChallengeCompleted.findOne(
{
where:
{
}
}
)
}
async function verifySignUpChallenge(signedEvent) {
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
const challenge = challengeTag[1];
const nostrChallenge = await nostrService.getNostrChallenge(
null, challenge
);
console.log(`Found this nostr challenge: ${nostrChallenge}`);
const signUpChallenge = await SignUpChallengeCreated.findOne({
where: {
nostr_challenge_uuid: nostrChallenge.uuid
}
})
console.log(`Found this signup challenge: ${signUpChallenge}`);
if (await nostrService.hasNostrChallengeBeenCompleted(challenge)) {
throw new errors.ChallengedUsedError("This challenge has already been used.");
}
console.log(`I'm gonna verify the nostr challenge`);
const completedNostrChallenge = await nostrService.verifyNostrChallenge(signedEvent);
console.log(`Verified the NostrChallenge: ${completedNostrChallenge}`);
const completedSignUpChallenge = await SignUpChallengeCompleted.create(
{
'uuid': uuid.v7(),
nostr_challenge_completed_uuid: completedNostrChallenge.uuid,
app_invite_uuid: signUpChallenge.app_invite_uuid,
public_key: completedNostrChallenge.public_key,
created_at: new Date().toISOString()
}
);
console.log(`Verified the SignUpChallenge: ${completedSignUpChallenge}`);
return completedSignUpChallenge;
}
async function signUpPublicKey(inviteUuid, publicKey) {
if (await isAppInviteSpent(inviteUuid)) {
@ -68,6 +149,8 @@ module.exports = {
getAppInvite,
isAppInviteSpent,
createAppInvite,
createSignUpChallenge,
verifySignUpChallenge,
signUpPublicKey,
isPublicKeySignedUp
};

View file

@ -1,12 +1,13 @@
const uuid = require("uuid");
const crypto = require("crypto");
const { Op } = require('sequelize');
const { Op, TimeoutError } = require('sequelize');
const { verifyEvent } = require("nostr-tools");
const NostrChallengeCreated = require('../models/NostrChallengeCreated');
const NostrChallengeCompleted = require("../models/NostrChallengeCompleted");
const constants = require('../constants');
const errors = require('../errors');
async function createNostrChallenge() {
@ -24,6 +25,59 @@ async function createNostrChallenge() {
return nostrChallenge;
}
async function getNostrChallenge(nostrChallengeUuid = null, challenge = null) {
if (nostrChallengeUuid) {
return await NostrChallengeCreated.findOne({
where: {
'uuid': nostrChallengeUuid
}
})
}
if (challenge) {
return await NostrChallengeCreated.findOne({
where: {
challenge
}
})
}
throw Error('You need to pass a uuid or a challenge.')
}
async function verifyNostrChallenge(signedEvent) {
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
const challenge = challengeTag[1];
console.log("Checking if fresh")
if (!(await isNostrChallengeFresh(challenge))) {
throw TimeoutError("Challenge expired, request new one.");
}
console.log("Checking if completed")
if (await hasNostrChallengeBeenCompleted(challenge)) {
throw new errors.ChallengedUsedError("Challenge already used, request new one.");
}
console.log("Checking if valid")
const isSignatureValid = verifyEvent(signedEvent);
if (!isSignatureValid) {
throw new errors.InvalidSignatureError("Signature is not valid.");
}
console.log("Persisting")
return await NostrChallengeCompleted.create({
'uuid': uuid.v7(),
challenge: challenge,
signed_event: signedEvent,
public_key: signedEvent.pubkey,
created_at: new Date().toISOString()
}
);
}
async function isNostrChallengeFresh(challengeString) {
const nostrChallenge = await NostrChallengeCreated.findOne({
where: {
@ -55,23 +109,11 @@ async function hasNostrChallengeBeenCompleted(challengeString) {
return false;
}
async function completeNostrChallenge(
challenge,
signedEvent
) {
await NostrChallengeCompleted.create({
'uuid': uuid.v7(),
challenge: challenge,
signed_event: signedEvent,
public_key: signedEvent.pubkey,
created_at: new Date().toISOString()
}
);
return;
}
exports.createNostrChallenge = createNostrChallenge;
exports.isNostrChallengeFresh = isNostrChallengeFresh;
exports.hasNostrChallengeBeenCompleted = hasNostrChallengeBeenCompleted;
exports.completeNostrChallenge = completeNostrChallenge;
module.exports = {
createNostrChallenge,
getNostrChallenge,
verifyNostrChallenge,
isNostrChallengeFresh,
hasNostrChallengeBeenCompleted
};