mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 07:26:00 +00:00
Merge pull request #1944 from akhilmhdh/feat/srp-handover
Removing Master password for Oauth/SSO/LDAP users.
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
|
||||||
|
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKey"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyIV"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyTag"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyEncoding"
|
||||||
|
);
|
||||||
|
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
|
||||||
|
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
|
||||||
|
if (!doesPasswordFieldExist) t.string("hashedPassword");
|
||||||
|
if (!doesPrivateKeyFieldExist) t.text("serverEncryptedPrivateKey");
|
||||||
|
if (!doesPrivateKeyIVFieldExist) t.text("serverEncryptedPrivateKeyIV");
|
||||||
|
if (!doesPrivateKeyTagFieldExist) t.text("serverEncryptedPrivateKeyTag");
|
||||||
|
if (!doesPrivateKeyEncodingFieldExist) t.text("serverEncryptedPrivateKeyEncoding");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
|
||||||
|
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKey"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyIV"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyTag"
|
||||||
|
);
|
||||||
|
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
"serverEncryptedPrivateKeyEncoding"
|
||||||
|
);
|
||||||
|
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
|
||||||
|
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
|
||||||
|
if (doesPasswordFieldExist) t.dropColumn("hashedPassword");
|
||||||
|
if (doesPrivateKeyFieldExist) t.dropColumn("serverEncryptedPrivateKey");
|
||||||
|
if (doesPrivateKeyIVFieldExist) t.dropColumn("serverEncryptedPrivateKeyIV");
|
||||||
|
if (doesPrivateKeyTagFieldExist) t.dropColumn("serverEncryptedPrivateKeyTag");
|
||||||
|
if (doesPrivateKeyEncodingFieldExist) t.dropColumn("serverEncryptedPrivateKeyEncoding");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,12 @@ export const UserEncryptionKeysSchema = z.object({
|
|||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
salt: z.string(),
|
salt: z.string(),
|
||||||
verifier: z.string(),
|
verifier: z.string(),
|
||||||
userId: z.string().uuid()
|
userId: z.string().uuid(),
|
||||||
|
hashedPassword: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKey: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKeyIV: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKeyTag: z.string().nullable().optional(),
|
||||||
|
serverEncryptedPrivateKeyEncoding: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>;
|
export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>;
|
||||||
|
|||||||
@@ -73,7 +73,13 @@ type TLdapConfigServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
TUserDALFactory,
|
TUserDALFactory,
|
||||||
"create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find"
|
| "create"
|
||||||
|
| "findOne"
|
||||||
|
| "transaction"
|
||||||
|
| "updateById"
|
||||||
|
| "findUserEncKeyByUserIdsBatch"
|
||||||
|
| "find"
|
||||||
|
| "findUserEncKeyByUserId"
|
||||||
>;
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
@@ -592,12 +598,14 @@ export const ldapConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
|
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f
|
|||||||
|
|
||||||
type TSamlConfigServiceFactoryDep = {
|
type TSamlConfigServiceFactoryDep = {
|
||||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
|
||||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
|
userDAL: Pick<
|
||||||
|
TUserDALFactory,
|
||||||
|
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
|
||||||
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
orgDAL: Pick<
|
orgDAL: Pick<
|
||||||
TOrgDALFactory,
|
TOrgDALFactory,
|
||||||
@@ -452,6 +455,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
@@ -464,6 +468,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
authMethod: authProvider,
|
authMethod: authProvider,
|
||||||
|
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||||
authType: UserAliasType.SAML,
|
authType: UserAliasType.SAML,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(relayState
|
...(relayState
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const envSchema = z
|
|||||||
DB_USER: zpStr(z.string().describe("Postgres database username").optional()),
|
DB_USER: zpStr(z.string().describe("Postgres database username").optional()),
|
||||||
DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()),
|
DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()),
|
||||||
DB_NAME: zpStr(z.string().describe("Postgres database name").optional()),
|
DB_NAME: zpStr(z.string().describe("Postgres database name").optional()),
|
||||||
|
BCRYPT_SALT_ROUND: z.number().default(12),
|
||||||
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
|
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
|
||||||
SALT_ROUNDS: z.coerce.number().default(10),
|
SALT_ROUNDS: z.coerce.number().default(10),
|
||||||
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
|
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import tweetnacl from "tweetnacl-util";
|
|||||||
|
|
||||||
import { TUserEncryptionKeys } from "@app/db/schemas";
|
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
||||||
|
|
||||||
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
@@ -97,7 +97,13 @@ export const generateUserSrpKeys = async (email: string, password: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
export const getUserPrivateKey = async (
|
||||||
|
password: string,
|
||||||
|
user: Pick<
|
||||||
|
TUserEncryptionKeys,
|
||||||
|
"protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
const derivedKey = await argon2.hash(password, {
|
const derivedKey = await argon2.hash(password, {
|
||||||
salt: Buffer.from(user.salt),
|
salt: Buffer.from(user.salt),
|
||||||
memoryCost: 65536,
|
memoryCost: 65536,
|
||||||
@@ -108,17 +114,18 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
|
|||||||
raw: true
|
raw: true
|
||||||
});
|
});
|
||||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
const key = decryptSymmetric({
|
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: user.protectedKey!,
|
ciphertext: user.protectedKey as string,
|
||||||
iv: user.protectedKeyIV!,
|
iv: user.protectedKeyIV as string,
|
||||||
tag: user.protectedKeyTag!,
|
tag: user.protectedKeyTag as string,
|
||||||
key: derivedKey.toString("base64")
|
key: derivedKey
|
||||||
});
|
});
|
||||||
const privateKey = decryptSymmetric({
|
|
||||||
|
const privateKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: user.encryptedPrivateKey,
|
ciphertext: user.encryptedPrivateKey,
|
||||||
iv: user.iv,
|
iv: user.iv,
|
||||||
tag: user.tag,
|
tag: user.tag,
|
||||||
key
|
key: Buffer.from(key, "hex")
|
||||||
});
|
});
|
||||||
return privateKey;
|
return privateKey;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
email: z.string().email().trim(),
|
email: z.string().email().trim(),
|
||||||
|
password: z.string().trim(),
|
||||||
firstName: z.string().trim(),
|
firstName: z.string().trim(),
|
||||||
lastName: z.string().trim().optional(),
|
lastName: z.string().trim().optional(),
|
||||||
protectedKey: z.string().trim(),
|
protectedKey: z.string().trim(),
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyIV: z.string().trim(),
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim()
|
verifier: z.string().trim(),
|
||||||
|
password: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -259,4 +259,50 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/token-exchange",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
providerAuthToken: z.string(),
|
||||||
|
email: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
|
|
||||||
|
const data = await server.services.login.oauth2TokenExchange({
|
||||||
|
email: req.body.email,
|
||||||
|
ip: req.realIp,
|
||||||
|
userAgent,
|
||||||
|
providerAuthToken: req.body.providerAuthToken
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.isMfaEnabled) {
|
||||||
|
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
|
||||||
|
}
|
||||||
|
|
||||||
|
void 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 || null,
|
||||||
|
protectedKeyIV: data.user.protectedKeyIV || null,
|
||||||
|
protectedKeyTag: data.user.protectedKeyTag || null
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
user: UsersSchema.merge(
|
||||||
|
UserEncryptionKeysSchema.pick({
|
||||||
|
clientPublicKey: true,
|
||||||
|
serverPrivateKey: true,
|
||||||
|
encryptionVersion: true,
|
||||||
|
protectedKey: true,
|
||||||
|
protectedKeyIV: true,
|
||||||
|
protectedKeyTag: true,
|
||||||
|
publicKey: true,
|
||||||
|
encryptedPrivateKey: true,
|
||||||
|
iv: true,
|
||||||
|
tag: true,
|
||||||
|
salt: true,
|
||||||
|
verifier: true,
|
||||||
|
userId: true
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -30,6 +46,26 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/private-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
privateKey: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||||
|
handler: async (req) => {
|
||||||
|
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
|
||||||
|
return { privateKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:userId/unlock",
|
url: "/:userId/unlock",
|
||||||
|
|||||||
@@ -255,7 +255,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Retrieve the current user on the request",
|
description: "Retrieve the current user on the request",
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
user: UsersSchema.merge(
|
||||||
|
UserEncryptionKeysSchema.pick({
|
||||||
|
clientPublicKey: true,
|
||||||
|
serverPrivateKey: true,
|
||||||
|
encryptionVersion: true,
|
||||||
|
protectedKey: true,
|
||||||
|
protectedKeyIV: true,
|
||||||
|
protectedKeyTag: true,
|
||||||
|
publicKey: true,
|
||||||
|
encryptedPrivateKey: true,
|
||||||
|
iv: true,
|
||||||
|
tag: true,
|
||||||
|
salt: true,
|
||||||
|
verifier: true,
|
||||||
|
userId: true
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
email: z.string().trim(),
|
email: z.string().trim(),
|
||||||
providerAuthToken: z.string().trim().optional(),
|
providerAuthToken: z.string().trim().optional(),
|
||||||
clientProof: z.string().trim(),
|
clientProof: z.string().trim(),
|
||||||
captchaToken: z.string().trim().optional()
|
captchaToken: z.string().trim().optional(),
|
||||||
|
password: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.discriminatedUnion("mfaEnabled", [
|
200: z.discriminatedUnion("mfaEnabled", [
|
||||||
@@ -112,7 +113,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent,
|
userAgent,
|
||||||
providerAuthToken: req.body.providerAuthToken,
|
providerAuthToken: req.body.providerAuthToken,
|
||||||
clientProof: req.body.clientProof
|
clientProof: req.body.clientProof,
|
||||||
|
password: req.body.password
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.isMfaEnabled) {
|
if (data.isMfaEnabled) {
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
verifier: z.string().trim(),
|
verifier: z.string().trim(),
|
||||||
organizationName: z.string().trim().min(1),
|
organizationName: z.string().trim().min(1),
|
||||||
providerAuthToken: z.string().trim().optional().nullish(),
|
providerAuthToken: z.string().trim().optional().nullish(),
|
||||||
attributionSource: z.string().trim().optional()
|
attributionSource: z.string().trim().optional(),
|
||||||
|
password: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -167,6 +168,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
email: z.string().email().trim(),
|
email: z.string().email().trim(),
|
||||||
|
password: z.string(),
|
||||||
firstName: z.string().trim(),
|
firstName: z.string().trim(),
|
||||||
lastName: z.string().trim().optional(),
|
lastName: z.string().trim().optional(),
|
||||||
protectedKey: z.string().trim(),
|
protectedKey: z.string().trim(),
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri
|
|||||||
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
|
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
|
||||||
|
|
||||||
if (decodedToken.organizationId) {
|
if (decodedToken.organizationId) {
|
||||||
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod };
|
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod, userName: decodedToken.username };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { authMethod: decodedToken.authMethod, orgId: null };
|
return { authMethod: decodedToken.authMethod, orgId: null, userName: decodedToken.username };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
|
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
@@ -5,6 +6,8 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ import {
|
|||||||
TLoginClientProofDTO,
|
TLoginClientProofDTO,
|
||||||
TLoginGenServerPublicKeyDTO,
|
TLoginGenServerPublicKeyDTO,
|
||||||
TOauthLoginDTO,
|
TOauthLoginDTO,
|
||||||
|
TOauthTokenExchangeDTO,
|
||||||
TVerifyMfaTokenDTO
|
TVerifyMfaTokenDTO
|
||||||
} from "./auth-login-type";
|
} from "./auth-login-type";
|
||||||
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
|
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
|
||||||
@@ -101,7 +105,7 @@ export const authLoginServiceFactory = ({
|
|||||||
user: TUsers;
|
user: TUsers;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
organizationId: string | undefined;
|
organizationId?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
}) => {
|
}) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
@@ -178,7 +182,8 @@ export const authLoginServiceFactory = ({
|
|||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
captchaToken
|
captchaToken,
|
||||||
|
password
|
||||||
}: TLoginClientProofDTO) => {
|
}: TLoginClientProofDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -248,14 +253,29 @@ export const authLoginServiceFactory = ({
|
|||||||
throw new Error("Failed to authenticate. Try again?");
|
throw new Error("Failed to authenticate. Try again?");
|
||||||
}
|
}
|
||||||
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
|
||||||
serverPrivateKey: null,
|
|
||||||
clientPublicKey: null
|
|
||||||
});
|
|
||||||
|
|
||||||
await userDAL.updateById(userEnc.userId, {
|
await userDAL.updateById(userEnc.userId, {
|
||||||
consecutiveFailedPasswordAttempts: 0
|
consecutiveFailedPasswordAttempts: 0
|
||||||
});
|
});
|
||||||
|
// from password decrypt the private key
|
||||||
|
if (password) {
|
||||||
|
const privateKey = await getUserPrivateKey(password, userEnc);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
|
||||||
|
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKey: ciphertext,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// send multi factor auth token if they it enabled
|
// send multi factor auth token if they it enabled
|
||||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||||
@@ -499,8 +519,14 @@ export const authLoginServiceFactory = ({
|
|||||||
authMethods: [authMethod],
|
authMethods: [authMethod],
|
||||||
isGhost: false
|
isGhost: false
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||||
|
if (isLinkingRequired) {
|
||||||
|
user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
const isUserCompleted = user.isAccepted;
|
const isUserCompleted = user.isAccepted;
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
{
|
{
|
||||||
@@ -511,9 +537,9 @@ export const authLoginServiceFactory = ({
|
|||||||
isEmailVerified: user.isEmailVerified,
|
isEmailVerified: user.isEmailVerified,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
|
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||||
authMethod,
|
authMethod,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
isLinkingRequired,
|
|
||||||
...(callbackPort
|
...(callbackPort
|
||||||
? {
|
? {
|
||||||
callbackPort
|
callbackPort
|
||||||
@@ -525,10 +551,71 @@ export const authLoginServiceFactory = ({
|
|||||||
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { isUserCompleted, providerAuthToken };
|
return { isUserCompleted, providerAuthToken };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles OAuth2 token exchange for user login with private key handoff.
|
||||||
|
*
|
||||||
|
* The process involves exchanging a provider's authorization token for an Infisical access token.
|
||||||
|
* The provider token is returned to the client, who then sends it back to obtain the Infisical access token.
|
||||||
|
*
|
||||||
|
* This approach is used instead of directly sending the access token for the following reasons:
|
||||||
|
* 1. To facilitate easier logic changes from SRP OAuth to simple OAuth.
|
||||||
|
* 2. To avoid attaching the access token to the URL, which could be logged. The provider token has a very short lifespan, reducing security risks.
|
||||||
|
*/
|
||||||
|
const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => {
|
||||||
|
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const { authMethod, userName } = decodedProviderToken;
|
||||||
|
if (!userName) throw new BadRequestError({ message: "Missing user name" });
|
||||||
|
const organizationId =
|
||||||
|
(isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId
|
||||||
|
? decodedProviderToken.orgId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||||
|
username: email
|
||||||
|
});
|
||||||
|
if (!userEnc) throw new BadRequestError({ message: "Invalid token" });
|
||||||
|
if (!userEnc.serverEncryptedPrivateKey)
|
||||||
|
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
|
||||||
|
// send multi factor auth token if they it enabled
|
||||||
|
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||||
|
enforceUserLockStatus(Boolean(userEnc.isLocked), userEnc.temporaryLockDateEnd);
|
||||||
|
|
||||||
|
const mfaToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authMethod,
|
||||||
|
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||||
|
userId: userEnc.userId
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: appCfg.JWT_MFA_LIFETIME
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendUserMfaCode({
|
||||||
|
userId: userEnc.userId,
|
||||||
|
email: userEnc.email
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isMfaEnabled: true, token: mfaToken } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await generateUserTokens({
|
||||||
|
user: { ...userEnc, id: userEnc.userId },
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
authMethod,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { token, isMfaEnabled: false, user: userEnc } as const;
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* logout user by incrementing the version by 1 meaning any old session will become invalid
|
* logout user by incrementing the version by 1 meaning any old session will become invalid
|
||||||
* as there number is behind
|
* as there number is behind
|
||||||
@@ -542,6 +629,7 @@ export const authLoginServiceFactory = ({
|
|||||||
loginExchangeClientProof,
|
loginExchangeClientProof,
|
||||||
logout,
|
logout,
|
||||||
oauth2Login,
|
oauth2Login,
|
||||||
|
oauth2TokenExchange,
|
||||||
resendMfaToken,
|
resendMfaToken,
|
||||||
verifyMfaToken,
|
verifyMfaToken,
|
||||||
selectOrganization,
|
selectOrganization,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type TLoginClientProofDTO = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
captchaToken?: string;
|
captchaToken?: string;
|
||||||
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyMfaTokenDTO = {
|
export type TVerifyMfaTokenDTO = {
|
||||||
@@ -31,3 +32,10 @@ export type TOauthLoginDTO = {
|
|||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
callbackPort?: string;
|
callbackPort?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TOauthTokenExchangeDTO = {
|
||||||
|
providerAuthToken: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
@@ -57,7 +58,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
tokenVersionId
|
tokenVersionId,
|
||||||
|
password
|
||||||
}: TChangePasswordDTO) => {
|
}: TChangePasswordDTO) => {
|
||||||
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!userEnc) throw new Error("Failed to find user");
|
if (!userEnc) throw new Error("Failed to find user");
|
||||||
@@ -76,6 +78,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
||||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@@ -87,7 +91,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
serverPrivateKey: null,
|
serverPrivateKey: null,
|
||||||
clientPublicKey: null
|
clientPublicKey: null,
|
||||||
|
hashedPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tokenVersionId) {
|
if (tokenVersionId) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type TChangePasswordDTO = {
|
|||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
tokenVersionId?: string;
|
tokenVersionId?: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TResetPasswordViaBackupKeyDTO = {
|
export type TResetPasswordViaBackupKeyDTO = {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||||
@@ -6,6 +7,8 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
@@ -119,6 +122,7 @@ export const authSignupServiceFactory = ({
|
|||||||
|
|
||||||
const completeEmailAccountSignup = async ({
|
const completeEmailAccountSignup = async ({
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
@@ -137,6 +141,7 @@ export const authSignupServiceFactory = ({
|
|||||||
userAgent,
|
userAgent,
|
||||||
authorization
|
authorization
|
||||||
}: TCompleteAccountSignupDTO) => {
|
}: TCompleteAccountSignupDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const user = await userDAL.findOne({ username: email });
|
const user = await userDAL.findOne({ username: email });
|
||||||
if (!user || (user && user.isAccepted)) {
|
if (!user || (user && user.isAccepted)) {
|
||||||
throw new Error("Failed to complete account for complete user");
|
throw new Error("Failed to complete account for complete user");
|
||||||
@@ -152,6 +157,17 @@ export const authSignupServiceFactory = ({
|
|||||||
validateSignUpAuthorization(authorization, user.id);
|
validateSignUpAuthorization(authorization, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
||||||
|
const privateKey = await getUserPrivateKey(password, {
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
});
|
||||||
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
||||||
const updateduser = await authDAL.transaction(async (tx) => {
|
const updateduser = await authDAL.transaction(async (tx) => {
|
||||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||||
if (!us) throw new Error("User not found");
|
if (!us) throw new Error("User not found");
|
||||||
@@ -166,7 +182,12 @@ export const authSignupServiceFactory = ({
|
|||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag
|
tag: encryptedPrivateKeyTag,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -227,7 +248,6 @@ export const authSignupServiceFactory = ({
|
|||||||
userId: updateduser.info.id
|
userId: updateduser.info.id
|
||||||
});
|
});
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
{
|
{
|
||||||
@@ -265,6 +285,7 @@ export const authSignupServiceFactory = ({
|
|||||||
ip,
|
ip,
|
||||||
salt,
|
salt,
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
verifier,
|
verifier,
|
||||||
firstName,
|
firstName,
|
||||||
publicKey,
|
publicKey,
|
||||||
@@ -295,6 +316,18 @@ export const authSignupServiceFactory = ({
|
|||||||
name: "complete account invite"
|
name: "complete account invite"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
||||||
|
const privateKey = await getUserPrivateKey(password, {
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
});
|
||||||
|
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
||||||
const updateduser = await authDAL.transaction(async (tx) => {
|
const updateduser = await authDAL.transaction(async (tx) => {
|
||||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||||
if (!us) throw new Error("User not found");
|
if (!us) throw new Error("User not found");
|
||||||
@@ -310,7 +343,12 @@ export const authSignupServiceFactory = ({
|
|||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag
|
tag: encryptedPrivateKeyTag,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKey: ciphertext
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -343,7 +381,6 @@ export const authSignupServiceFactory = ({
|
|||||||
userId: updateduser.info.id
|
userId: updateduser.info.id
|
||||||
});
|
});
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type TCompleteAccountSignupDTO = {
|
export type TCompleteAccountSignupDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
@@ -21,6 +22,7 @@ export type TCompleteAccountSignupDTO = {
|
|||||||
|
|
||||||
export type TCompleteAccountInviteDTO = {
|
export type TCompleteAccountInviteDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||||
@@ -77,6 +81,7 @@ export const superAdminServiceFactory = ({
|
|||||||
firstName,
|
firstName,
|
||||||
salt,
|
salt,
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
verifier,
|
verifier,
|
||||||
publicKey,
|
publicKey,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@@ -92,6 +97,17 @@ export const superAdminServiceFactory = ({
|
|||||||
const existingUser = await userDAL.findOne({ email });
|
const existingUser = await userDAL.findOne({ email });
|
||||||
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
|
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
|
||||||
|
|
||||||
|
const privateKey = await getUserPrivateKey(password, {
|
||||||
|
salt,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
});
|
||||||
|
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
|
||||||
|
const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
|
||||||
const userInfo = await userDAL.transaction(async (tx) => {
|
const userInfo = await userDAL.transaction(async (tx) => {
|
||||||
const newUser = await userDAL.create(
|
const newUser = await userDAL.create(
|
||||||
{
|
{
|
||||||
@@ -119,7 +135,12 @@ export const superAdminServiceFactory = ({
|
|||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
verifier,
|
verifier,
|
||||||
userId: newUser.id
|
userId: newUser.id,
|
||||||
|
hashedPassword,
|
||||||
|
serverEncryptedPrivateKey: ciphertext,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type TAdminSignUpDTO = {
|
export type TAdminSignUpDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
@@ -230,6 +232,21 @@ export const userServiceFactory = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUserPrivateKey = async (userId: string) => {
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
|
if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) {
|
||||||
|
throw new BadRequestError({ message: "Private key not found. Please login again" });
|
||||||
|
}
|
||||||
|
const privateKey = infisicalSymmetricDecrypt({
|
||||||
|
ciphertext: user.serverEncryptedPrivateKey,
|
||||||
|
tag: user.serverEncryptedPrivateKeyTag,
|
||||||
|
iv: user.serverEncryptedPrivateKeyIV,
|
||||||
|
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
||||||
|
});
|
||||||
|
|
||||||
|
return privateKey;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
@@ -240,6 +257,7 @@ export const userServiceFactory = ({
|
|||||||
getMe,
|
getMe,
|
||||||
createUserAction,
|
createUserAction,
|
||||||
getUserAction,
|
getUserAction,
|
||||||
unlockUser
|
unlockUser,
|
||||||
|
getUserPrivateKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ type GetLoginOneV2Response struct {
|
|||||||
type GetLoginTwoV2Request struct {
|
type GetLoginTwoV2Request struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
ClientProof string `json:"clientProof"`
|
ClientProof string `json:"clientProof"`
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetLoginTwoV2Response struct {
|
type GetLoginTwoV2Response struct {
|
||||||
|
|||||||
@@ -539,6 +539,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R
|
|||||||
loginTwoResponseResult, err := api.CallLogin2V2(httpClient, api.GetLoginTwoV2Request{
|
loginTwoResponseResult, err := api.CallLogin2V2(httpClient, api.GetLoginTwoV2Request{
|
||||||
Email: email,
|
Email: email,
|
||||||
ClientProof: hex.EncodeToString(srpM1),
|
ClientProof: hex.EncodeToString(srpM1),
|
||||||
|
Password: password,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Generated
+726
-205
File diff suppressed because it is too large
Load Diff
@@ -161,6 +161,7 @@ export default function UserInfoStep({
|
|||||||
|
|
||||||
const response = await completeAccountSignup({
|
const response = await completeAccountSignup({
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
firstName: name.split(" ")[0],
|
firstName: name.split(" ")[0],
|
||||||
lastName: name.split(" ").slice(1).join(" "),
|
lastName: name.split(" ").slice(1).join(" "),
|
||||||
protectedKey,
|
protectedKey,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
|
|||||||
});
|
});
|
||||||
|
|
||||||
await changePassword({
|
await changePassword({
|
||||||
|
password: newPassword,
|
||||||
clientProof,
|
clientProof,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const attemptLogin = async ({
|
|||||||
tag
|
tag
|
||||||
} = await login2({
|
} = await login2({
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
clientProof,
|
clientProof,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
captchaToken
|
captchaToken
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const attemptLogin = async ({
|
|||||||
} = await login2({
|
} = await login2({
|
||||||
captchaToken,
|
captchaToken,
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
clientProof,
|
clientProof,
|
||||||
providerAuthToken
|
providerAuthToken
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type TServerConfig = {
|
|||||||
|
|
||||||
export type TCreateAdminUserDTO = {
|
export type TCreateAdminUserDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
useGetAuthToken,
|
useGetAuthToken,
|
||||||
|
useOauthTokenExchange,
|
||||||
useResetPassword,
|
useResetPassword,
|
||||||
useSelectOrganization,
|
useSelectOrganization,
|
||||||
useSendMfaToken,
|
useSendMfaToken,
|
||||||
@@ -7,4 +8,5 @@ export {
|
|||||||
useSendVerificationEmail,
|
useSendVerificationEmail,
|
||||||
useVerifyMfaToken,
|
useVerifyMfaToken,
|
||||||
useVerifyPasswordResetCode,
|
useVerifyPasswordResetCode,
|
||||||
useVerifySignupEmailVerificationCode} from "./queries";
|
useVerifySignupEmailVerificationCode
|
||||||
|
} from "./queries";
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
SendMfaTokenDTO,
|
SendMfaTokenDTO,
|
||||||
SRP1DTO,
|
SRP1DTO,
|
||||||
SRPR1Res,
|
SRPR1Res,
|
||||||
|
TOauthTokenExchangeDTO,
|
||||||
VerifyMfaTokenDTO,
|
VerifyMfaTokenDTO,
|
||||||
VerifyMfaTokenRes,
|
VerifyMfaTokenRes,
|
||||||
VerifySignupInviteDTO
|
VerifySignupInviteDTO
|
||||||
@@ -92,6 +93,7 @@ export const useLogin2 = () => {
|
|||||||
mutationFn: async (details: {
|
mutationFn: async (details: {
|
||||||
email: string;
|
email: string;
|
||||||
clientProof: string;
|
clientProof: string;
|
||||||
|
password: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return login2(details);
|
return login2(details);
|
||||||
@@ -99,6 +101,20 @@ export const useLogin2 = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const oauthTokenExchange = async (details: TOauthTokenExchangeDTO) => {
|
||||||
|
const { data } = await apiRequest.post<Login2Res>("/api/v1/sso/token-exchange", details);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOauthTokenExchange = () => {
|
||||||
|
// note: use after srp1
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (details: TOauthTokenExchangeDTO) => {
|
||||||
|
return oauthTokenExchange(details);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const srp1 = async (details: SRP1DTO) => {
|
export const srp1 = async (details: SRP1DTO) => {
|
||||||
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
|
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export type VerifyMfaTokenRes = {
|
|||||||
tag: string;
|
tag: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TOauthTokenExchangeDTO = {
|
||||||
|
providerAuthToken: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Login1DTO = {
|
export type Login1DTO = {
|
||||||
email: string;
|
email: string;
|
||||||
clientPublicKey: string;
|
clientPublicKey: string;
|
||||||
@@ -34,6 +39,7 @@ export type Login2DTO = {
|
|||||||
email: string;
|
email: string;
|
||||||
clientProof: string;
|
clientProof: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Login1Res = {
|
export type Login1Res = {
|
||||||
@@ -86,6 +92,7 @@ export type CompleteAccountDTO = {
|
|||||||
encryptedPrivateKeyTag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CompleteAccountSignupDTO = CompleteAccountDTO & {
|
export type CompleteAccountSignupDTO = CompleteAccountDTO & {
|
||||||
@@ -101,6 +108,7 @@ export type VerifySignupInviteDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ChangePasswordDTO = {
|
export type ChangePasswordDTO = {
|
||||||
|
password: string;
|
||||||
clientProof: string;
|
clientProof: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
protectedKeyIV: string;
|
protectedKeyIV: string;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
|
|
||||||
export const userKeys = {
|
export const userKeys = {
|
||||||
getUser: ["user"] as const,
|
getUser: ["user"] as const,
|
||||||
|
getPrivateKey: ["user"] as const,
|
||||||
userAction: ["user-action"] as const,
|
userAction: ["user-action"] as const,
|
||||||
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
||||||
myIp: ["ip"] as const,
|
myIp: ["ip"] as const,
|
||||||
@@ -351,3 +352,11 @@ export const useGetMyOrganizationProjects = (orgId: string) => {
|
|||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchMyPrivateKey = async () => {
|
||||||
|
const {
|
||||||
|
data: { privateKey }
|
||||||
|
} = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key");
|
||||||
|
|
||||||
|
return privateKey;
|
||||||
|
};
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export default function SignupInvite() {
|
|||||||
|
|
||||||
const { token: jwtToken } = await completeAccountSignupInvite({
|
const { token: jwtToken } = await completeAccountSignupInvite({
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import Error from "@app/components/basic/Error";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa";
|
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa";
|
||||||
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
|
||||||
import { useSendMfaToken } from "@app/hooks/api/auth";
|
import { useSendMfaToken } from "@app/hooks/api/auth";
|
||||||
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { useSelectOrganization, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
|
||||||
|
|
||||||
import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils";
|
import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils";
|
||||||
|
|
||||||
@@ -56,15 +55,59 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const sendMfaToken = useSendMfaToken();
|
const sendMfaToken = useSendMfaToken();
|
||||||
const { mutateAsync: updateUserAuthMethodsMutateAsync } = useUpdateUserAuthMethods();
|
|
||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
|
|
||||||
|
// They don't have password
|
||||||
|
const handleLoginMfaOauth = async (callbackPort: string, organizationId?: string) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const { token } = await verifyMfaToken({
|
||||||
|
email,
|
||||||
|
mfaCode
|
||||||
|
});
|
||||||
|
//
|
||||||
|
// unset temporary (MFA) JWT token and set JWT token
|
||||||
|
SecurityClient.setMfaToken("");
|
||||||
|
SecurityClient.setToken(token);
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
const privateKey = await fetchMyPrivateKey();
|
||||||
|
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||||
|
|
||||||
|
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||||
|
if (organizationId) {
|
||||||
|
const { token: newJwtToken } = await selectOrganization({ organizationId });
|
||||||
|
if (callbackPort) {
|
||||||
|
const cliUrl = `http://127.0.0.1:${callbackPort}/`;
|
||||||
|
const instance = axios.create();
|
||||||
|
await instance.post(cliUrl, {
|
||||||
|
email,
|
||||||
|
privateKey,
|
||||||
|
JTWToken: newJwtToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await navigateUserToOrg(router, organizationId);
|
||||||
|
}
|
||||||
|
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
|
||||||
|
// if the user has no orgs, navigate to the create org page
|
||||||
|
else {
|
||||||
|
const userOrgs = await fetchOrganizations();
|
||||||
|
|
||||||
|
// case: user has orgs, so we navigate the user to select an org
|
||||||
|
if (userOrgs.length > 0) {
|
||||||
|
navigateUserToSelectOrg(router, callbackPort);
|
||||||
|
}
|
||||||
|
// case: no orgs found, so we navigate the user to create an org
|
||||||
|
// cli login will fail in this case
|
||||||
|
else {
|
||||||
|
await navigateUserToOrg(router);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLoginMfa = async () => {
|
const handleLoginMfa = async () => {
|
||||||
try {
|
try {
|
||||||
let isLinkingRequired: undefined | boolean;
|
|
||||||
let callbackPort: undefined | string;
|
let callbackPort: undefined | string;
|
||||||
let authMethod: undefined | AuthMethod;
|
|
||||||
let organizationId: undefined | string;
|
let organizationId: undefined | string;
|
||||||
|
let hasExchangedPrivateKey: undefined | boolean;
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
@@ -73,10 +116,9 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
|||||||
if (providerAuthToken) {
|
if (providerAuthToken) {
|
||||||
const decodedToken = jwt_decode(providerAuthToken) as any;
|
const decodedToken = jwt_decode(providerAuthToken) as any;
|
||||||
|
|
||||||
isLinkingRequired = decodedToken.isLinkingRequired;
|
|
||||||
callbackPort = decodedToken.callbackPort;
|
callbackPort = decodedToken.callbackPort;
|
||||||
authMethod = decodedToken.authMethod;
|
|
||||||
organizationId = decodedToken?.organizationId;
|
organizationId = decodedToken?.organizationId;
|
||||||
|
hasExchangedPrivateKey = decodedToken?.hasExchangedPrivateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mfaCode.length !== 6) {
|
if (mfaCode.length !== 6) {
|
||||||
@@ -87,6 +129,11 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasExchangedPrivateKey) {
|
||||||
|
await handleLoginMfaOauth(callbackPort as string, organizationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (callbackPort) {
|
if (callbackPort) {
|
||||||
// attemptCliLogin
|
// attemptCliLogin
|
||||||
@@ -145,14 +192,6 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
|||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLinkingRequired && authMethod) {
|
|
||||||
const user = await fetchUserDetails();
|
|
||||||
const newAuthMethods = [...user.authMethods, authMethod];
|
|
||||||
await updateUserAuthMethodsMutateAsync({
|
|
||||||
authMethods: newAuthMethods
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
await navigateUserToOrg(router, organizationId);
|
await navigateUserToOrg(router, organizationId);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef,useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -10,11 +10,11 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||||
import { Button, Input } from "@app/components/v2";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
import { Button, Input, Spinner } from "@app/components/v2";
|
||||||
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
|
||||||
|
|
||||||
import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils";
|
import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils";
|
||||||
|
|
||||||
@@ -36,12 +36,94 @@ export const PasswordStep = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useUpdateUserAuthMethods();
|
|
||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
|
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
|
||||||
|
|
||||||
const { callbackPort, isLinkingRequired, authMethod, organizationId } = jwt_decode(
|
const { callbackPort, organizationId, hasExchangedPrivateKey } =
|
||||||
providerAuthToken
|
jwt_decode(providerAuthToken) as any;
|
||||||
) as any;
|
|
||||||
|
const handleExchange = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const oauthLogin = await oauthTokenExchange({
|
||||||
|
email,
|
||||||
|
providerAuthToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// attemptCliLogin
|
||||||
|
if (oauthLogin.mfaEnabled) {
|
||||||
|
SecurityClient.setMfaToken(oauthLogin.token);
|
||||||
|
// case: login requires MFA step
|
||||||
|
setStep(2);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cliUrl = `http://127.0.0.1:${callbackPort}/`;
|
||||||
|
|
||||||
|
// case: MFA is not enabled
|
||||||
|
|
||||||
|
// unset provider auth token in case it was used
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
// set JWT token
|
||||||
|
SecurityClient.setToken(oauthLogin.token);
|
||||||
|
|
||||||
|
const privateKey = await fetchMyPrivateKey();
|
||||||
|
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||||
|
|
||||||
|
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||||
|
if (organizationId) {
|
||||||
|
const { token: newJwtToken } = await selectOrganization({ organizationId });
|
||||||
|
if (callbackPort) {
|
||||||
|
console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken);
|
||||||
|
const instance = axios.create();
|
||||||
|
await instance.post(cliUrl, {
|
||||||
|
privateKey,
|
||||||
|
email,
|
||||||
|
JTWToken: newJwtToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigateUserToOrg(router, organizationId);
|
||||||
|
}
|
||||||
|
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
|
||||||
|
// if the user has no orgs, navigate to the create org page
|
||||||
|
else {
|
||||||
|
const userOrgs = await fetchOrganizations();
|
||||||
|
|
||||||
|
// case: user has orgs, so we navigate the user to select an org
|
||||||
|
if (userOrgs.length > 0) {
|
||||||
|
navigateUserToSelectOrg(router, callbackPort);
|
||||||
|
}
|
||||||
|
// case: no orgs found, so we navigate the user to create an org
|
||||||
|
else {
|
||||||
|
await navigateUserToOrg(router);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setIsLoading(false);
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
if (err.response.data.error === "User Locked") {
|
||||||
|
createNotification({
|
||||||
|
title: err.response.data.error,
|
||||||
|
text: err.response.data.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasExchangedPrivateKey) {
|
||||||
|
handleExchange();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [captchaToken, setCaptchaToken] = useState("");
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||||
@@ -128,14 +210,6 @@ export const PasswordStep = ({
|
|||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLinkingRequired) {
|
|
||||||
const user = await fetchUserDetails();
|
|
||||||
const newAuthMethods = [...user.authMethods, authMethod];
|
|
||||||
await mutateAsync({
|
|
||||||
authMethods: newAuthMethods
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// case: organization ID is present from the provider auth token -- navigate directly to the org
|
// case: organization ID is present from the provider auth token -- navigate directly to the org
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
await navigateUserToOrg(router, organizationId);
|
await navigateUserToOrg(router, organizationId);
|
||||||
@@ -183,20 +257,21 @@ export const PasswordStep = ({
|
|||||||
setCaptchaToken("");
|
setCaptchaToken("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (hasExchangedPrivateKey) {
|
||||||
|
return (
|
||||||
|
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||||
|
<Spinner />
|
||||||
|
<p className="text-white opacity-80">Loading, please wait</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleLogin} className="mx-auto h-full w-full max-w-md px-6 pt-8">
|
<form onSubmit={handleLogin} className="mx-auto h-full w-full max-w-md px-6 pt-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<p className="mx-auto mb-4 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
<p className="mx-auto mb-4 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||||
{isLinkingRequired ? "Link your account" : "What's your Infisical password?"}
|
What's your Infisical password?
|
||||||
</p>
|
</p>
|
||||||
{isLinkingRequired && (
|
|
||||||
<div className="mx-auto flex w-max flex-col items-center text-xs text-bunker-400">
|
|
||||||
<span className="max-w-sm px-4 text-center duration-200">
|
|
||||||
An existing account without this SSO authentication method enabled was found under the
|
|
||||||
same email. Login with your password to link the account.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="relative mx-auto flex max-h-24 w-1/4 w-full min-w-[22rem] items-center justify-center rounded-lg md:max-h-28 lg:w-1/6">
|
<div className="relative mx-auto flex max-h-24 w-1/4 w-full min-w-[22rem] items-center justify-center rounded-lg md:max-h-28 lg:w-1/6">
|
||||||
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
|
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ import crypto from "crypto";
|
|||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faInfoCircle, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
import nacl from "tweetnacl";
|
import nacl from "tweetnacl";
|
||||||
import { encodeBase64 } from "tweetnacl-util";
|
import { encodeBase64 } from "tweetnacl-util";
|
||||||
|
|
||||||
import InputField from "@app/components/basic/InputField";
|
|
||||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
|
||||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||||
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||||
@@ -32,17 +28,6 @@ type Props = {
|
|||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Errors = {
|
|
||||||
tooShort?: string;
|
|
||||||
tooLong?: string;
|
|
||||||
noLetterChar?: string;
|
|
||||||
noNumOrSpecialChar?: string;
|
|
||||||
repeatedChar?: string;
|
|
||||||
escapeChar?: string;
|
|
||||||
lowEntropy?: string;
|
|
||||||
breached?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the step of the sign up flow where people provife their name/surname and password
|
* This is the step of the sign up flow where people provife their name/surname and password
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
@@ -69,12 +54,13 @@ export const UserInfoSSOStep = ({
|
|||||||
const [organizationName, setOrganizationName] = useState("");
|
const [organizationName, setOrganizationName] = useState("");
|
||||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||||
const [attributionSource, setAttributionSource] = useState("");
|
const [attributionSource, setAttributionSource] = useState("");
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const randomPassword = crypto.randomBytes(32).toString("hex");
|
||||||
|
setPassword(randomPassword);
|
||||||
if (providerOrganizationName !== undefined) {
|
if (providerOrganizationName !== undefined) {
|
||||||
setOrganizationName(providerOrganizationName);
|
setOrganizationName(providerOrganizationName);
|
||||||
}
|
}
|
||||||
@@ -98,11 +84,6 @@ export const UserInfoSSOStep = ({
|
|||||||
setOrganizationNameError(false);
|
setOrganizationNameError(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorCheck = await checkPassword({
|
|
||||||
password,
|
|
||||||
setErrors
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!errorCheck) {
|
if (!errorCheck) {
|
||||||
// Generate a random pair of a public and a private key
|
// Generate a random pair of a public and a private key
|
||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
@@ -158,6 +139,7 @@ export const UserInfoSSOStep = ({
|
|||||||
|
|
||||||
const response = await completeAccountSignup({
|
const response = await completeAccountSignup({
|
||||||
email: username,
|
email: username,
|
||||||
|
password,
|
||||||
firstName: name.split(" ")[0],
|
firstName: name.split(" ")[0],
|
||||||
lastName: name.split(" ").slice(1).join(" "),
|
lastName: name.split(" ").slice(1).join(" "),
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@@ -214,6 +196,12 @@ export const UserInfoSSOStep = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (password && providerOrganizationName) {
|
||||||
|
signupErrorCheck();
|
||||||
|
}
|
||||||
|
}, [providerOrganizationName, password]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mb-36 h-full w-max rounded-xl md:mb-16 md:px-8">
|
<div className="mx-auto mb-36 h-full w-max rounded-xl md:mb-16 md:px-8">
|
||||||
<p className="text-medium mx-8 mb-6 flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-xl font-bold text-transparent md:mx-16">
|
<p className="text-medium mx-8 mb-6 flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-xl font-bold text-transparent md:mx-16">
|
||||||
@@ -272,53 +260,6 @@ export const UserInfoSSOStep = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 flex max-h-60 w-1/4 w-full min-w-[20rem] flex-col items-center justify-center rounded-lg py-2 lg:w-1/6">
|
|
||||||
<InputField
|
|
||||||
label="Infisical Password"
|
|
||||||
onChangeHandler={async (pass: string) => {
|
|
||||||
setPassword(pass);
|
|
||||||
await checkPassword({
|
|
||||||
password: pass,
|
|
||||||
setErrors
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
isRequired
|
|
||||||
error={Object.keys(errors).length > 0}
|
|
||||||
autoComplete="new-password"
|
|
||||||
id="new-password"
|
|
||||||
/>
|
|
||||||
<div className="mt-2 max-h-60 w-min min-w-[20rem] flex-col items-center justify-center rounded-md bg-mineshaft-500 p-1.5 px-1.5 text-xs text-mineshaft-300">
|
|
||||||
<FontAwesomeIcon icon={faInfoCircle} className="mr-1.5" />
|
|
||||||
Infisical Password is used as part of the encryption mechanism so that even the
|
|
||||||
authentication provider is not able to access your secrets.
|
|
||||||
</div>
|
|
||||||
{Object.keys(errors).length > 0 && (
|
|
||||||
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
|
||||||
<div className="mb-2 text-sm text-gray-400">
|
|
||||||
{t("section.password.validate-base")}
|
|
||||||
</div>
|
|
||||||
{Object.keys(errors).map((key) => {
|
|
||||||
if (errors[key as keyof Errors]) {
|
|
||||||
return (
|
|
||||||
<div className="items-top ml-1 flex flex-row justify-start" key={key}>
|
|
||||||
<div>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faXmark}
|
|
||||||
className="text-md ml-0.5 mr-2.5 text-red"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||||
<div className="text-l w-full py-1 text-lg">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
<Button
|
<Button
|
||||||
@@ -330,6 +271,7 @@ export const UserInfoSSOStep = ({
|
|||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading}
|
||||||
>
|
>
|
||||||
{" "}
|
{" "}
|
||||||
{String(t("signup.signup"))}{" "}
|
{String(t("signup.signup"))}{" "}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export const SignUpPage = () => {
|
|||||||
const { privateKey, ...userPass } = await generateUserPassKey(email, password);
|
const { privateKey, ...userPass } = await generateUserPassKey(email, password);
|
||||||
const res = await createAdminUser({
|
const res = await createAdminUser({
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
...userPass
|
...userPass
|
||||||
|
|||||||
Reference in New Issue
Block a user