2024-06-16 18:56:41 +00:00
|
|
|
import { createVerify } from "node:crypto";
|
|
|
|
import Fastify from "fastify";
|
|
|
|
import * as Jose from "jose";
|
|
|
|
import { v4 as uuidv4 } from "uuid";
|
2024-06-16 10:18:19 +00:00
|
|
|
import assert from "node:assert";
|
2024-06-13 20:00:45 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
const IDENTITY_API_LANDING_MESSAGE = "identity-api v1.0.0";
|
|
|
|
const JWT_SECRET = new TextEncoder().encode("cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2");
|
|
|
|
const JWT_ALG = "HS256";
|
2024-06-13 20:00:45 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
const ASSET_API_ENDPOINT = "http://localhost:3001/";
|
|
|
|
let ASSET_API_PUBKEY = await loadAssetPubkey();
|
|
|
|
let ASSET_API_ALGORITHM = await loadAssetAlgo();
|
2024-06-16 18:39:10 +00:00
|
|
|
|
|
|
|
setInterval(async () => {
|
|
|
|
try {
|
2024-06-16 18:56:41 +00:00
|
|
|
let pubkey = await loadAssetPubkey();
|
|
|
|
let algo = await loadAssetAlgo();
|
2024-06-16 18:39:10 +00:00
|
|
|
|
|
|
|
if (pubkey != null && algo != null) {
|
|
|
|
if (ASSET_API_PUBKEY !== pubkey) {
|
2024-06-16 18:56:41 +00:00
|
|
|
console.warn("The M2M public key has changed!");
|
2024-06-16 18:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (ASSET_API_ALGORITHM !== algo) {
|
2024-06-16 18:56:41 +00:00
|
|
|
console.warn("The M2M algorith has changed!");
|
2024-06-16 18:39:10 +00:00
|
|
|
}
|
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
ASSET_API_PUBKEY = pubkey;
|
|
|
|
ASSET_API_ALGORITHM = algo;
|
|
|
|
console.debug("Successfully updated the M2M public key and algorithm");
|
2024-06-16 18:39:10 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2024-06-16 18:56:41 +00:00
|
|
|
console.warn("Failed to update the M2M public key", e);
|
2024-06-16 18:39:10 +00:00
|
|
|
}
|
2024-06-16 18:56:41 +00:00
|
|
|
}, 60 * 1000);
|
2024-06-16 10:18:19 +00:00
|
|
|
|
|
|
|
let session_keys = {
|
2024-06-16 18:56:41 +00:00
|
|
|
"uid:005d6417-a23c-48bd-b348-eafeae649b94": "e381ba8c-e18a-4bca-afce-b212b37bc26b",
|
|
|
|
"key:e381ba8c-e18a-4bca-afce-b212b37bc26b": "005d6417-a23c-48bd-b348-eafeae649b94",
|
2024-06-16 10:18:19 +00:00
|
|
|
};
|
|
|
|
|
2024-06-13 20:00:45 +00:00
|
|
|
let users = {
|
2024-06-16 18:56:41 +00:00
|
|
|
"jane@identity.net": {
|
|
|
|
uid: "005d6417-a23c-48bd-b348-eafeae649b94",
|
|
|
|
email: "jane@identity.net",
|
|
|
|
password: "12345678901234567890",
|
|
|
|
name: "Jane Doe",
|
2024-06-16 10:53:12 +00:00
|
|
|
assets: ["f9d040d6-598c-4483-952f-08e7d35d5420.jpg"],
|
2024-06-16 18:56:41 +00:00
|
|
|
},
|
|
|
|
};
|
2024-06-13 20:00:45 +00:00
|
|
|
|
|
|
|
const fastify = Fastify({
|
|
|
|
logger: true,
|
2024-06-16 18:56:41 +00:00
|
|
|
});
|
2024-06-13 20:00:45 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
fastify.get("/", async () => {
|
2024-06-13 20:00:45 +00:00
|
|
|
return IDENTITY_API_LANDING_MESSAGE;
|
2024-06-16 18:56:41 +00:00
|
|
|
});
|
2024-06-13 20:00:45 +00:00
|
|
|
|
2024-06-16 10:53:12 +00:00
|
|
|
fastify.put("/m2m/asset", {
|
2024-06-16 10:18:19 +00:00
|
|
|
async handler(request, reply) {
|
|
|
|
if (!verifySignature(request.body, ASSET_API_PUBKEY)) {
|
2024-06-16 18:56:41 +00:00
|
|
|
reply.statusCode(401);
|
|
|
|
return;
|
2024-06-16 10:53:12 +00:00
|
|
|
}
|
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let body = JSON.parse(getContentFromSigned(request.body));
|
2024-06-16 10:53:12 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let uid = session_keys[`key:${body.session_key}`];
|
|
|
|
let user = Object.values(users).filter((v) => v.uid === uid);
|
|
|
|
assert(user.length === 1);
|
2024-06-16 10:53:12 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
users[user[0].email].assets.push(body.asset_id);
|
|
|
|
console.log(users[user[0].email].assets);
|
|
|
|
},
|
|
|
|
});
|
2024-06-16 10:53:12 +00:00
|
|
|
|
|
|
|
fastify.post("/m2m/account", {
|
|
|
|
async handler(request, reply) {
|
|
|
|
if (!verifySignature(request.body, ASSET_API_PUBKEY)) {
|
2024-06-16 18:56:41 +00:00
|
|
|
reply.statusCode(401);
|
|
|
|
return;
|
2024-06-16 10:18:19 +00:00
|
|
|
}
|
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let body = JSON.parse(getContentFromSigned(request.body));
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let uid = session_keys[`key:${body.session_key}`];
|
|
|
|
let user = Object.values(users).filter((v) => v.uid === uid);
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
assert(user.length === 1);
|
|
|
|
user[0].password = undefined;
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
return user[0];
|
|
|
|
},
|
|
|
|
});
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
fastify.get("/auth/genkey", {
|
|
|
|
async handler(request) {
|
|
|
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
|
|
|
let { payload } = await Jose.jwtVerify(jwt, JWT_SECRET);
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let key = uuidv4();
|
|
|
|
session_keys[`uid:${payload.uid}`] = key;
|
|
|
|
session_keys[`key:${key}`] = payload.uid;
|
2024-06-16 10:18:19 +00:00
|
|
|
|
|
|
|
return {
|
2024-06-16 18:56:41 +00:00
|
|
|
session_key: session_keys[payload.uid],
|
|
|
|
};
|
2024-06-16 10:18:19 +00:00
|
|
|
},
|
|
|
|
schema: {
|
|
|
|
headers: {
|
2024-06-16 18:56:41 +00:00
|
|
|
type: "object",
|
2024-06-16 10:18:19 +00:00
|
|
|
properties: {
|
2024-06-16 18:56:41 +00:00
|
|
|
authorization: { type: "string" },
|
2024-06-16 10:18:19 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2024-06-16 18:56:41 +00:00
|
|
|
});
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
fastify.get("/auth/account", {
|
|
|
|
async handler(request) {
|
|
|
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
|
|
|
let { payload } = await Jose.jwtVerify(jwt, JWT_SECRET);
|
2024-06-13 21:48:12 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let user = users[payload.email];
|
|
|
|
user.password = undefined;
|
|
|
|
return user;
|
2024-06-13 21:48:12 +00:00
|
|
|
},
|
|
|
|
schema: {
|
|
|
|
headers: {
|
2024-06-16 18:56:41 +00:00
|
|
|
type: "object",
|
2024-06-13 21:48:12 +00:00
|
|
|
properties: {
|
2024-06-16 18:56:41 +00:00
|
|
|
authorization: { type: "string" },
|
2024-06-13 21:48:12 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2024-06-16 18:56:41 +00:00
|
|
|
});
|
2024-06-13 21:48:12 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
fastify.post("/auth/login", {
|
2024-06-13 20:00:45 +00:00
|
|
|
async handler(request, reply) {
|
|
|
|
let user = users[request.body.email];
|
|
|
|
if (user != null && user.password == request.body.password) {
|
|
|
|
let token = await new Jose.SignJWT({
|
|
|
|
uid: user.uid,
|
2024-06-13 21:48:12 +00:00
|
|
|
email: request.body.email,
|
2024-06-13 20:00:45 +00:00
|
|
|
name: user.name,
|
|
|
|
})
|
|
|
|
.setProtectedHeader({ alg: JWT_ALG })
|
|
|
|
.setIssuedAt()
|
2024-06-16 18:56:41 +00:00
|
|
|
.setExpirationTime("4w")
|
2024-06-13 20:00:45 +00:00
|
|
|
.sign(JWT_SECRET);
|
2024-06-16 18:56:41 +00:00
|
|
|
|
2024-06-13 20:00:45 +00:00
|
|
|
return {
|
|
|
|
token,
|
2024-06-16 18:56:41 +00:00
|
|
|
};
|
2024-06-13 20:00:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
reply.code(400);
|
|
|
|
return {
|
2024-06-16 18:56:41 +00:00
|
|
|
error: "invalid credentials",
|
|
|
|
};
|
2024-06-13 20:00:45 +00:00
|
|
|
},
|
|
|
|
schema: {
|
|
|
|
body: {
|
2024-06-16 18:56:41 +00:00
|
|
|
type: "object",
|
2024-06-13 20:00:45 +00:00
|
|
|
properties: {
|
2024-06-16 18:56:41 +00:00
|
|
|
email: { type: "string" },
|
|
|
|
password: { type: "string" },
|
2024-06-13 20:00:45 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2024-06-16 18:56:41 +00:00
|
|
|
});
|
2024-06-13 20:00:45 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
fastify.post("/auth/register", {
|
2024-06-13 20:00:45 +00:00
|
|
|
async handler(request, reply) {
|
|
|
|
if (users[request.body.email] == null) {
|
|
|
|
users[request.body.email] = {
|
|
|
|
uid: uuidv4(),
|
2024-06-16 18:39:10 +00:00
|
|
|
email: request.body.email,
|
2024-06-13 20:00:45 +00:00
|
|
|
password: request.body.password,
|
|
|
|
name: request.body.name,
|
2024-06-16 10:53:12 +00:00
|
|
|
assets: [],
|
2024-06-16 18:56:41 +00:00
|
|
|
};
|
2024-06-13 20:00:45 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let user = users[request.body.email];
|
2024-06-13 20:00:45 +00:00
|
|
|
let token = await new Jose.SignJWT({
|
|
|
|
uid: user.uid,
|
2024-06-13 21:48:12 +00:00
|
|
|
email: request.body.email,
|
2024-06-13 20:00:45 +00:00
|
|
|
name: user.name,
|
|
|
|
})
|
|
|
|
.setProtectedHeader({ alg: JWT_ALG })
|
|
|
|
.setIssuedAt()
|
2024-06-16 18:56:41 +00:00
|
|
|
.setExpirationTime("4w")
|
2024-06-13 20:00:45 +00:00
|
|
|
.sign(JWT_SECRET);
|
2024-06-16 18:56:41 +00:00
|
|
|
|
|
|
|
return { token };
|
2024-06-13 20:00:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
reply.code(400);
|
|
|
|
return {
|
2024-06-16 18:56:41 +00:00
|
|
|
error: "invalid data",
|
|
|
|
};
|
2024-06-13 20:00:45 +00:00
|
|
|
},
|
|
|
|
schema: {
|
|
|
|
body: {
|
2024-06-16 18:56:41 +00:00
|
|
|
type: "object",
|
2024-06-13 20:00:45 +00:00
|
|
|
properties: {
|
2024-06-16 18:56:41 +00:00
|
|
|
name: { type: "string" },
|
|
|
|
email: { type: "string" },
|
|
|
|
password: { type: "string" },
|
2024-06-13 20:00:45 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2024-06-16 18:56:41 +00:00
|
|
|
});
|
2024-06-13 20:00:45 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
fastify.listen({ port: 3000 });
|
2024-06-16 10:18:19 +00:00
|
|
|
|
|
|
|
async function loadAssetPubkey() {
|
2024-06-16 18:56:41 +00:00
|
|
|
let url = new URL(ASSET_API_ENDPOINT);
|
|
|
|
url.pathname = "/crypto/cert";
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let res = await fetch(url);
|
|
|
|
return await res.text();
|
2024-06-16 10:18:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function loadAssetAlgo() {
|
2024-06-16 18:56:41 +00:00
|
|
|
let url = new URL(ASSET_API_ENDPOINT);
|
|
|
|
url.pathname = "/crypto/algo";
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let res = await fetch(url);
|
|
|
|
return await res.text();
|
2024-06-16 10:18:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-06-16 18:56:41 +00:00
|
|
|
*
|
|
|
|
* @param {string} content
|
|
|
|
* @param {string} pubkeyText
|
2024-06-16 10:18:19 +00:00
|
|
|
*/
|
|
|
|
function verifySignature(content, pubkeyText) {
|
|
|
|
let parts = content
|
|
|
|
.replace("-----BEGIN SIGNED MESSAGE-----\n\n", "")
|
|
|
|
.replace("\n-----END SIGNATURE-----", "")
|
2024-06-16 18:56:41 +00:00
|
|
|
.split("\n\n-----BEGIN SIGNATURE-----\n\n");
|
|
|
|
|
2024-06-16 10:18:19 +00:00
|
|
|
assert(parts.length === 2);
|
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let verify = createVerify(ASSET_API_ALGORITHM);
|
|
|
|
verify.update(parts[0]);
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
let pubkey = Buffer.from(pubkeyText, "ascii");
|
|
|
|
let digest = Buffer.from(parts[1], "base64");
|
2024-06-16 10:18:19 +00:00
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
return verify.verify(pubkey, digest);
|
2024-06-16 10:18:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getContentFromSigned(content) {
|
|
|
|
let parts = content
|
|
|
|
.replace("-----BEGIN SIGNED MESSAGE-----\n\n", "")
|
|
|
|
.replace("\n-----END SIGNATURE-----", "")
|
2024-06-16 18:56:41 +00:00
|
|
|
.split("\n\n-----BEGIN SIGNATURE-----\n\n");
|
|
|
|
|
2024-06-16 10:18:19 +00:00
|
|
|
assert(parts.length === 2);
|
|
|
|
|
2024-06-16 18:56:41 +00:00
|
|
|
return parts[0];
|
|
|
|
}
|