feat(infisical-pg): completed signup, login, password and backup key migration

This commit is contained in:
Akhil Mohan
2024-01-27 12:38:22 +05:30
parent 05205d1eff
commit fcfd6b3fb2
68 changed files with 7529 additions and 58 deletions
+16 -24
View File
@@ -1,6 +1,5 @@
import dotenv from "dotenv";
import fasitfy from "fastify";
import { z } from "zod";
import type { FastifyCookieOptions } from "@fastify/cookie";
import cookie from "@fastify/cookie";
import type { FastifyCorsOptions } from "@fastify/cors";
@@ -9,20 +8,24 @@ import helmet from "@fastify/helmet";
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
import ratelimiter from "@fastify/rate-limit";
import { initEnvConfig } from "@lib/config/env";
import { initDbConnection } from "@app/db";
import { smtpServiceFactory } from "@app/services/smtp/smtp-service";
import { formatSmtpConfig, initEnvConfig } from "@lib/config/env";
import { initLogger } from "@lib/logger";
import { globalRateLimiterCfg } from "./config/rateLimiter";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
import { fastifyIp } from "./plugins/ip";
import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes";
dotenv.config();
// Run the server!
const main = async () => {
const logger = await initLogger();
initEnvConfig(logger);
const envCfg = initEnvConfig(logger);
const server = fasitfy({
logger,
@@ -32,42 +35,31 @@ const main = async () => {
server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler);
const db = initDbConnection(envCfg.DB_CONNECTION_URI);
const smtp = smtpServiceFactory(formatSmtpConfig());
try {
// TODO(akhilmhdh:pg): change this to environment variable with default
await server.register<FastifyCookieOptions>(cookie, {
secret: "infisical-cookie-secret"
secret: envCfg.COOKIE_SECRET_SIGN_KEY
});
await server.register<FastifyCorsOptions>(cors, {
credentials: true,
origin: "http://localhost:3000"
origin: true
});
// pull ip based on various proxy headers
await server.register(fastifyIp);
// Rate limiters and security headers
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
await server.register(helmet);
await server.register(fastifySwagger);
// Declare a route
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: z.object({ hello: z.string() })
}
},
handler: () => ({
hello: "world"
})
});
// Rate limiters and security headers
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
await server.register(helmet, { contentSecurityPolicy: false });
await server.register(registerRoutes, { prefix: "/api", smtp, db });
await server.ready();
server.swagger();
await server.listen({ port: 8000 });
await server.listen({ port: envCfg.PORT, host: envCfg.HOST });
} catch (err) {
server.log.error(err);
process.exit(1);
+5 -4
View File
@@ -4,6 +4,7 @@ import swaggerUI from "@fastify/swagger-ui";
import { jsonSchemaTransform } from "./fastify-zod";
// TODO(akhilmhdh-pg): change the localhost port later
export const fastifySwagger = fp(async (fastify) => {
await fastify.register(swagger, {
transform: jsonSchemaTransform,
@@ -15,12 +16,12 @@ export const fastifySwagger = fp(async (fastify) => {
},
servers: [
{
url: "https://app.infisical.com",
description: "Production server"
url: "http://localhost:4000",
description: "Local server"
},
{
url: "http://localhost:8000",
description: "Local server"
url: "https://app.infisical.com",
description: "Production server"
}
],
components: {
+44
View File
@@ -0,0 +1,44 @@
import { Knex } from "knex";
import { authDalFactory } from "@app/services/auth/auth-dal";
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { tokenDalFactory } from "@app/services/token/token-dal";
import { tokenServiceFactory } from "@app/services/token/token-service";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
export const registerRoutes = async (
server: FastifyZodProvider,
{ db, smtp }: { db: Knex; smtp: TSmtpService }
) => {
// db layers
const authDal = authDalFactory(db);
const authTokenDal = tokenDalFactory(db);
// service layers
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
const loginService = authLoginServiceFactory({ authDal, smtpService: smtp, tokenService });
const passwordService = authPaswordServiceFactory({ tokenService, smtpService: smtp, authDal });
const signupService = authSignupServiceFactory({ tokenService, smtpService: smtp, authDal });
// inject all services
server.decorate("services", {
login: loginService,
password: passwordService,
signup: signupService
} as FastifyZodProvider["services"]);
server.decorate("store", {
user: authDal
} as FastifyZodProvider["store"]);
// register routes for v1
await server.register(registerV1Routes, { prefix: "/v1" });
await server.register(registerV2Routes, { prefix: "/v2" });
await server.register(registerV3Routes, { prefix: "/v3" });
};
+5
View File
@@ -0,0 +1,5 @@
import { registerPasswordRouter } from "./password-router";
export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerPasswordRouter, { prefix: "/password" });
};
@@ -0,0 +1,115 @@
import { z } from "zod";
import { BackupPrivateKeySchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/srp1",
schema: {
body: z.object({
clientPublicKey: z.string().trim()
}),
response: {
200: z.object({
serverPublicKey: z.string(),
salt: z.string()
})
}
},
handler: async (req) => {
const { salt, serverPublicKey } = await server.services.password.generateServerPubKey(
req.auth.userId,
req.body.clientPublicKey
);
return { salt, serverPublicKey };
}
});
server.route({
method: "POST",
url: "/change-password",
schema: {
body: z.object({
clientProof: z.string().trim(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req, res) => {
const appCfg = getConfig();
await server.services.password.changePassword({ ...req.body, userId: req.auth.userId });
res.cookie("jid", appCfg.COOKIE_SECRET_SIGN_KEY, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully changed password" };
}
});
server.route({
method: "POST",
url: "/backup-private-key",
schema: {
body: z.object({
clientProof: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
iv: z.string().trim(),
tag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
backupPrivateKey: BackupPrivateKeySchema
})
}
},
handler: async (req) => {
const backupPrivateKey = await server.services.password.createBackupPrivateKey({
...req.body,
userId: req.auth.userId
});
if (!backupPrivateKey) throw new Error("Failed to create backup key");
return { message: "Successfully updated backup private key", backupPrivateKey };
}
});
server.route({
method: "GET",
url: "/backup-private-key",
schema: {
response: {
200: z.object({
message: z.string(),
backupPrivateKey: BackupPrivateKeySchema
})
}
},
handler: async (req) => {
const backupPrivateKey = await server.services.password.getBackupPrivateKeyOfUser(
req.auth.userId
);
if (!backupPrivateKey) throw new Error("Failed to find backup key");
return { message: "Successfully updated backup private key", backupPrivateKey };
}
});
};
+5
View File
@@ -0,0 +1,5 @@
import { registerMfaRouter } from "./mfa-router";
export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" });
};
@@ -0,0 +1,89 @@
import jwt, { JwtPayload } from "jsonwebtoken";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { AuthTokenType } from "@app/services/auth/auth-signup-type";
export const registerMfaRouter = async (server: FastifyZodProvider) => {
const cfg = getConfig();
server.addHook("preParsing", async (req, res) => {
const authorizationHeader = req.headers.authorization;
if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) {
res.status(401).send({ error: "Missing bearer token" });
return;
}
const token = authorizationHeader.split(" ")[1];
if (!token) res.status(401).send({ error: "Missing bearer token" });
const decodedToken = jwt.verify(token, cfg.JWT_AUTH_SECRET) as JwtPayload;
if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN)
throw new Error("Unauthorized access");
const user = await server.store.user.getUserById(decodedToken.userId);
if (!user) throw new Error("User not found");
req.mfa.userId = user.id;
req.mfa.user = user;
});
server.route({
url: "/mfa/send",
method: "POST",
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.login.resendMfaToken(req.mfa.userId);
return { message: "Successfully send new mfa code" };
}
});
server.route({
url: "/mfa/verify",
method: "POST",
schema: {
body: z.object({
mfaToken: z.string().trim()
}),
response: {
200: z.object({
encryptionVersion: z.number().default(1).optional(),
protectedKey: z.string(),
protectedKeyIV: z.string(),
protectedKeyTag: z.string(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
token: z.string()
})
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const appCfg = getConfig();
const { user, token } = await server.services.login.verifyMfaToken({
userAgent,
ip: req.realIp,
userId: req.mfa.userId,
mfaToken: req.body.mfaToken
});
res.setCookie("jid", token.refresh, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { token: token.access, ...user };
}
});
};
+7
View File
@@ -0,0 +1,7 @@
import { registerLoginRouter } from "./login-router";
import { registerSignupRouter } from "./signup-router";
export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerSignupRouter, { prefix: "/signup" });
await server.register(registerLoginRouter, { prefix: "/auth" });
};
@@ -0,0 +1,97 @@
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
export const registerLoginRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/login1",
schema: {
body: z.object({
email: z.string().email().trim(),
providerAuthToken: z.string().trim().optional(),
clientPublicKey: z.string().trim()
}),
response: {
200: z.object({
serverPublicKey: z.string(),
salt: z.string()
})
}
},
handler: async (req) => {
const { serverPublicKey, salt } = await server.services.login.loginGenServerPublicKey({
email: req.body.email,
clientPublicKey: req.body.clientPublicKey,
providerAuthToken: req.body.providerAuthToken
});
return { serverPublicKey, salt };
}
});
server.route({
method: "POST",
url: "/login2",
schema: {
body: z.object({
email: z.string().email().trim(),
providerAuthToken: z.string().trim().optional(),
clientProof: z.string().trim()
}),
response: {
200: z.discriminatedUnion("mfaEnabled", [
z.object({ mfaEnabled: z.literal(true), token: z.string() }),
z.object({
mfaEnabled: z.literal(false),
encryptionVersion: z.number().default(1).optional(),
protectedKey: z.string(),
protectedKeyIV: z.string(),
protectedKeyTag: z.string(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
token: z.string()
})
])
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const appCfg = getConfig();
const data = await server.services.login.loginExchangeClientProof({
email: req.body.email,
ip: req.realIp,
userAgent,
clientProof: req.body.clientProof
});
if (data.isMfaEnabled) {
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
}
res.setCookie("jid", data.token.refresh, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return {
mfaEnabled: false,
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,
publicKey: data.user.publicKey,
encryptedPrivateKey: data.user.encryptedPrivateKey,
iv: data.user.iv,
tag: data.user.tag,
protectedKey: data.user.protectedKey,
protectedKeyIV: data.user.protectedKeyIV,
protectedKeyTag: data.user.protectedKeyTag
} as const;
}
});
};
@@ -0,0 +1,103 @@
import { z } from "zod";
import { UserSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
export const registerSignupRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/email/signup",
method: "POST",
schema: {
body: z.object({
email: z.string().email().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.signup.beginEmailSignupProcess(req.body.email);
return { message: `Sent an email verification code to ${req.body.email}` };
}
});
server.route({
url: "/email/verify",
method: "POST",
schema: {
body: z.object({
email: z.string().email().trim(),
code: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
token: z.string(),
user: UserSchema
})
}
},
handler: async (req) => {
const { token, user } = await server.services.signup.verifyEmailSignup(
req.body.email,
req.body.code
);
return { message: "Successfuly verified email", token, user };
}
});
server.route({
url: "/complete-account/signup",
method: "POST",
schema: {
body: z.object({
email: z.string().email().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
organizationName: z.string().trim(),
providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional()
}),
response: {
200: z.object({
message: z.string(),
user: UserSchema,
token: z.string()
})
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const appCfg = getConfig();
const { user, accessToken, refreshToken } =
await server.services.signup.completeEmailAccountSignup({
...req.body,
ip: req.realIp,
userAgent
});
res.setCookie("jid", refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
// TODO(akhilmhdh-pg): add telemetry service
return { message: "Successfully set up account", user, token: accessToken };
}
});
};