mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 14:27:59 +00:00
Merge pull request #769 from Infisical/saml
Add Google SSO and SAML SSO (Okta)
This commit is contained in:
@@ -24,7 +24,6 @@
|
|||||||
],
|
],
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"unused-imports/no-unused-imports": "error",
|
"unused-imports/no-unused-imports": "error",
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"unused-imports/no-unused-vars": [
|
"unused-imports/no-unused-vars": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+1834
-2147
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||||
"@godaddy/terminus": "^4.12.0",
|
"@godaddy/terminus": "^4.12.0",
|
||||||
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^19.0.5",
|
"@octokit/rest": "^19.0.5",
|
||||||
"@sentry/node": "^7.49.0",
|
"@sentry/node": "^7.49.0",
|
||||||
"@sentry/tracing": "^7.48.0",
|
"@sentry/tracing": "^7.48.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { Integration } from "../../models";
|
import { Integration } from "../../models";
|
||||||
import { EventService } from "../../services";
|
import { EventService } from "../../services";
|
||||||
import { eventPushSecrets, eventStartIntegration } from "../../events";
|
import { eventStartIntegration } from "../../events";
|
||||||
import Folder from "../../models/folder";
|
import Folder from "../../models/folder";
|
||||||
import { getFolderByPath } from "../../services/FolderService";
|
import { getFolderByPath } from "../../services/FolderService";
|
||||||
import { BadRequestError } from "../../utils/errors";
|
import { BadRequestError } from "../../utils/errors";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { createOrganization as create } from "../../helpers/organization";
|
import { createOrganization as create } from "../../helpers/organization";
|
||||||
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
||||||
import { ACCEPTED, OWNER } from "../../variables";
|
import { ACCEPTED, OWNER } from "../../variables";
|
||||||
import { getSiteURL, getLicenseServerUrl } from "../../config";
|
import { getLicenseServerUrl, getSiteURL } from "../../config";
|
||||||
import { licenseServerKeyRequest } from "../../config/request";
|
import { licenseServerKeyRequest } from "../../config/request";
|
||||||
|
|
||||||
export const getOrganizations = async (req: Request, res: Response) => {
|
export const getOrganizations = async (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
} from "../../types/secret";
|
} from "../../types/secret";
|
||||||
const { ValidationError } = mongoose.Error;
|
const { ValidationError } = mongoose.Error;
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
|
||||||
InternalServerError,
|
|
||||||
ValidationError as RouteValidationError,
|
ValidationError as RouteValidationError,
|
||||||
UnauthorizedRequestError
|
UnauthorizedRequestError
|
||||||
} from "../../utils/errors";
|
} from "../../utils/errors";
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { ServiceAccount, ServiceTokenData, User } from "../../models";
|
|||||||
import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
|
import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
|
||||||
import { getSaltRounds } from "../../config";
|
import { getSaltRounds } from "../../config";
|
||||||
import { BadRequestError } from "../../utils/errors";
|
import { BadRequestError } from "../../utils/errors";
|
||||||
import Folder from "../../models/folder";
|
|
||||||
import { getFolderByPath } from "../../services/FolderService";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return service token data associated with service token on request
|
* Return service token data associated with service token on request
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||||
let user, token, refreshToken;
|
let user;
|
||||||
const {
|
const {
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
@@ -119,7 +119,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
userAgent: req.headers["user-agent"] ?? "",
|
userAgent: req.headers["user-agent"] ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
token = tokens.token;
|
const token = tokens.token;
|
||||||
|
|
||||||
// sending a welcome email to new users
|
// sending a welcome email to new users
|
||||||
if (await getLoopsApiKey()) {
|
if (await getLoopsApiKey()) {
|
||||||
@@ -159,7 +159,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||||
let user, token, refreshToken;
|
let user;
|
||||||
const {
|
const {
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
@@ -244,7 +244,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
|
|||||||
userAgent: req.headers["user-agent"] ?? "",
|
userAgent: req.headers["user-agent"] ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
token = tokens.token;
|
const token = tokens.token;
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie("jid", tokens.refreshToken, {
|
res.cookie("jid", tokens.refreshToken, {
|
||||||
|
|||||||
@@ -179,10 +179,9 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let key;
|
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
key = await Key.findOne({
|
const key = await Key.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
receiver: req.user._id
|
receiver: req.user._id
|
||||||
}).populate("sender", "+publicKey");
|
}).populate("sender", "+publicKey");
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { standardRequest } from "../../config/request";
|
|||||||
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
|
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
|
||||||
import { BadRequestError } from "../../utils/errors";
|
import { BadRequestError } from "../../utils/errors";
|
||||||
import { TelemetryService } from "../../services";
|
import { TelemetryService } from "../../services";
|
||||||
|
import { AuthProvider } from "../../models";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
@@ -116,11 +117,13 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
||||||
|
|
||||||
// initialize default organization and workspace
|
if (user.authProvider !== AuthProvider.OKTA_SAML) {
|
||||||
await initializeDefaultOrg({
|
// initialize default organization and workspace
|
||||||
organizationName,
|
await initializeDefaultOrg({
|
||||||
user,
|
organizationName,
|
||||||
});
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// update organization membership statuses that are
|
// update organization membership statuses that are
|
||||||
// invited to completed with user attached
|
// invited to completed with user attached
|
||||||
@@ -174,7 +177,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
distinctId: email,
|
distinctId: email,
|
||||||
properties: {
|
properties: {
|
||||||
email,
|
email,
|
||||||
attributionSource,
|
...(attributionSource ? { attributionSource } : {})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as secretController from "./secretController";
|
import * as secretController from "./secretController";
|
||||||
import * as secretSnapshotController from "./secretSnapshotController";
|
import * as secretSnapshotController from "./secretSnapshotController";
|
||||||
import * as organizationsController from "./organizationsController";
|
import * as organizationsController from "./organizationsController";
|
||||||
|
import * as ssoController from "./ssoController";
|
||||||
import * as workspaceController from "./workspaceController";
|
import * as workspaceController from "./workspaceController";
|
||||||
import * as actionController from "./actionController";
|
import * as actionController from "./actionController";
|
||||||
import * as membershipController from "./membershipController";
|
import * as membershipController from "./membershipController";
|
||||||
@@ -10,6 +11,7 @@ export {
|
|||||||
secretController,
|
secretController,
|
||||||
secretSnapshotController,
|
secretSnapshotController,
|
||||||
organizationsController,
|
organizationsController,
|
||||||
|
ssoController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
actionController,
|
actionController,
|
||||||
membershipController,
|
membershipController,
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete tax id with id [taxId] from organization tax ids on file
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
|
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
|
||||||
const { taxId } = req.params;
|
const { taxId } = req.params;
|
||||||
|
|
||||||
@@ -188,6 +194,12 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organization's invoices on file
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const getOrganizationInvoices = async (req: Request, res: Response) => {
|
export const getOrganizationInvoices = async (req: Request, res: Response) => {
|
||||||
const { data: { invoices } } = await licenseServerKeyRequest.get(
|
const { data: { invoices } } = await licenseServerKeyRequest.get(
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { BotOrgService } from "../../../services";
|
||||||
|
import { SSOConfig } from "../../models";
|
||||||
|
import {
|
||||||
|
MembershipOrg,
|
||||||
|
User
|
||||||
|
} from "../../../models";
|
||||||
|
import { getSSOConfigHelper } from "../../helpers/organizations";
|
||||||
|
import { client } from "../../../config";
|
||||||
|
import { ResourceNotFoundError } from "../../../utils/errors";
|
||||||
|
import { getSiteURL } from "../../../config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect user to appropriate SSO endpoint after successful authentication
|
||||||
|
* to finish inputting their master key for logging in or signing up
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const redirectSSO = async (req: Request, res: Response) => {
|
||||||
|
if (req.isUserCompleted) {
|
||||||
|
return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organization SAML SSO configuration
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSSOConfig = async (req: Request, res: Response) => {
|
||||||
|
const organizationId = req.query.organizationId as string;
|
||||||
|
|
||||||
|
const data = await getSSOConfigHelper({
|
||||||
|
organizationId: new Types.ObjectId(organizationId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update organization SAML SSO configuration
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateSSOConfig = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
organizationId,
|
||||||
|
authProvider,
|
||||||
|
isActive,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
interface PatchUpdate {
|
||||||
|
authProvider?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
encryptedEntryPoint?: string;
|
||||||
|
entryPointIV?: string;
|
||||||
|
entryPointTag?: string;
|
||||||
|
encryptedIssuer?: string;
|
||||||
|
issuerIV?: string;
|
||||||
|
issuerTag?: string;
|
||||||
|
encryptedCert?: string;
|
||||||
|
certIV?: string;
|
||||||
|
certTag?: string;
|
||||||
|
encryptedAudience?: string;
|
||||||
|
audienceIV?: string;
|
||||||
|
audienceTag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update: PatchUpdate = {};
|
||||||
|
|
||||||
|
if (authProvider) {
|
||||||
|
update.authProvider = authProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive !== undefined) {
|
||||||
|
update.isActive = isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await BotOrgService.getSymmetricKey(
|
||||||
|
new Types.ObjectId(organizationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entryPoint) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedEntryPoint,
|
||||||
|
iv: entryPointIV,
|
||||||
|
tag: entryPointTag
|
||||||
|
} = client.encryptSymmetric(entryPoint, key);
|
||||||
|
|
||||||
|
update.encryptedEntryPoint = encryptedEntryPoint;
|
||||||
|
update.entryPointIV = entryPointIV;
|
||||||
|
update.entryPointTag = entryPointTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issuer) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedIssuer,
|
||||||
|
iv: issuerIV,
|
||||||
|
tag: issuerTag
|
||||||
|
} = client.encryptSymmetric(issuer, key);
|
||||||
|
|
||||||
|
update.encryptedIssuer = encryptedIssuer;
|
||||||
|
update.issuerIV = issuerIV;
|
||||||
|
update.issuerTag = issuerTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cert) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedCert,
|
||||||
|
iv: certIV,
|
||||||
|
tag: certTag
|
||||||
|
} = client.encryptSymmetric(cert, key);
|
||||||
|
|
||||||
|
update.encryptedCert = encryptedCert;
|
||||||
|
update.certIV = certIV;
|
||||||
|
update.certTag = certTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audience) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedAudience,
|
||||||
|
iv: audienceIV,
|
||||||
|
tag: audienceTag
|
||||||
|
} = client.encryptSymmetric(audience, key);
|
||||||
|
|
||||||
|
update.encryptedAudience = encryptedAudience;
|
||||||
|
update.audienceIV = audienceIV;
|
||||||
|
update.audienceTag = audienceTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssoConfig = await SSOConfig.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
organization: new Types.ObjectId(organizationId)
|
||||||
|
},
|
||||||
|
update,
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ssoConfig) throw ResourceNotFoundError({
|
||||||
|
message: "Failed to find SSO config to update"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (update.isActive !== undefined) {
|
||||||
|
const membershipOrgs = await MembershipOrg.find({
|
||||||
|
organization: new Types.ObjectId(organizationId)
|
||||||
|
}).select("user");
|
||||||
|
|
||||||
|
if (update.isActive) {
|
||||||
|
await User.updateMany(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authProvider: ssoConfig.authProvider
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await User.updateMany(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unset: {
|
||||||
|
authProvider: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(ssoConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create organization SAML SSO configuration
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createSSOConfig = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
organizationId,
|
||||||
|
authProvider,
|
||||||
|
isActive,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const key = await BotOrgService.getSymmetricKey(
|
||||||
|
new Types.ObjectId(organizationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedEntryPoint,
|
||||||
|
iv: entryPointIV,
|
||||||
|
tag: entryPointTag
|
||||||
|
} = client.encryptSymmetric(entryPoint, key);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedIssuer,
|
||||||
|
iv: issuerIV,
|
||||||
|
tag: issuerTag
|
||||||
|
} = client.encryptSymmetric(issuer, key);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedCert,
|
||||||
|
iv: certIV,
|
||||||
|
tag: certTag
|
||||||
|
} = client.encryptSymmetric(cert, key);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedAudience,
|
||||||
|
iv: audienceIV,
|
||||||
|
tag: audienceTag
|
||||||
|
} = client.encryptSymmetric(audience, key);
|
||||||
|
|
||||||
|
const ssoConfig = await new SSOConfig({
|
||||||
|
organization: new Types.ObjectId(organizationId),
|
||||||
|
authProvider,
|
||||||
|
isActive,
|
||||||
|
encryptedEntryPoint,
|
||||||
|
entryPointIV,
|
||||||
|
entryPointTag,
|
||||||
|
encryptedIssuer,
|
||||||
|
issuerIV,
|
||||||
|
issuerTag,
|
||||||
|
encryptedCert,
|
||||||
|
certIV,
|
||||||
|
certTag,
|
||||||
|
encryptedAudience,
|
||||||
|
audienceIV,
|
||||||
|
audienceTag
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
return res.status(200).send(ssoConfig);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
import {
|
||||||
|
SSOConfig
|
||||||
|
} from "../models";
|
||||||
|
import {
|
||||||
|
BotOrgService
|
||||||
|
} from "../../services";
|
||||||
|
import { client } from "../../config";
|
||||||
|
import { ValidationError } from "../../utils/errors";
|
||||||
|
|
||||||
|
export const getSSOConfigHelper = async ({
|
||||||
|
organizationId,
|
||||||
|
ssoConfigId
|
||||||
|
}: {
|
||||||
|
organizationId?: Types.ObjectId;
|
||||||
|
ssoConfigId?: Types.ObjectId;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
if (!organizationId && !ssoConfigId) throw ValidationError({
|
||||||
|
message: "Getting SSO data requires either id of organization or SSO data"
|
||||||
|
});
|
||||||
|
|
||||||
|
const ssoConfig = await SSOConfig.findOne({
|
||||||
|
...(organizationId ? { organization: organizationId } : {}),
|
||||||
|
...(ssoConfigId ? { _id: ssoConfigId } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ssoConfig) throw new Error("Failed to find organization SSO data");
|
||||||
|
|
||||||
|
const key = await BotOrgService.getSymmetricKey(
|
||||||
|
ssoConfig.organization
|
||||||
|
);
|
||||||
|
|
||||||
|
const entryPoint = client.decryptSymmetric(
|
||||||
|
ssoConfig.encryptedEntryPoint,
|
||||||
|
key,
|
||||||
|
ssoConfig.entryPointIV,
|
||||||
|
ssoConfig.entryPointTag
|
||||||
|
);
|
||||||
|
|
||||||
|
const issuer = client.decryptSymmetric(
|
||||||
|
ssoConfig.encryptedIssuer,
|
||||||
|
key,
|
||||||
|
ssoConfig.issuerIV,
|
||||||
|
ssoConfig.issuerTag
|
||||||
|
);
|
||||||
|
|
||||||
|
const cert = client.decryptSymmetric(
|
||||||
|
ssoConfig.encryptedCert,
|
||||||
|
key,
|
||||||
|
ssoConfig.certIV,
|
||||||
|
ssoConfig.certTag
|
||||||
|
);
|
||||||
|
|
||||||
|
const audience = client.decryptSymmetric(
|
||||||
|
ssoConfig.encryptedAudience,
|
||||||
|
key,
|
||||||
|
ssoConfig.audienceIV,
|
||||||
|
ssoConfig.audienceTag
|
||||||
|
);
|
||||||
|
|
||||||
|
return ({
|
||||||
|
_id: ssoConfig._id,
|
||||||
|
organization: ssoConfig.organization,
|
||||||
|
authProvider: ssoConfig.authProvider,
|
||||||
|
isActive: ssoConfig.isActive,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import requireLicenseAuth from "./requireLicenseAuth";
|
|
||||||
import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth";
|
import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
requireLicenseAuth,
|
|
||||||
requireSecretSnapshotAuth,
|
requireSecretSnapshotAuth,
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if organization hosting meets license requirements to
|
|
||||||
* access a license-specific route.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String[]} obj.acceptedTiers
|
|
||||||
*/
|
|
||||||
const requireLicenseAuth = ({
|
|
||||||
acceptedTiers,
|
|
||||||
}: {
|
|
||||||
acceptedTiers: string[];
|
|
||||||
}) => {
|
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default requireLicenseAuth;
|
|
||||||
@@ -3,6 +3,7 @@ import SecretVersion, { ISecretVersion } from "./secretVersion";
|
|||||||
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
|
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
|
||||||
import Log, { ILog } from "./log";
|
import Log, { ILog } from "./log";
|
||||||
import Action, { IAction } from "./action";
|
import Action, { IAction } from "./action";
|
||||||
|
import SSOConfig, { ISSOConfig } from "./ssoConfig";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
@@ -15,4 +16,6 @@ export {
|
|||||||
ILog,
|
ILog,
|
||||||
Action,
|
Action,
|
||||||
IAction,
|
IAction,
|
||||||
|
SSOConfig,
|
||||||
|
ISSOConfig
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,9 +63,10 @@ const logSchema = new Schema<ILog>(
|
|||||||
ipAddress: {
|
ipAddress: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
}, {
|
},
|
||||||
timestamps: true,
|
{
|
||||||
}
|
timestamps: true,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Log = model<ILog>("Log", logSchema);
|
const Log = model<ILog>("Log", logSchema);
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
|
export interface ISSOConfig {
|
||||||
|
organization: Types.ObjectId;
|
||||||
|
authProvider: "okta-saml"
|
||||||
|
isActive: boolean;
|
||||||
|
encryptedEntryPoint: string;
|
||||||
|
entryPointIV: string;
|
||||||
|
entryPointTag: string;
|
||||||
|
encryptedIssuer: string;
|
||||||
|
issuerIV: string;
|
||||||
|
issuerTag: string;
|
||||||
|
encryptedCert: string;
|
||||||
|
certIV: string;
|
||||||
|
certTag: string;
|
||||||
|
encryptedAudience: string;
|
||||||
|
audienceIV: string;
|
||||||
|
audienceTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssoConfigSchema = new Schema<ISSOConfig>(
|
||||||
|
{
|
||||||
|
organization: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "Organization"
|
||||||
|
},
|
||||||
|
authProvider: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
"okta-saml"
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
encryptedEntryPoint: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
entryPointIV: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
entryPointTag: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
encryptedIssuer: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
issuerIV: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
issuerTag: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
encryptedCert: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
certIV: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
certTag: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
encryptedAudience: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
audienceIV: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
audienceTag: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);
|
||||||
|
|
||||||
|
export default SSOConfig;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import secret from "./secret";
|
import secret from "./secret";
|
||||||
import secretSnapshot from "./secretSnapshot";
|
import secretSnapshot from "./secretSnapshot";
|
||||||
import organizations from "./organizations";
|
import organizations from "./organizations";
|
||||||
|
import sso from "./sso";
|
||||||
import workspace from "./workspace";
|
import workspace from "./workspace";
|
||||||
import action from "./action";
|
import action from "./action";
|
||||||
import cloudProducts from "./cloudProducts";
|
import cloudProducts from "./cloudProducts";
|
||||||
@@ -9,6 +10,7 @@ export {
|
|||||||
secret,
|
secret,
|
||||||
secretSnapshot,
|
secretSnapshot,
|
||||||
organizations,
|
organizations,
|
||||||
|
sso,
|
||||||
workspace,
|
workspace,
|
||||||
action,
|
action,
|
||||||
cloudProducts,
|
cloudProducts,
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import express from "express";
|
||||||
|
const router = express.Router();
|
||||||
|
import passport from "passport";
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth,
|
||||||
|
validateRequest,
|
||||||
|
} from "../../../middleware";
|
||||||
|
import { body, query } from "express-validator";
|
||||||
|
import { ssoController } from "../../controllers/v1";
|
||||||
|
import { authLimiter } from "../../../helpers/rateLimiter";
|
||||||
|
import {
|
||||||
|
ACCEPTED,
|
||||||
|
OWNER,
|
||||||
|
ADMIN
|
||||||
|
} from "../../../variables";
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/redirect/google",
|
||||||
|
authLimiter,
|
||||||
|
passport.authenticate("google", {
|
||||||
|
scope: ["profile", "email"],
|
||||||
|
session: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/google",
|
||||||
|
passport.authenticate("google", {
|
||||||
|
failureRedirect: "/login/provider/error",
|
||||||
|
session: false
|
||||||
|
}),
|
||||||
|
ssoController.redirectSSO
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/redirect/saml2/:ssoIdentifier",
|
||||||
|
authLimiter,
|
||||||
|
passport.authenticate("saml", {
|
||||||
|
failureRedirect: "/login/fail"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post("/saml2/:ssoIdentifier",
|
||||||
|
passport.authenticate("saml", {
|
||||||
|
failureRedirect: "/login/provider/error",
|
||||||
|
failureFlash: true,
|
||||||
|
session: false
|
||||||
|
}),
|
||||||
|
ssoController.redirectSSO
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/config",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ["jwt"],
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN],
|
||||||
|
acceptedStatuses: [ACCEPTED],
|
||||||
|
locationOrganizationId: "query"
|
||||||
|
}),
|
||||||
|
query("organizationId").exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
ssoController.getSSOConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/config",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ["jwt"],
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN],
|
||||||
|
acceptedStatuses: [ACCEPTED],
|
||||||
|
locationOrganizationId: "body"
|
||||||
|
}),
|
||||||
|
body("organizationId").exists().trim(),
|
||||||
|
body("authProvider").exists().isString(),
|
||||||
|
body("isActive").exists().isBoolean(),
|
||||||
|
body("entryPoint").exists().isString(),
|
||||||
|
body("issuer").exists().isString(),
|
||||||
|
body("cert").exists().isString(),
|
||||||
|
body("audience").exists().isString(),
|
||||||
|
validateRequest,
|
||||||
|
ssoController.createSSOConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/config",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ["jwt"],
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN],
|
||||||
|
acceptedStatuses: [ACCEPTED],
|
||||||
|
locationOrganizationId: "body"
|
||||||
|
}),
|
||||||
|
body("organizationId").exists().trim(),
|
||||||
|
body("authProvider").optional().isString(),
|
||||||
|
body("isActive").optional().isBoolean(),
|
||||||
|
body("entryPoint").optional().isString(),
|
||||||
|
body("issuer").optional().isString(),
|
||||||
|
body("cert").optional().isString(),
|
||||||
|
body("audience").optional().isString(),
|
||||||
|
validateRequest,
|
||||||
|
ssoController.updateSSOConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -30,7 +30,8 @@ interface FeatureSet {
|
|||||||
customRateLimits: boolean;
|
customRateLimits: boolean;
|
||||||
customAlerts: boolean;
|
customAlerts: boolean;
|
||||||
auditLogs: boolean;
|
auditLogs: boolean;
|
||||||
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null;
|
samlSSO: boolean;
|
||||||
|
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
||||||
trial_end: number | null;
|
trial_end: number | null;
|
||||||
has_used_trial: boolean;
|
has_used_trial: boolean;
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,7 @@ class EELicenseService {
|
|||||||
customRateLimits: true,
|
customRateLimits: true,
|
||||||
customAlerts: true,
|
customAlerts: true,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
|
samlSSO: false,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true
|
has_used_trial: true
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
||||||
|
import { BotOrg } from "../models";
|
||||||
|
import { decryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto";
|
||||||
|
import {
|
||||||
|
ENCODING_SCHEME_BASE64,
|
||||||
|
ENCODING_SCHEME_UTF8
|
||||||
|
} from "../variables";
|
||||||
|
import { InternalServerError } from "../utils/errors";
|
||||||
|
|
||||||
|
// TODO: DOCstrings
|
||||||
|
|
||||||
|
export const getSymmetricKeyHelper = async (organizationId: Types.ObjectId) => {
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
|
||||||
|
const botOrg = await BotOrg.findOne({
|
||||||
|
organization: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!botOrg) throw new Error("Failed to find organization bot");
|
||||||
|
|
||||||
|
if (rootEncryptionKey && botOrg.symmetricKeyKeyEncoding == ENCODING_SCHEME_BASE64) {
|
||||||
|
const key = client.decryptSymmetric(
|
||||||
|
botOrg.encryptedSymmetricKey,
|
||||||
|
rootEncryptionKey,
|
||||||
|
botOrg.symmetricKeyIV,
|
||||||
|
botOrg.symmetricKeyTag
|
||||||
|
);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
} else if (encryptionKey && botOrg.symmetricKeyKeyEncoding === ENCODING_SCHEME_UTF8) {
|
||||||
|
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: botOrg.encryptedSymmetricKey,
|
||||||
|
iv: botOrg.symmetricKeyIV,
|
||||||
|
tag: botOrg.symmetricKeyTag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: "Failed to match encryption key with organization bot symmetric key encoding"
|
||||||
|
});
|
||||||
|
}
|
||||||
+10
-7
@@ -19,6 +19,7 @@ import {
|
|||||||
action as eeActionRouter,
|
action as eeActionRouter,
|
||||||
cloudProducts as eeCloudProductsRouter,
|
cloudProducts as eeCloudProductsRouter,
|
||||||
organizations as eeOrganizationsRouter,
|
organizations as eeOrganizationsRouter,
|
||||||
|
sso as eeSSORouter,
|
||||||
secret as eeSecretRouter,
|
secret as eeSecretRouter,
|
||||||
secretSnapshot as eeSecretSnapshotRouter,
|
secretSnapshot as eeSecretSnapshotRouter,
|
||||||
workspace as eeWorkspaceRouter
|
workspace as eeWorkspaceRouter
|
||||||
@@ -41,22 +42,22 @@ import {
|
|||||||
signup as v1SignupRouter,
|
signup as v1SignupRouter,
|
||||||
userAction as v1UserActionRouter,
|
userAction as v1UserActionRouter,
|
||||||
user as v1UserRouter,
|
user as v1UserRouter,
|
||||||
workspace as v1WorkspaceRouter,
|
|
||||||
webhooks as v1WebhooksRouter,
|
webhooks as v1WebhooksRouter,
|
||||||
|
workspace as v1WorkspaceRouter,
|
||||||
secretImport as v1SecretImportRouter
|
secretImport as v1SecretImportRouter
|
||||||
} from "./routes/v1";
|
} from "./routes/v1";
|
||||||
import {
|
import {
|
||||||
auth as v2AuthRouter,
|
auth as v2AuthRouter,
|
||||||
|
environment as v2EnvironmentRouter,
|
||||||
organizations as v2OrganizationsRouter,
|
organizations as v2OrganizationsRouter,
|
||||||
signup as v2SignupRouter,
|
|
||||||
users as v2UsersRouter,
|
|
||||||
workspace as v2WorkspaceRouter,
|
|
||||||
secret as v2SecretRouter, // begin to phase out
|
secret as v2SecretRouter, // begin to phase out
|
||||||
secrets as v2SecretsRouter,
|
secrets as v2SecretsRouter,
|
||||||
serviceTokenData as v2ServiceTokenDataRouter,
|
|
||||||
serviceAccounts as v2ServiceAccountsRouter,
|
serviceAccounts as v2ServiceAccountsRouter,
|
||||||
environment as v2EnvironmentRouter,
|
serviceTokenData as v2ServiceTokenDataRouter,
|
||||||
tags as v2TagsRouter
|
signup as v2SignupRouter,
|
||||||
|
tags as v2TagsRouter,
|
||||||
|
users as v2UsersRouter,
|
||||||
|
workspace as v2WorkspaceRouter,
|
||||||
} from "./routes/v2";
|
} from "./routes/v2";
|
||||||
import {
|
import {
|
||||||
auth as v3AuthRouter,
|
auth as v3AuthRouter,
|
||||||
@@ -81,6 +82,7 @@ const main = async () => {
|
|||||||
const app = express();
|
const app = express();
|
||||||
app.enable("trust proxy");
|
app.enable("trust proxy");
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: false }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
@@ -130,6 +132,7 @@ const main = async () => {
|
|||||||
app.use("/api/v1/workspace", eeWorkspaceRouter);
|
app.use("/api/v1/workspace", eeWorkspaceRouter);
|
||||||
app.use("/api/v1/action", eeActionRouter);
|
app.use("/api/v1/action", eeActionRouter);
|
||||||
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
||||||
|
app.use("/api/v1/sso", eeSSORouter);
|
||||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
||||||
|
|
||||||
// v1 routes (default)
|
// v1 routes (default)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const revokeAccess = async ({
|
|||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
let deletedIntegrationAuth;
|
|
||||||
// add any integration-specific revocation logic
|
// add any integration-specific revocation logic
|
||||||
switch (integrationAuth.integration) {
|
switch (integrationAuth.integration) {
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
@@ -33,7 +32,7 @@ const revokeAccess = async ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||||
_id: integrationAuth._id,
|
_id: integrationAuth._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Types } from "mongoose";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import {
|
import {
|
||||||
getAuthAPIKeyPayload,
|
getAuthAPIKeyPayload,
|
||||||
@@ -51,6 +52,10 @@ const requireAuth = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
let authPayload: IUser | IServiceAccount | IServiceTokenData;
|
let authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
let authUserPayload: {
|
||||||
|
user: IUser;
|
||||||
|
tokenVersionId: Types.ObjectId;
|
||||||
|
};
|
||||||
switch (authMode) {
|
switch (authMode) {
|
||||||
case AUTH_MODE_SERVICE_ACCOUNT:
|
case AUTH_MODE_SERVICE_ACCOUNT:
|
||||||
authPayload = await getAuthSAAKPayload({
|
authPayload = await getAuthSAAKPayload({
|
||||||
@@ -71,12 +76,12 @@ const requireAuth = ({
|
|||||||
req.user = authPayload;
|
req.user = authPayload;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
const { user, tokenVersionId } = await getAuthUserPayload({
|
authUserPayload = await getAuthUserPayload({
|
||||||
authTokenValue,
|
authTokenValue,
|
||||||
});
|
});
|
||||||
authPayload = user;
|
authPayload = authUserPayload.user;
|
||||||
req.user = user;
|
req.user = authUserPayload.user;
|
||||||
req.tokenVersionId = tokenVersionId;
|
req.tokenVersionId = authUserPayload.tokenVersionId;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Schema, Types, model } from "mongoose";
|
||||||
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_BASE64,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
} from "../variables";
|
||||||
|
|
||||||
|
export interface IBotOrg {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
|
organization: Types.ObjectId;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedSymmetricKey: string;
|
||||||
|
symmetricKeyIV: string;
|
||||||
|
symmetricKeyTag: string;
|
||||||
|
symmetricKeyAlgorithm: "aes-256-gcm";
|
||||||
|
symmetricKeyKeyEncoding: "base64" | "utf8";
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
privateKeyIV: string;
|
||||||
|
privateKeyTag: string;
|
||||||
|
privateKeyAlgorithm: "aes-256-gcm";
|
||||||
|
privateKeyKeyEncoding: "base64" | "utf8";
|
||||||
|
}
|
||||||
|
|
||||||
|
const botOrgSchema = new Schema<IBotOrg>(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "Organization",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
publicKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
encryptedSymmetricKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
symmetricKeyIV: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
symmetricKeyTag: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
symmetricKeyAlgorithm: {
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
symmetricKeyKeyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64,
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
privateKeyIV: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
privateKeyTag: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
privateKeyAlgorithm: {
|
||||||
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
privateKeyKeyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64,
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BotOrg = model<IBotOrg>("BotOrg", botOrgSchema);
|
||||||
|
|
||||||
|
export default BotOrg;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey";
|
import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey";
|
||||||
import Bot, { IBot } from "./bot";
|
import Bot, { IBot } from "./bot";
|
||||||
|
import BotOrg, { IBotOrg } from "./botOrg";
|
||||||
import BotKey, { IBotKey } from "./botKey";
|
import BotKey, { IBotKey } from "./botKey";
|
||||||
import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg";
|
import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg";
|
||||||
import Integration, { IIntegration } from "./integration";
|
import Integration, { IIntegration } from "./integration";
|
||||||
@@ -31,6 +32,8 @@ export {
|
|||||||
IBackupPrivateKey,
|
IBackupPrivateKey,
|
||||||
Bot,
|
Bot,
|
||||||
IBot,
|
IBot,
|
||||||
|
BotOrg,
|
||||||
|
IBotOrg,
|
||||||
BotKey,
|
BotKey,
|
||||||
IBotKey,
|
IBotKey,
|
||||||
IncidentContactOrg,
|
IncidentContactOrg,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Document, Schema, Types, model } from "mongoose";
|
|||||||
|
|
||||||
export enum AuthProvider {
|
export enum AuthProvider {
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
|
OKTA_SAML = "okta-saml"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser extends Document {
|
export interface IUser extends Document {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { body } from "express-validator";
|
import { body } from "express-validator";
|
||||||
import passport from "passport";
|
|
||||||
import { requireAuth, validateRequest } from "../../middleware";
|
import { requireAuth, validateRequest } from "../../middleware";
|
||||||
import { authController } from "../../controllers/v1";
|
import { authController } from "../../controllers/v1";
|
||||||
import { authLimiter } from "../../helpers/rateLimiter";
|
import { authLimiter } from "../../helpers/rateLimiter";
|
||||||
@@ -44,21 +43,6 @@ router.post(
|
|||||||
authController.checkAuth
|
authController.checkAuth
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/redirect/google",
|
|
||||||
authLimiter,
|
|
||||||
passport.authenticate("google", {
|
|
||||||
scope: ["profile", "email"],
|
|
||||||
session: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/callback/google",
|
|
||||||
passport.authenticate("google", { failureRedirect: "/login/provider/error", session: false }),
|
|
||||||
authController.handleAuthProviderCallback,
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/common-passwords",
|
"/common-passwords",
|
||||||
authLimiter,
|
authLimiter,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import express from "express";
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
|
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
|
||||||
import { body, param, query } from "express-validator";
|
import { body, param, query } from "express-validator";
|
||||||
import { ADMIN, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, MEMBER } from "../../variables";
|
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
|
||||||
import { webhookController } from "../../controllers/v1";
|
import { webhookController } from "../../controllers/v1";
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ router.post(
|
|||||||
body("salt").exists().isString().trim().notEmpty(),
|
body("salt").exists().isString().trim().notEmpty(),
|
||||||
body("verifier").exists().isString().trim().notEmpty(),
|
body("verifier").exists().isString().trim().notEmpty(),
|
||||||
body("organizationName").exists().isString().trim().notEmpty(),
|
body("organizationName").exists().isString().trim().notEmpty(),
|
||||||
body("providerAuthToken").isString().trim().optional({nullable: true}),
|
body("providerAuthToken").isString().trim().optional({ nullable: true }),
|
||||||
|
body("attributionSource").optional().isString().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
signupController.completeAccountSignup,
|
signupController.completeAccountSignup,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
import { getSymmetricKeyHelper } from "../helpers/botOrg";
|
||||||
|
|
||||||
|
// TODO: DOCstrings
|
||||||
|
|
||||||
|
class BotOrgService {
|
||||||
|
static async getSymmetricKey(organizationId: Types.ObjectId) {
|
||||||
|
return await getSymmetricKeyHelper(organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BotOrgService;
|
||||||
@@ -2,6 +2,7 @@ import DatabaseService from "./DatabaseService";
|
|||||||
// import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
|
// import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
|
||||||
import TelemetryService from "./TelemetryService";
|
import TelemetryService from "./TelemetryService";
|
||||||
import BotService from "./BotService";
|
import BotService from "./BotService";
|
||||||
|
import BotOrgService from "./BotOrgService";
|
||||||
import EventService from "./EventService";
|
import EventService from "./EventService";
|
||||||
import IntegrationService from "./IntegrationService";
|
import IntegrationService from "./IntegrationService";
|
||||||
import TokenService from "./TokenService";
|
import TokenService from "./TokenService";
|
||||||
@@ -12,9 +13,10 @@ export {
|
|||||||
TelemetryService,
|
TelemetryService,
|
||||||
DatabaseService,
|
DatabaseService,
|
||||||
BotService,
|
BotService,
|
||||||
|
BotOrgService,
|
||||||
EventService,
|
EventService,
|
||||||
IntegrationService,
|
IntegrationService,
|
||||||
TokenService,
|
TokenService,
|
||||||
SecretService,
|
SecretService,
|
||||||
GithubSecretScanningService
|
GithubSecretScanningService
|
||||||
};
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -20,6 +20,7 @@ declare global {
|
|||||||
workspace: any;
|
workspace: any;
|
||||||
membership: any;
|
membership: any;
|
||||||
targetMembership: any;
|
targetMembership: any;
|
||||||
|
isUserCompleted: boolean;
|
||||||
providerAuthToken: any;
|
providerAuthToken: any;
|
||||||
organization: any;
|
organization: any;
|
||||||
membershipOrg: any;
|
membershipOrg: any;
|
||||||
|
|||||||
+114
-12
@@ -1,11 +1,14 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
|
import { Types } from "mongoose";
|
||||||
import { AuthData } from "../interfaces/middleware";
|
import { AuthData } from "../interfaces/middleware";
|
||||||
import {
|
import {
|
||||||
AuthProvider,
|
AuthProvider,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
User,
|
User,
|
||||||
|
Organization,
|
||||||
|
MembershipOrg
|
||||||
} from "../models";
|
} from "../models";
|
||||||
import { createToken } from "../helpers/auth";
|
import { createToken } from "../helpers/auth";
|
||||||
import {
|
import {
|
||||||
@@ -14,11 +17,15 @@ import {
|
|||||||
getJwtProviderAuthLifetime,
|
getJwtProviderAuthLifetime,
|
||||||
getJwtProviderAuthSecret,
|
getJwtProviderAuthSecret,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
|
import { getSSOConfigHelper } from "../ee/helpers/organizations";
|
||||||
|
import { OrganizationNotFoundError, InternalServerError } from "./errors";
|
||||||
|
import { MEMBER, INVITED } from "../variables";
|
||||||
|
import { getSiteURL } from "../config";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
// TODO: find a more optimal folder structure to store these types of functions
|
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing the id of the authentication data payload
|
* Returns an object containing the id of the authentication data payload
|
||||||
@@ -39,7 +46,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing the user associated with the authentication data payload
|
* Returns an object containing the user associated with the authentication data payload
|
||||||
* @param {AuthData} authData - authentication data object
|
* @param {AuthData} authData - authentication data object
|
||||||
@@ -56,7 +62,7 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
return { user: authData.authPayload.user };
|
return { user: authData.authPayload.user };0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,47 +74,143 @@ const initializePassport = async () => {
|
|||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
clientID: googleClientId,
|
clientID: googleClientId,
|
||||||
clientSecret: googleClientSecret,
|
clientSecret: googleClientSecret,
|
||||||
callbackURL: "/api/v1/auth/callback/google",
|
callbackURL: "/api/v1/sso/google",
|
||||||
scope: ["profile", " email"],
|
scope: ["profile", " email"],
|
||||||
}, async (
|
}, async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
profile: any,
|
profile: any,
|
||||||
cb: any
|
done: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const email = profile.emails[0].value;
|
const email = profile.emails[0].value;
|
||||||
|
const firstName = profile.name.givenName;
|
||||||
|
const lastName = profile.name.familyName;
|
||||||
|
|
||||||
let user = await User.findOne({
|
let user = await User.findOne({
|
||||||
authProvider: AuthProvider.GOOGLE,
|
email
|
||||||
authId: profile.id,
|
}).select("+publicKey");
|
||||||
}).select("+publicKey")
|
|
||||||
|
if (user && user.authProvider !== AuthProvider.GOOGLE) {
|
||||||
|
done(InternalServerError());
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email,
|
email,
|
||||||
authProvider: AuthProvider.GOOGLE,
|
authProvider: AuthProvider.GOOGLE,
|
||||||
authId: profile.id,
|
authId: profile.id,
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUserCompleted = !!user.publicKey;
|
||||||
const providerAuthToken = createToken({
|
const providerAuthToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId: user._id.toString(),
|
userId: user._id.toString(),
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
authProvider: user.authProvider,
|
authProvider: user.authProvider,
|
||||||
isUserCompleted: !!user.publicKey,
|
isUserCompleted
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtProviderAuthLifetime(),
|
expiresIn: await getJwtProviderAuthLifetime(),
|
||||||
secret: await getJwtProviderAuthSecret(),
|
secret: await getJwtProviderAuthSecret(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
req.isUserCompleted = isUserCompleted;
|
||||||
req.providerAuthToken = providerAuthToken;
|
req.providerAuthToken = providerAuthToken;
|
||||||
cb(null, profile);
|
done(null, profile);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
cb(null, false);
|
done(null, false);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
passport.use("saml", new MultiSamlStrategy(
|
||||||
|
{
|
||||||
|
passReqToCallback: true,
|
||||||
|
getSamlOptions: async (req: any, done: any) => {
|
||||||
|
const { ssoIdentifier } = req.params;
|
||||||
|
|
||||||
|
const ssoConfig = await getSSOConfigHelper({
|
||||||
|
ssoConfigId: new Types.ObjectId(ssoIdentifier)
|
||||||
|
});
|
||||||
|
|
||||||
|
const samlConfig = ({
|
||||||
|
path: "/api/v1/auth/callback/saml",
|
||||||
|
callbackURL: `${await getSiteURL()}/api/v1/auth/callback/saml`,
|
||||||
|
entryPoint: ssoConfig.entryPoint,
|
||||||
|
issuer: ssoConfig.issuer,
|
||||||
|
cert: ssoConfig.cert,
|
||||||
|
audience: ssoConfig.audience
|
||||||
|
});
|
||||||
|
|
||||||
|
req.ssoConfig = ssoConfig;
|
||||||
|
|
||||||
|
done(null, samlConfig);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req: any, profile: any, done: any) => {
|
||||||
|
|
||||||
|
if (!req.ssoConfig.isActive) return done(InternalServerError());
|
||||||
|
|
||||||
|
const organization = await Organization.findById(req.ssoConfig.organization);
|
||||||
|
|
||||||
|
if (!organization) return done(OrganizationNotFoundError());
|
||||||
|
|
||||||
|
const email = profile.email;
|
||||||
|
const firstName = profile.firstName;
|
||||||
|
const lastName = profile.lastName;
|
||||||
|
|
||||||
|
let user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select("+publicKey");
|
||||||
|
|
||||||
|
if (user && user.authProvider !== AuthProvider.OKTA_SAML) {
|
||||||
|
done(InternalServerError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = await new User({
|
||||||
|
email,
|
||||||
|
authProvider: AuthProvider.OKTA_SAML,
|
||||||
|
authId: profile.id,
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
await new MembershipOrg({
|
||||||
|
inviteEmail: email,
|
||||||
|
user: user._id,
|
||||||
|
organization: organization?._id,
|
||||||
|
role: MEMBER,
|
||||||
|
status: INVITED
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserCompleted = !!user.publicKey;
|
||||||
|
const providerAuthToken = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
organizationName: organization?.name,
|
||||||
|
authProvider: user.authProvider,
|
||||||
|
isUserCompleted
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtProviderAuthLifetime(),
|
||||||
|
secret: await getJwtProviderAuthSecret(),
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isUserCompleted = isUserCompleted;
|
||||||
|
req.providerAuthToken = providerAuthToken;
|
||||||
|
|
||||||
|
done(null, profile);
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
|
|||||||
stack: error?.stack,
|
stack: error?.stack,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
|
export const ResourceNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
statusCode: error?.statusCode ?? 404,
|
statusCode: error?.statusCode ?? 404,
|
||||||
type: error?.type ?? "resource_not_found",
|
type: error?.type ?? "resource_not_found",
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
|
|||||||
import {
|
import {
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
Bot,
|
Bot,
|
||||||
|
BotOrg,
|
||||||
ISecret,
|
ISecret,
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
|
Organization,
|
||||||
Secret,
|
Secret,
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
@@ -137,6 +139,103 @@ export const backfillBots = async () => {
|
|||||||
await Bot.insertMany(botsToInsert);
|
await Bot.insertMany(botsToInsert);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill organization bots to ensure that every organization has a bot
|
||||||
|
*/
|
||||||
|
export const backfillBotOrgs = async () => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
const organizationIdsWithBot = await BotOrg.distinct("organization");
|
||||||
|
const organizationIdsToAddBot = await Organization.distinct("_id", {
|
||||||
|
_id: {
|
||||||
|
$nin: organizationIdsWithBot
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organizationIdsToAddBot.length === 0) return;
|
||||||
|
|
||||||
|
const botsToInsert = await Promise.all(
|
||||||
|
organizationIdsToAddBot.map(async (organizationToAddBot) => {
|
||||||
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
|
|
||||||
|
const key = client.createSymmetricKey();
|
||||||
|
|
||||||
|
if (rootEncryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: privateKeyIV,
|
||||||
|
tag: privateKeyTag
|
||||||
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSymmetricKey,
|
||||||
|
iv: symmetricKeyIV,
|
||||||
|
tag: symmetricKeyTag
|
||||||
|
} = client.encryptSymmetric(key, rootEncryptionKey);
|
||||||
|
|
||||||
|
return new BotOrg({
|
||||||
|
name: "Infisical Bot",
|
||||||
|
organization: organizationToAddBot,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedSymmetricKey,
|
||||||
|
symmetricKeyIV,
|
||||||
|
symmetricKeyTag,
|
||||||
|
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
privateKeyIV,
|
||||||
|
privateKeyTag,
|
||||||
|
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
});
|
||||||
|
} else if (encryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: privateKeyIV,
|
||||||
|
tag: privateKeyTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSymmetricKey,
|
||||||
|
iv: symmetricKeyIV,
|
||||||
|
tag: symmetricKeyTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: key,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return new BotOrg({
|
||||||
|
name: "Infisical Bot",
|
||||||
|
organization: organizationToAddBot,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedSymmetricKey,
|
||||||
|
symmetricKeyIV,
|
||||||
|
symmetricKeyTag,
|
||||||
|
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
privateKeyIV,
|
||||||
|
privateKeyTag,
|
||||||
|
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: "Failed to backfill organization bots due to missing encryption key"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await BotOrg.insertMany(botsToInsert);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backfill secret blind index data to ensure that every workspace
|
* Backfill secret blind index data to ensure that every workspace
|
||||||
* has a secret blind index data
|
* has a secret blind index data
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { createTestUserForDevelopment } from "../addDevelopmentUser";
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
import { validateEncryptionKeysConfig } from "./validateConfig";
|
import { validateEncryptionKeysConfig } from "./validateConfig";
|
||||||
import {
|
import {
|
||||||
|
backfillBotOrgs,
|
||||||
backfillBots,
|
backfillBots,
|
||||||
backfillEncryptionMetadata,
|
backfillEncryptionMetadata,
|
||||||
backfillIntegration,
|
backfillIntegration,
|
||||||
@@ -16,7 +17,11 @@ import {
|
|||||||
backfillServiceToken,
|
backfillServiceToken,
|
||||||
backfillServiceTokenMultiScope
|
backfillServiceTokenMultiScope
|
||||||
} from "./backfillData";
|
} from "./backfillData";
|
||||||
import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData";
|
import {
|
||||||
|
reencryptBotOrgKeys,
|
||||||
|
reencryptBotPrivateKeys,
|
||||||
|
reencryptSecretBlindIndexDataSalts
|
||||||
|
} from "./reencryptData";
|
||||||
import {
|
import {
|
||||||
getClientIdGoogle,
|
getClientIdGoogle,
|
||||||
getClientSecretGoogle,
|
getClientSecretGoogle,
|
||||||
@@ -72,6 +77,7 @@ export const setup = async () => {
|
|||||||
// backfilling data to catch up with new collections and updated fields
|
// backfilling data to catch up with new collections and updated fields
|
||||||
await backfillSecretVersions();
|
await backfillSecretVersions();
|
||||||
await backfillBots();
|
await backfillBots();
|
||||||
|
await backfillBotOrgs();
|
||||||
await backfillSecretBlindIndexData();
|
await backfillSecretBlindIndexData();
|
||||||
await backfillEncryptionMetadata();
|
await backfillEncryptionMetadata();
|
||||||
await backfillSecretFolders();
|
await backfillSecretFolders();
|
||||||
@@ -82,6 +88,7 @@ export const setup = async () => {
|
|||||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
await reencryptBotPrivateKeys();
|
await reencryptBotPrivateKeys();
|
||||||
|
await reencryptBotOrgKeys();
|
||||||
await reencryptSecretBlindIndexDataSalts();
|
await reencryptSecretBlindIndexDataSalts();
|
||||||
|
|
||||||
// initializing Sentry
|
// initializing Sentry
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
|
BotOrg,
|
||||||
IBot,
|
IBot,
|
||||||
|
IBotOrg,
|
||||||
ISecretBlindIndexData,
|
ISecretBlindIndexData,
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
@@ -17,7 +19,7 @@ import {
|
|||||||
} from "../../variables";
|
} from "../../variables";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY
|
* Re-encrypt bot private keys from under hex 128-bit ENCRYPTION_KEY
|
||||||
* to base64 256-bit ROOT_ENCRYPTION_KEY
|
* to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
*/
|
*/
|
||||||
export const reencryptBotPrivateKeys = async () => {
|
export const reencryptBotPrivateKeys = async () => {
|
||||||
@@ -70,6 +72,79 @@ export const reencryptBotPrivateKeys = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-encrypt organization bot keys (symmetric and private) from under hex 128-bit ENCRYPTION_KEY
|
||||||
|
* to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
*/
|
||||||
|
export const reencryptBotOrgKeys = async () => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
if (encryptionKey && rootEncryptionKey) {
|
||||||
|
// 1: re-encrypt organization bot keys under ROOT_ENCRYPTION_KEY
|
||||||
|
const botOrgs = await BotOrg.find({
|
||||||
|
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
|
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}).select("+encryptedPrivateKey iv tag algorithm keyEncoding");
|
||||||
|
|
||||||
|
if (botOrgs.length === 0) return;
|
||||||
|
|
||||||
|
const operationsBotOrg = await Promise.all(
|
||||||
|
botOrgs.map(async (botOrg: IBotOrg) => {
|
||||||
|
const privateKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: botOrg.encryptedPrivateKey,
|
||||||
|
iv: botOrg.privateKeyIV,
|
||||||
|
tag: botOrg.privateKeyTag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: privateKeyIV,
|
||||||
|
tag: privateKeyTag,
|
||||||
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
|
const symmetricKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: botOrg.encryptedSymmetricKey,
|
||||||
|
iv: botOrg.symmetricKeyIV,
|
||||||
|
tag: botOrg.symmetricKeyTag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSymmetricKey,
|
||||||
|
iv: symmetricKeyIV,
|
||||||
|
tag: symmetricKeyTag,
|
||||||
|
} = client.encryptSymmetric(symmetricKey, rootEncryptionKey);
|
||||||
|
|
||||||
|
return ({
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
_id: botOrg._id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
encryptedSymmetricKey,
|
||||||
|
symmetricKeyIV,
|
||||||
|
symmetricKeyTag,
|
||||||
|
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
privateKeyIV,
|
||||||
|
privateKeyTag,
|
||||||
|
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await BotOrg.bulkWrite(operationsBotOrg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY
|
* Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY
|
||||||
* to base64 256-bit ROOT_ENCRYPTION_KEY
|
* to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import { FormEvent, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import axios from "axios"
|
|
||||||
|
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
|
||||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
|
||||||
|
|
||||||
import Error from "../basic/Error";
|
|
||||||
import attemptCliLogin from "../utilities/attemptCliLogin";
|
|
||||||
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
|
|
||||||
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { Button, Input } from "../v2";
|
|
||||||
|
|
||||||
export default function InitialLoginStep({
|
|
||||||
setStep,
|
|
||||||
email,
|
|
||||||
setEmail,
|
|
||||||
password,
|
|
||||||
setPassword,
|
|
||||||
}: {
|
|
||||||
setStep: (step: number) => void;
|
|
||||||
email: string;
|
|
||||||
setEmail: (email: string) => void;
|
|
||||||
password: string;
|
|
||||||
setPassword: (password: string) => void;
|
|
||||||
}) {
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [loginError, setLoginError] = useState(false);
|
|
||||||
|
|
||||||
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault()
|
|
||||||
try {
|
|
||||||
if (!email || !password) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
|
||||||
if (queryParams && queryParams.get("callback_port")) {
|
|
||||||
const callbackPort = queryParams.get("callback_port")
|
|
||||||
|
|
||||||
// attemptCliLogin
|
|
||||||
const isCliLoginSuccessful = await attemptCliLogin({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
|
||||||
|
|
||||||
if (isCliLoginSuccessful.mfaEnabled) {
|
|
||||||
// case: login requires MFA step
|
|
||||||
setStep(2);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// case: login was successful
|
|
||||||
const cliUrl = `http://localhost:${callbackPort}`
|
|
||||||
|
|
||||||
// send request to server endpoint
|
|
||||||
const instance = axios.create()
|
|
||||||
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
|
|
||||||
console.log(cliResp)
|
|
||||||
|
|
||||||
// cli page
|
|
||||||
router.push("/cli-redirect");
|
|
||||||
|
|
||||||
// on success, router.push to cli Login Successful page
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const isLoginSuccessful = await attemptLogin({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
|
||||||
// case: login was successful
|
|
||||||
|
|
||||||
if (isLoginSuccessful.mfaEnabled) {
|
|
||||||
// case: login requires MFA step
|
|
||||||
setStep(2);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const userOrgs = await getOrganizations();
|
|
||||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
|
||||||
|
|
||||||
// case: login does not require MFA step
|
|
||||||
router.push(`/org/${userOrg}/overview`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
setLoginError(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
|
|
||||||
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
|
|
||||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="solid"
|
|
||||||
onClick={() => {
|
|
||||||
window.open('/api/v1/auth/redirect/google')
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
|
||||||
className="h-14 w-full mx-0"
|
|
||||||
>
|
|
||||||
{t('login.continue-with-google')}
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<Input
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
type="email"
|
|
||||||
placeholder="Enter your email..."
|
|
||||||
isRequired
|
|
||||||
autoComplete="username"
|
|
||||||
className="h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<Input
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter your password..."
|
|
||||||
isRequired
|
|
||||||
autoComplete="current-password"
|
|
||||||
id="current-password"
|
|
||||||
className="h-12 select:-webkit-autofill:focus"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
|
||||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="sm"
|
|
||||||
isFullWidth
|
|
||||||
className='h-12'
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="solid"
|
|
||||||
isLoading={isLoading}
|
|
||||||
> Login </Button>
|
|
||||||
</div>
|
|
||||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
|
|
||||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
|
||||||
<span className='px-4 text-sm text-bunker-400'>or</span>
|
|
||||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
|
||||||
</div>
|
|
||||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => router.push("/saml-sso")}
|
|
||||||
isFullWidth
|
|
||||||
className="h-14 w-full mx-0"
|
|
||||||
>
|
|
||||||
Continue with SAML SSO
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
|
|
||||||
<span className="mr-1">Don't have an acount yet?</span>
|
|
||||||
<Link href="/signup">
|
|
||||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="text-bunker-400 text-sm flex flex-row">
|
|
||||||
<span className="mr-1">Forgot password?</span>
|
|
||||||
<Link href="/verify-email">
|
|
||||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import Error from "@app/components/basic/Error";
|
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
|
||||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
|
||||||
|
|
||||||
import SecurityClient from "../utilities/SecurityClient";
|
|
||||||
import { Button, Input } from "../v2";
|
|
||||||
|
|
||||||
export default function PasswordInputStep({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
providerAuthToken,
|
|
||||||
setPassword,
|
|
||||||
setProviderAuthToken,
|
|
||||||
setStep
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
providerAuthToken: string;
|
|
||||||
setPassword: (password: string) => void;
|
|
||||||
setProviderAuthToken: (value: string) => void;
|
|
||||||
setStep: (step: number) => void;
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [loginError, setLoginError] = useState(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const loginAttempt = await attemptLogin({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
providerAuthToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loginAttempt && loginAttempt.success) {
|
|
||||||
// case: login was successful
|
|
||||||
|
|
||||||
if (loginAttempt.mfaEnabled) {
|
|
||||||
// case: login requires MFA step
|
|
||||||
setStep(2);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// case: login does not require MFA step
|
|
||||||
const userOrgs = await getOrganizations();
|
|
||||||
const userOrg = userOrgs[0]._id;
|
|
||||||
router.push(`/org/${userOrg?._id}/overview`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setLoginError(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={(e) => e.preventDefault()}>
|
|
||||||
<div className="h-full mx-auto w-full max-w-md px-6 pt-8">
|
|
||||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
|
||||||
What’s your Infisical Password?
|
|
||||||
</p>
|
|
||||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<Input
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter your password..."
|
|
||||||
isRequired
|
|
||||||
autoComplete="current-password"
|
|
||||||
id="current-password"
|
|
||||||
className="h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
|
||||||
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={async () => handleLogin()}
|
|
||||||
isFullWidth
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="h-14"
|
|
||||||
>
|
|
||||||
{t("login.login")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
|
|
||||||
<span className='duration-200 max-w-sm text-center px-4'>
|
|
||||||
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
|
|
||||||
</span>
|
|
||||||
<Link href="/verify-email">
|
|
||||||
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center justify-center">
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
SecurityClient.setProviderAuthToken("");
|
|
||||||
setProviderAuthToken("");
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
|
||||||
>
|
|
||||||
{t("login.other-option")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
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";
|
||||||
|
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
|
|
||||||
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { Button } from "../v2";
|
import { Button } from "../v2";
|
||||||
|
|
||||||
export default function InitialSignupStep({
|
export default function InitialSignupStep({
|
||||||
@@ -16,19 +16,6 @@ export default function InitialSignupStep({
|
|||||||
|
|
||||||
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
|
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
|
||||||
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t("signup.initial-title")}</h1>
|
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t("signup.initial-title")}</h1>
|
||||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="solid"
|
|
||||||
onClick={() => {
|
|
||||||
window.open('/api/v1/auth/redirect/google')
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
|
||||||
className="h-14 w-full mx-0"
|
|
||||||
>
|
|
||||||
{t('signup.continue-with-google')}
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
@@ -42,6 +29,25 @@ export default function InitialSignupStep({
|
|||||||
Sign Up with email
|
Sign Up with email
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'>
|
||||||
|
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||||
|
<span className='px-4 text-sm text-bunker-400'>or</span>
|
||||||
|
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||||
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("/api/v1/sso/redirect/google");
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||||
|
className="h-14 w-full mx-0"
|
||||||
|
>
|
||||||
|
{t("signup.continue-with-google")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const publicPaths = [
|
|||||||
"/signupinvite",
|
"/signupinvite",
|
||||||
"/pricing",
|
"/pricing",
|
||||||
"/signup",
|
"/signup",
|
||||||
|
"/signup/sso",
|
||||||
"/login",
|
"/login",
|
||||||
"/blog",
|
"/blog",
|
||||||
"/docs",
|
"/docs",
|
||||||
@@ -18,8 +19,9 @@ export const publicPaths = [
|
|||||||
"/verify-email",
|
"/verify-email",
|
||||||
"/password-reset",
|
"/password-reset",
|
||||||
"/saml-sso",
|
"/saml-sso",
|
||||||
"/login/provider/success",
|
"/login/provider/success", // TODO: change
|
||||||
"/login/provider/error"
|
"/login/provider/error", // TODO: change
|
||||||
|
"/login/sso"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const languageMap = {
|
export const languageMap = {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export * from "./secrets";
|
|||||||
export * from "./secretSnapshots";
|
export * from "./secretSnapshots";
|
||||||
export * from "./serviceAccounts";
|
export * from "./serviceAccounts";
|
||||||
export * from "./serviceTokens";
|
export * from "./serviceTokens";
|
||||||
|
export * from "./ssoConfig";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./tags";
|
export * from "./tags";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const organizationKeys = {
|
|||||||
getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const,
|
getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const,
|
||||||
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
|
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
|
||||||
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
|
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
|
||||||
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const
|
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetOrganization = () => {
|
export const useGetOrganization = () => {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
useCreateSSOConfig,
|
||||||
|
useGetSSOConfig,
|
||||||
|
useUpdateSSOConfig
|
||||||
|
} from "./queries";
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
const ssoConfigKeys = {
|
||||||
|
getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetSSOConfig = (organizationId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ssoConfigKeys.getSSOConfig(organizationId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get(
|
||||||
|
`/api/v1/sso/config?organizationId=${organizationId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateSSOConfig = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
organizationId,
|
||||||
|
authProvider,
|
||||||
|
isActive,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
authProvider: string;
|
||||||
|
isActive: boolean;
|
||||||
|
entryPoint: string;
|
||||||
|
issuer: string;
|
||||||
|
cert: string;
|
||||||
|
audience: string;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.post(
|
||||||
|
"/api/v1/sso/config",
|
||||||
|
{
|
||||||
|
organizationId,
|
||||||
|
authProvider,
|
||||||
|
isActive,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess(_, dto) {
|
||||||
|
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSSOConfig = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
organizationId,
|
||||||
|
authProvider,
|
||||||
|
isActive,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
authProvider?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
entryPoint?: string;
|
||||||
|
issuer?: string;
|
||||||
|
cert?: string;
|
||||||
|
audience?: string;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.patch(
|
||||||
|
"/api/v1/sso/config",
|
||||||
|
{
|
||||||
|
organizationId,
|
||||||
|
...(authProvider !== undefined ? { authProvider } : {}),
|
||||||
|
...(isActive !== undefined ? { isActive } : {}),
|
||||||
|
...(entryPoint !== undefined ? { entryPoint } : {}),
|
||||||
|
...(issuer !== undefined ? { issuer } : {}),
|
||||||
|
...(cert !== undefined ? { cert } : {}),
|
||||||
|
...(audience !== undefined ? { audience } : {})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess(_, dto) {
|
||||||
|
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ export type SubscriptionPlan = {
|
|||||||
workspaceLimit: number;
|
workspaceLimit: number;
|
||||||
workspacesUsed: number;
|
workspacesUsed: number;
|
||||||
environmentLimit: number;
|
environmentLimit: number;
|
||||||
|
samlSSO: boolean;
|
||||||
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
||||||
trial_end: number | null;
|
trial_end: number | null;
|
||||||
has_used_trial: boolean;
|
has_used_trial: boolean;
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import jwt_decode from "jwt-decode";
|
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
|
||||||
import SecurityClient, { PROVIDER_AUTH_TOKEN_KEY } from "@app/components/utilities/SecurityClient";
|
|
||||||
|
|
||||||
export const useProviderAuth = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { providerAuthToken: redirectedProviderAuthToken } = router.query;
|
|
||||||
const [email, setEmail] = useState<string>("");
|
|
||||||
const [userId, setUserId] = useState<string>("");
|
|
||||||
const [providerAuthToken, setProviderAuthToken] = useState<string>(
|
|
||||||
redirectedProviderAuthToken as string || ""
|
|
||||||
);
|
|
||||||
const [isProviderUserCompleted, setIsProviderUserCompleted] = useState<boolean>();
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
const AUTH_ERROR_KEY = "PROVIDER_AUTH_ERROR"
|
|
||||||
|
|
||||||
const handleRedirectWithToken = () => {
|
|
||||||
if (providerAuthToken) {
|
|
||||||
const {
|
|
||||||
userId: resultUserId,
|
|
||||||
email: resultEmail,
|
|
||||||
isUserCompleted: resultIsUserCompleted,
|
|
||||||
} = jwt_decode(providerAuthToken) as any;
|
|
||||||
setEmail(resultEmail);
|
|
||||||
setUserId(resultUserId);
|
|
||||||
setIsProviderUserCompleted(resultIsUserCompleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleRedirectWithToken();
|
|
||||||
|
|
||||||
// reset when there is no redirect auth token
|
|
||||||
if (!providerAuthToken) {
|
|
||||||
SecurityClient.setProviderAuthToken("");
|
|
||||||
}
|
|
||||||
|
|
||||||
window.localStorage.removeItem(AUTH_ERROR_KEY);
|
|
||||||
|
|
||||||
const handleStorageChange = (event: StorageEvent) => {
|
|
||||||
if (event.storageArea !== localStorage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === PROVIDER_AUTH_TOKEN_KEY) {
|
|
||||||
if (event.newValue) {
|
|
||||||
const token = event.newValue;
|
|
||||||
const {
|
|
||||||
userId: resultUserId,
|
|
||||||
email: resultEmail,
|
|
||||||
isUserCompleted: resultIsUserCompleted,
|
|
||||||
} = jwt_decode(token) as any;
|
|
||||||
setIsProviderUserCompleted(resultIsUserCompleted);
|
|
||||||
setProviderAuthToken(token);
|
|
||||||
setEmail(resultEmail);
|
|
||||||
setUserId(resultUserId);
|
|
||||||
} else {
|
|
||||||
setProviderAuthToken("");
|
|
||||||
setEmail("");
|
|
||||||
setUserId("");
|
|
||||||
setIsProviderUserCompleted(false);
|
|
||||||
}
|
|
||||||
setProviderAuthToken(event.newValue || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === AUTH_ERROR_KEY) {
|
|
||||||
if (event.newValue) {
|
|
||||||
createNotification({
|
|
||||||
text: "An error has occured during login.",
|
|
||||||
type: "error",
|
|
||||||
timeoutMs: 6000,
|
|
||||||
})
|
|
||||||
|
|
||||||
window.localStorage.removeItem(AUTH_ERROR_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("storage", handleStorageChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("storage", handleStorageChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
email,
|
|
||||||
isProviderUserCompleted,
|
|
||||||
providerAuthToken,
|
|
||||||
userId,
|
|
||||||
setEmail,
|
|
||||||
setProviderAuthToken,
|
|
||||||
setUserId,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -70,7 +70,7 @@ const completeAccountInformationSignup = async ({
|
|||||||
verifier,
|
verifier,
|
||||||
organizationName,
|
organizationName,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
attributionSource,
|
...(attributionSource ? { attributionSource } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import axios from "axios"
|
|
||||||
|
|
||||||
// import ListBox from '@app/components/basic/Listbox';
|
|
||||||
import InitialLoginStep from "@app/components/login/InitialLoginStep";
|
|
||||||
import MFAStep from "@app/components/login/MFAStep";
|
|
||||||
import PasswordInputStep from "@app/components/login/PasswordInputStep";
|
|
||||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
|
||||||
import { useProviderAuth } from "@app/hooks/useProviderAuth";
|
|
||||||
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
|
||||||
|
|
||||||
import getOrganizations from "./api/organization/getOrgs";
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [step, setStep] = useState(1);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
// const lang = router.locale ?? 'en';
|
|
||||||
const {
|
|
||||||
providerAuthToken,
|
|
||||||
email: providerEmail,
|
|
||||||
setProviderAuthToken,
|
|
||||||
isProviderUserCompleted
|
|
||||||
} = useProviderAuth();
|
|
||||||
|
|
||||||
if (providerAuthToken && isProviderUserCompleted === false) {
|
|
||||||
router.push(`/signup?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// const setLanguage = async (to: string) => {
|
|
||||||
// router.push('/login', '/login', { locale: to });
|
|
||||||
// localStorage.setItem('lang', to);
|
|
||||||
// };
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// TODO(akhilmhdh): workspace will be controlled by a workspace context
|
|
||||||
const redirectToDashboard = async () => {
|
|
||||||
try {
|
|
||||||
const userOrgs = await getOrganizations();
|
|
||||||
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
|
|
||||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
|
||||||
|
|
||||||
// user details
|
|
||||||
const userDetails = await fetchUserDetails()
|
|
||||||
// send details back to client
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
|
||||||
if (queryParams && queryParams.get("callback_port")) {
|
|
||||||
const callbackPort = queryParams.get("callback_port")
|
|
||||||
|
|
||||||
// send post request to cli with details
|
|
||||||
const cliUrl = `http://localhost:${callbackPort}`
|
|
||||||
const instance = axios.create()
|
|
||||||
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
|
|
||||||
}
|
|
||||||
router.push(`/org/${userOrg}/overview`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error - Not logged in yet");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isLoggedIn()) {
|
|
||||||
redirectToDashboard();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderView = (loginStep: number) => {
|
|
||||||
if (providerAuthToken && step === 1) {
|
|
||||||
return (
|
|
||||||
<PasswordInputStep
|
|
||||||
email={providerEmail}
|
|
||||||
password={password}
|
|
||||||
providerAuthToken={providerAuthToken}
|
|
||||||
setPassword={setPassword}
|
|
||||||
setProviderAuthToken={setProviderAuthToken}
|
|
||||||
setStep={setStep}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginStep === 1) {
|
|
||||||
return <InitialLoginStep
|
|
||||||
setStep={setStep}
|
|
||||||
email={email}
|
|
||||||
setEmail={setEmail}
|
|
||||||
password={password}
|
|
||||||
setPassword={setPassword}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === 2) {
|
|
||||||
return (
|
|
||||||
<MFAStep
|
|
||||||
email={email || providerEmail}
|
|
||||||
password={password}
|
|
||||||
providerAuthToken={providerAuthToken}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
|
||||||
<Head>
|
|
||||||
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
<meta property="og:image" content="/images/message.png" />
|
|
||||||
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
|
||||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
|
||||||
</Head>
|
|
||||||
<Link href="/">
|
|
||||||
<div className="mb-4 mt-20 flex justify-center">
|
|
||||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
{renderView(step)}
|
|
||||||
{/* <div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
|
|
||||||
<div className="mx-auto w-48">
|
|
||||||
<ListBox
|
|
||||||
isSelected={lang}
|
|
||||||
onChange={setLanguage}
|
|
||||||
data={['en', 'ko', 'fr', 'pt-BR']}
|
|
||||||
isFull
|
|
||||||
text={`${t('common.language')}: `}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { Login } from "@app/views/Login";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
||||||
|
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||||
|
</Head>
|
||||||
|
<Link href="/">
|
||||||
|
<div className="mb-4 mt-20 flex justify-center">
|
||||||
|
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<Login />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
import { LoginSSO } from "@app/views/Login";
|
||||||
|
|
||||||
|
export default function LoginSSOPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const token = router.query.token as string;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
||||||
|
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||||
|
</Head>
|
||||||
|
<Link href="/">
|
||||||
|
<div className="mb-4 mt-20 flex justify-center">
|
||||||
|
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<LoginSSO providerAuthToken={token} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { Button, Input } from "@app/components/v2";
|
|
||||||
import { isLoggedIn } from "@app/reactQuery";
|
|
||||||
|
|
||||||
import getOrganizations from "./api/organization/getOrgs";
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// TODO(akhilmhdh): workspace will be controlled by a workspace context
|
|
||||||
const redirectToDashboard = async () => {
|
|
||||||
let userOrg;
|
|
||||||
try {
|
|
||||||
const userOrgs = await getOrganizations();
|
|
||||||
userOrg = userOrgs[0] && userOrgs[0]._id;
|
|
||||||
router.push(`/org/${userOrg}/overview`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error - Not logged in yet");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isLoggedIn()) {
|
|
||||||
redirectToDashboard();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
|
||||||
<Head>
|
|
||||||
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
<meta property="og:image" content="/images/message.png" />
|
|
||||||
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
|
||||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
|
||||||
</Head>
|
|
||||||
<Link href="/">
|
|
||||||
<div className="mb-4 mt-20 flex justify-center">
|
|
||||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<div className="mx-auto w-full max-w-md md:px-6">
|
|
||||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
|
||||||
What’s your email?
|
|
||||||
</p>
|
|
||||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
|
|
||||||
<Input
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
type="email"
|
|
||||||
placeholder="Enter your email..."
|
|
||||||
isRequired
|
|
||||||
autoComplete="email"
|
|
||||||
id="email"
|
|
||||||
className="h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {}}
|
|
||||||
isFullWidth
|
|
||||||
className="h-14"
|
|
||||||
>
|
|
||||||
{t("login.login")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center justify-center mt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => {router.push("/login")}}
|
|
||||||
type="button"
|
|
||||||
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
|
||||||
>
|
|
||||||
{t("login.other-option")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -13,10 +13,8 @@ import TeamInviteStep from "@app/components/signup/TeamInviteStep";
|
|||||||
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { useProviderAuth } from "@app/hooks/useProviderAuth";
|
import checkEmailVerificationCode from "@app/pages/api/auth/CheckEmailVerificationCode";
|
||||||
|
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||||
import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode";
|
|
||||||
import getOrganizations from "./api/organization/getOrgs";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns the signup page
|
* @returns the signup page
|
||||||
@@ -35,15 +33,6 @@ export default function SignUp() {
|
|||||||
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
|
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
|
||||||
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { email: providerEmail, providerAuthToken, isProviderUserCompleted } = useProviderAuth();
|
|
||||||
|
|
||||||
if (providerAuthToken && isProviderUserCompleted) {
|
|
||||||
router.push(`/login?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerAuthToken && step < 3) {
|
|
||||||
setStep(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tryAuth = async () => {
|
const tryAuth = async () => {
|
||||||
@@ -121,7 +110,7 @@ export default function SignUp() {
|
|||||||
return (
|
return (
|
||||||
<UserInfoStep
|
<UserInfoStep
|
||||||
incrementStep={incrementStep}
|
incrementStep={incrementStep}
|
||||||
email={email || providerEmail}
|
email={email}
|
||||||
password={password}
|
password={password}
|
||||||
setPassword={setPassword}
|
setPassword={setPassword}
|
||||||
name={name}
|
name={name}
|
||||||
@@ -130,7 +119,7 @@ export default function SignUp() {
|
|||||||
setOrganizationName={setOrganizationName}
|
setOrganizationName={setOrganizationName}
|
||||||
attributionSource={attributionSource}
|
attributionSource={attributionSource}
|
||||||
setAttributionSource={setAttributionSource}
|
setAttributionSource={setAttributionSource}
|
||||||
providerAuthToken={providerAuthToken}
|
providerAuthToken={undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -139,7 +128,7 @@ export default function SignUp() {
|
|||||||
return (
|
return (
|
||||||
<DownloadBackupPDF
|
<DownloadBackupPDF
|
||||||
incrementStep={incrementStep}
|
incrementStep={incrementStep}
|
||||||
email={email || providerEmail}
|
email={email}
|
||||||
password={password}
|
password={password}
|
||||||
name={name}
|
name={name}
|
||||||
/>
|
/>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
import { SignupSSO } from "@app/views/Signup";
|
||||||
|
|
||||||
|
export default function SignupSSOPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const token = router.query.token as string;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("signup.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content={t("signup.og-title") as string} />
|
||||||
|
<meta name="og:description" content={t("signup.og-description") as string} />
|
||||||
|
</Head>
|
||||||
|
<div className="mb-4 mt-20 flex justify-center">
|
||||||
|
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||||
|
</div>
|
||||||
|
<SignupSSO providerAuthToken={token} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||||
|
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||||
|
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
||||||
|
|
||||||
|
import {
|
||||||
|
InitialStep,
|
||||||
|
MFAStep,
|
||||||
|
SAMLSSOStep
|
||||||
|
} from "./components";
|
||||||
|
|
||||||
|
export const Login = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO(akhilmhdh): workspace will be controlled by a workspace context
|
||||||
|
const redirectToDashboard = async () => {
|
||||||
|
try {
|
||||||
|
const userOrgs = await getOrganizations();
|
||||||
|
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
|
||||||
|
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||||
|
|
||||||
|
// user details
|
||||||
|
const userDetails = await fetchUserDetails()
|
||||||
|
// send details back to client
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams(window.location.search)
|
||||||
|
if (queryParams && queryParams.get("callback_port")) {
|
||||||
|
const callbackPort = queryParams.get("callback_port")
|
||||||
|
|
||||||
|
// send post request to cli with details
|
||||||
|
const cliUrl = `http://localhost:${callbackPort}`
|
||||||
|
const instance = axios.create()
|
||||||
|
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
|
||||||
|
}
|
||||||
|
router.push(`/org/${userOrg}/overview`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error - Not logged in yet");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
redirectToDashboard();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderView = () => {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<InitialStep
|
||||||
|
setStep={setStep}
|
||||||
|
email={email}
|
||||||
|
setEmail={setEmail}
|
||||||
|
password={password}
|
||||||
|
setPassword={setPassword}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<MFAStep
|
||||||
|
email={email}
|
||||||
|
password={password}
|
||||||
|
providerAuthToken={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<SAMLSSOStep setStep={setStep} />
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{renderView()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MFAStep,
|
||||||
|
PasswordStep
|
||||||
|
} from "./components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
providerAuthToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginSSO = ({ providerAuthToken }: Props) => {
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
isUserCompleted,
|
||||||
|
} = jwt_decode(providerAuthToken) as any;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUserCompleted) {
|
||||||
|
router.push(`/signup/sso?token=${encodeURIComponent(providerAuthToken)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUserCompleted) {
|
||||||
|
setStep(1);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderView = () => {
|
||||||
|
// TODO: consider adding a complete account step here that's uniquely for SSO
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<PasswordStep
|
||||||
|
providerAuthToken={providerAuthToken}
|
||||||
|
email={email}
|
||||||
|
password={password}
|
||||||
|
setPassword={setPassword}
|
||||||
|
setStep={setStep}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<MFAStep
|
||||||
|
providerAuthToken={providerAuthToken}
|
||||||
|
email={email}
|
||||||
|
password={password}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{renderView()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
import Error from "@app/components/basic/Error";
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
|
import { Button, Input } from "@app/components/v2";
|
||||||
|
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
email: string;
|
||||||
|
setEmail: (email: string) => void;
|
||||||
|
password: string;
|
||||||
|
setPassword: (email: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InitialStep = ({
|
||||||
|
setStep,
|
||||||
|
email,
|
||||||
|
setEmail,
|
||||||
|
password,
|
||||||
|
setPassword
|
||||||
|
}: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loginError, setLoginError] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
if (!email || !password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const queryParams = new URLSearchParams(window.location.search)
|
||||||
|
if (queryParams && queryParams.get("callback_port")) {
|
||||||
|
const callbackPort = queryParams.get("callback_port")
|
||||||
|
|
||||||
|
// attemptCliLogin
|
||||||
|
const isCliLoginSuccessful = await attemptCliLogin({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||||
|
|
||||||
|
if (isCliLoginSuccessful.mfaEnabled) {
|
||||||
|
// case: login requires MFA step
|
||||||
|
setStep(1);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// case: login was successful
|
||||||
|
const cliUrl = `http://localhost:${callbackPort}`
|
||||||
|
|
||||||
|
// send request to server endpoint
|
||||||
|
const instance = axios.create()
|
||||||
|
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
|
||||||
|
console.log(cliResp)
|
||||||
|
|
||||||
|
// cli page
|
||||||
|
router.push("/cli-redirect");
|
||||||
|
|
||||||
|
// on success, router.push to cli Login Successful page
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const isLoginSuccessful = await attemptLogin({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||||
|
// case: login was successful
|
||||||
|
|
||||||
|
if (isLoginSuccessful.mfaEnabled) {
|
||||||
|
// case: login requires MFA step
|
||||||
|
setStep(1);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const userOrgs = await getOrganizations();
|
||||||
|
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||||
|
|
||||||
|
// case: login does not require MFA step
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully logged in",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
router.push(`/org/${userOrg}/overview`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setLoginError(true);
|
||||||
|
createNotification({
|
||||||
|
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
|
||||||
|
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
|
||||||
|
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<Input
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter your email..."
|
||||||
|
isRequired
|
||||||
|
autoComplete="username"
|
||||||
|
className="h-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<Input
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password..."
|
||||||
|
isRequired
|
||||||
|
autoComplete="current-password"
|
||||||
|
id="current-password"
|
||||||
|
className="h-12 select:-webkit-autofill:focus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||||
|
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
isFullWidth
|
||||||
|
className='h-12'
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="solid"
|
||||||
|
isLoading={isLoading}
|
||||||
|
> Login </Button>
|
||||||
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'>
|
||||||
|
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||||
|
<span className='px-4 text-sm text-bunker-400'>or</span>
|
||||||
|
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||||
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("/api/v1/sso/redirect/google");
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||||
|
className="h-14 w-full mx-0"
|
||||||
|
>
|
||||||
|
{t("login.continue-with-google")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
setStep(2);
|
||||||
|
}}
|
||||||
|
isFullWidth
|
||||||
|
className="h-14 w-full mx-0"
|
||||||
|
>
|
||||||
|
Continue with SAML SSO
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
|
||||||
|
<span className="mr-1">Don't have an acount yet?</span>
|
||||||
|
<Link href="/signup">
|
||||||
|
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="text-bunker-400 text-sm flex flex-row">
|
||||||
|
<span className="mr-1">Forgot password?</span>
|
||||||
|
<Link href="/verify-email">
|
||||||
|
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { InitialStep } from "./InitialStep";
|
||||||
+35
-24
@@ -1,18 +1,17 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ReactCodeInput from "react-code-input";
|
import ReactCodeInput from "react-code-input";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
|
|
||||||
|
import Error from "@app/components/basic/Error"; // which to notification
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
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 { Button } from "@app/components/v2";
|
||||||
import { useSendMfaToken } from "@app/hooks/api/auth";
|
import { useSendMfaToken } from "@app/hooks/api/auth";
|
||||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||||
|
|
||||||
import Error from "../basic/Error";
|
|
||||||
import { Button } from "../v2";
|
|
||||||
|
|
||||||
// The style for the verification code input
|
// The style for the verification code input
|
||||||
const props = {
|
const props = {
|
||||||
inputStyle: {
|
inputStyle: {
|
||||||
@@ -33,6 +32,12 @@ const props = {
|
|||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface VerifyMfaTokenError {
|
interface VerifyMfaTokenError {
|
||||||
response: {
|
response: {
|
||||||
data: {
|
data: {
|
||||||
@@ -45,23 +50,12 @@ interface VerifyMfaTokenError {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const MFAStep = ({
|
||||||
* 2nd step of login - users enter their MFA code
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.email - email of user
|
|
||||||
* @param {String} obj.password - password of user
|
|
||||||
* @param {Function} obj.setStep - function to set the login flow step
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function MFAStep({
|
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken,
|
providerAuthToken
|
||||||
}: {
|
}: Props) => {
|
||||||
email: string;
|
const { createNotification } = useNotificationContext();
|
||||||
password: string;
|
|
||||||
providerAuthToken?: string;
|
|
||||||
}): JSX.Element {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||||
@@ -75,6 +69,10 @@ export default function MFAStep({
|
|||||||
const handleLoginMfa = async () => {
|
const handleLoginMfa = async () => {
|
||||||
try {
|
try {
|
||||||
if (mfaCode.length !== 6) {
|
if (mfaCode.length !== 6) {
|
||||||
|
createNotification({
|
||||||
|
text: "Please enter a 6-digit MFA code and try again",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +114,25 @@ export default function MFAStep({
|
|||||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||||
|
|
||||||
// case: login does not require MFA step
|
// case: login does not require MFA step
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully logged in",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
router.push(`/org/${userOrg}/overview`);
|
router.push(`/org/${userOrg}/overview`);
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to log in",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as VerifyMfaTokenError;
|
const error = err as VerifyMfaTokenError;
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to log in",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
|
||||||
if (error?.response?.status === 500) {
|
if (error?.response?.status === 500) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -147,8 +158,8 @@ export default function MFAStep({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="mx-auto w-max md:px-8 pb-4 pt-4 md:mb-16">
|
<form className="mx-auto w-max md:px-8 pb-4 pt-4 md:mb-16">
|
||||||
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
||||||
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email} </p>
|
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email} </p>
|
||||||
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
|
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
|
||||||
@@ -204,6 +215,6 @@ export default function MFAStep({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-bunker-400 pb-2">{t("signup.step2-spam-alert")}</p>
|
<p className="text-sm text-bunker-400 pb-2">{t("signup.step2-spam-alert")}</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { MFAStep } from "./MFAStep";
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
|
import { Button, Input } from "@app/components/v2";
|
||||||
|
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
providerAuthToken: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
setPassword: (password: string) => void;
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordStep = ({
|
||||||
|
providerAuthToken,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
setPassword,
|
||||||
|
setStep
|
||||||
|
}: Props) => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const loginAttempt = await attemptLogin({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
providerAuthToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loginAttempt && loginAttempt.success) {
|
||||||
|
// case: login was successful
|
||||||
|
|
||||||
|
if (loginAttempt.mfaEnabled) {
|
||||||
|
// TODO: deal with MFA
|
||||||
|
// case: login requires MFA step
|
||||||
|
setIsLoading(false);
|
||||||
|
setStep(2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: login does not require MFA step
|
||||||
|
const userOrgs = await getOrganizations();
|
||||||
|
const userOrg = userOrgs[0]._id;
|
||||||
|
setIsLoading(false);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully logged in",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
router.push(`/org/${userOrg?._id}/overview`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setIsLoading(false);
|
||||||
|
createNotification({
|
||||||
|
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
className="h-full mx-auto w-full max-w-md px-6 pt-8"
|
||||||
|
>
|
||||||
|
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||||
|
What’s your Infisical Password?
|
||||||
|
</p>
|
||||||
|
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<Input
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password..."
|
||||||
|
isRequired
|
||||||
|
autoComplete="current-password"
|
||||||
|
id="current-password"
|
||||||
|
className="h-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={async () => handleLogin()}
|
||||||
|
isFullWidth
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="h-14"
|
||||||
|
>
|
||||||
|
{t("login.login")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
|
||||||
|
<span className='duration-200 max-w-sm text-center px-4'>
|
||||||
|
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
|
||||||
|
</span>
|
||||||
|
<Link href="/verify-email">
|
||||||
|
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
router.push("/login");
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("login.other-option")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PasswordStep } from "./PasswordStep";
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Button, Input } from "@app/components/v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SAMLSSOStep = ({
|
||||||
|
setStep
|
||||||
|
}: Props) => {
|
||||||
|
const [ssoIdentifier, setSSOIdentifier] = useState("");
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md md:px-6">
|
||||||
|
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||||
|
What's your SSO Identifier?
|
||||||
|
</p>
|
||||||
|
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<Input
|
||||||
|
value={ssoIdentifier}
|
||||||
|
onChange={(e) => setSSOIdentifier(e.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter your SSO identifier..."
|
||||||
|
isRequired
|
||||||
|
autoComplete="email"
|
||||||
|
id="email"
|
||||||
|
className="h-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(`/api/v1/sso/redirect/saml2/${ssoIdentifier}`);
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
isFullWidth
|
||||||
|
className="h-14"
|
||||||
|
>
|
||||||
|
{t("login.login")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center justify-center mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setStep(0);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("login.other-option")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SAMLSSOStep } from "./SAMLSSOStep";
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export { InitialStep } from "./InitialStep";
|
||||||
|
export { MFAStep } from "./MFAStep";
|
||||||
|
export { SAMLSSOStep } from "./SAMLSSOStep";
|
||||||
|
|
||||||
|
// SSO-specific step
|
||||||
|
export { PasswordStep } from "./PasswordStep";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { Login } from "./Login";
|
||||||
|
export { LoginSSO } from "./LoginSSO";
|
||||||
+2
-1
@@ -7,7 +7,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input} from "@app/components/v2";
|
Input
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetOrgBillingDetails,
|
useGetOrgBillingDetails,
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ export const PmtMethodsSection = () => {
|
|||||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||||
Payment Methods
|
Payment methods
|
||||||
</h2>
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddPmtMethodBtnClick}
|
onClick={handleAddPmtMethodBtnClick}
|
||||||
|
|||||||
+20
-19
@@ -10,7 +10,8 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem} from "@app/components/v2";
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useAddOrgTaxId } from "@app/hooks/api";
|
import { useAddOrgTaxId } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -138,22 +139,22 @@ export const TaxIDModal = ({
|
|||||||
defaultValue="eu_vat"
|
defaultValue="eu_vat"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Type"
|
label="Type"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(e) => onChange(e)}
|
onValueChange={(e) => onChange(e)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{taxIDTypes.map(({ label, value }) => (
|
{taxIDTypes.map(({ label, value }) => (
|
||||||
<SelectItem value={String(value || "")} key={label}>
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
{label}
|
{label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -163,9 +164,9 @@ export const TaxIDModal = ({
|
|||||||
name="value"
|
name="value"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Value"
|
label="Value"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import { OrgTabGroup } from "./components";
|
||||||
OrgIncidentContactsSection,
|
|
||||||
OrgNameChangeSection,
|
|
||||||
OrgServiceAccountsTable
|
|
||||||
} from "./components";
|
|
||||||
|
|
||||||
export const OrgSettingsPage = () => {
|
export const OrgSettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -15,11 +11,7 @@ export const OrgSettingsPage = () => {
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
|
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
|
||||||
</div>
|
</div>
|
||||||
<OrgNameChangeSection />
|
<OrgTabGroup />
|
||||||
<OrgIncidentContactsSection />
|
|
||||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
|
||||||
<OrgServiceAccountsTable />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { OrgSSOSection } from "./OrgSSOSection";
|
||||||
|
|
||||||
|
export const OrgAuthTab = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<OrgSSOSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
|
||||||
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
|
import {
|
||||||
|
useGetSSOConfig,
|
||||||
|
useUpdateSSOConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { SSOModal } from "./SSOModal";
|
||||||
|
|
||||||
|
const ssoAuthProviderMap: { [key: string]: string } = {
|
||||||
|
"okta-saml": "Okta SAML 2.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrgSSOSection = (): JSX.Element => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||||
|
const { mutateAsync } = useUpdateSSOConfig();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"upgradePlan",
|
||||||
|
"addSSO"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const handleSamlSSOToggle = async (value: boolean) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
organizationId: currentOrg?._id,
|
||||||
|
isActive: value
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${value ? "enable" : "disable"} SAML SSO`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||||
|
<div className="flex items-center mb-8">
|
||||||
|
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||||
|
Configuration
|
||||||
|
</h2>
|
||||||
|
{!isLoading && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.samlSSO) {
|
||||||
|
handlePopUpOpen("addSSO");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
colorSchema="secondary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
{data ? "Update SAML SSO" : "Set up SAML SSO"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isLoading && data && (
|
||||||
|
<>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Switch
|
||||||
|
id="enable-saml-sso"
|
||||||
|
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||||
|
isChecked={data.isActive}
|
||||||
|
>
|
||||||
|
Enable SAML SSO
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
|
||||||
|
<p className="text-gray-400 text-md">{data._id}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">Type</h3>
|
||||||
|
<p className="text-gray-400 text-md">{ssoAuthProviderMap[data.authProvider]}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">Audience</h3>
|
||||||
|
<p className="text-gray-400 text-md">{data.audience}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
|
||||||
|
<p className="text-gray-400 text-md">{data.entryPoint}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
|
||||||
|
<p className="text-gray-400 text-md">{data.issuer}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SSOModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You can use SAML SSO if you switch to Infisical's Pro plan."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateSSOConfig,
|
||||||
|
useGetSSOConfig,
|
||||||
|
useUpdateSSOConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const ssoAuthProviders = [
|
||||||
|
{ label: "Okta SAML 2.0", value: "okta-saml" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
authProvider: yup.string().required("SSO Type is required"),
|
||||||
|
entryPoint: yup.string().required("IDP entrypoint is required"),
|
||||||
|
issuer: yup.string().required("Issuer string is required"),
|
||||||
|
cert: yup.string().required("IDP's public signing certificate is required"),
|
||||||
|
audience: yup.string().required("Expected SAML response audience is required"),
|
||||||
|
}).required();
|
||||||
|
|
||||||
|
export type AddSSOFormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["addSSO"]>;
|
||||||
|
handlePopUpClose: (popUpName: keyof UsePopUpState<["addSSO"]>) => void;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addSSO"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SSOModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpClose,
|
||||||
|
handlePopUpToggle
|
||||||
|
}: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateSSOConfig();
|
||||||
|
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateSSOConfig();
|
||||||
|
const { data } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
} = useForm<AddSSOFormData>({
|
||||||
|
defaultValues: {
|
||||||
|
authProvider: "okta-saml"
|
||||||
|
},
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset({
|
||||||
|
authProvider: data?.authProvider ?? "",
|
||||||
|
entryPoint: data?.entryPoint ?? "",
|
||||||
|
issuer: data?.issuer ?? "",
|
||||||
|
cert: data?.cert ?? "",
|
||||||
|
audience: data?.audience ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onSSOModalSubmit = async ({
|
||||||
|
authProvider,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
}: AddSSOFormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg) return;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
await createMutateAsync({
|
||||||
|
organizationId: currentOrg._id,
|
||||||
|
authProvider,
|
||||||
|
isActive: false,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await updateMutateAsync({
|
||||||
|
organizationId: currentOrg._id,
|
||||||
|
authProvider,
|
||||||
|
isActive: false,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert,
|
||||||
|
audience
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpClose("addSSO");
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${!data ? "added" : "updated"} SAML SSO configuration`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${!data ? "add" : "update"} SAML SSO configuration`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const authProvider = watch("authProvider");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addSSO?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addSSO", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title="Add SSO">
|
||||||
|
<form onSubmit={handleSubmit(onSSOModalSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="authProvider"
|
||||||
|
defaultValue="okta-saml"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Type"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{ssoAuthProviders.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{authProvider && authProvider === "okta-saml" && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="audience"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Audience"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="https://your-domain.com"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="entryPoint"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Entrypoint"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="https://your-domain.okta.com/app/app-name/xxx/sso/saml"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="issuer"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Issuer"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="http://www.okta.com/xxx"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="cert"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Certificate"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- ..."
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={createIsLoading || updateIsLoading}
|
||||||
|
>
|
||||||
|
{!data ? "Add" : "Update"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose("addSSO")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgAuthTab } from "./OrgAuthTab";
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
|
||||||
|
import { OrgNameChangeSection } from "../OrgNameChangeSection";
|
||||||
|
import { OrgServiceAccountsTable } from "../OrgServiceAccountsTable";
|
||||||
|
|
||||||
|
export const OrgGeneralTab = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<OrgNameChangeSection />
|
||||||
|
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
|
||||||
|
<OrgServiceAccountsTable />
|
||||||
|
</div>
|
||||||
|
<OrgIncidentContactsSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgGeneralTab } from "./OrgGeneralTab";
|
||||||
+1
-1
@@ -17,7 +17,7 @@ export const OrgIncidentContactsSection = () => {
|
|||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex justify-between mb-4">
|
<div className="flex justify-between mb-4">
|
||||||
<p className="min-w-max text-xl font-semibold">
|
<p className="min-w-max text-xl font-semibold">
|
||||||
{t("section.incident.incident-contacts")}
|
{t("section.incident.incident-contacts")}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Fragment } from "react"
|
||||||
|
import { Tab } from "@headlessui/react"
|
||||||
|
|
||||||
|
import { useOrganization,useUser } from "@app/context";
|
||||||
|
import {
|
||||||
|
useGetOrgUsers
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { OrgAuthTab } from "../OrgAuthTab";
|
||||||
|
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||||
|
|
||||||
|
export const OrgTabGroup = () => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { user } = useUser();
|
||||||
|
const { data } = useGetOrgUsers(currentOrg?._id ?? "");
|
||||||
|
|
||||||
|
const isRoleSufficient = data?.some((orgUser) => {
|
||||||
|
return orgUser.role !== "member" && orgUser.user._id === user._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: "General", key: "tab-org-general" },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isRoleSufficient) {
|
||||||
|
tabs.push(
|
||||||
|
{ name: "SAML SSO", key: "tab-org-saml" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab as={Fragment} key={tab.key}>
|
||||||
|
{({ selected }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel>
|
||||||
|
<OrgGeneralTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
{isRoleSufficient && (
|
||||||
|
<Tab.Panel>
|
||||||
|
<OrgAuthTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
)}
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgTabGroup } from "./OrgTabGroup";
|
||||||
@@ -1,3 +1 @@
|
|||||||
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
|
export { OrgTabGroup } from "./OrgTabGroup";
|
||||||
export { OrgNameChangeSection } from "./OrgNameChangeSection";
|
|
||||||
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BackupPDFStep,
|
||||||
|
UserInfoSSOStep} from "./components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
providerAuthToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignupSSO = ({
|
||||||
|
providerAuthToken
|
||||||
|
}: Props) => {
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
organizationName,
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
} = jwt_decode(providerAuthToken) as any;
|
||||||
|
|
||||||
|
const renderView = () => {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<UserInfoSSOStep
|
||||||
|
email={email}
|
||||||
|
name={`${firstName} ${lastName}`}
|
||||||
|
providerOrganizationName={organizationName}
|
||||||
|
password={password}
|
||||||
|
setPassword={setPassword}
|
||||||
|
setStep={setStep}
|
||||||
|
providerAuthToken={providerAuthToken}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<BackupPDFStep
|
||||||
|
email={email}
|
||||||
|
password={password}
|
||||||
|
name={`${firstName} ${lastName}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{renderView()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
|
||||||
|
interface DownloadBackupPDFStepProps {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the step of the signup flow where the user downloads the backup pdf
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
|
||||||
|
* @param {string} obj.email - user's email
|
||||||
|
* @param {string} obj.password - user's password
|
||||||
|
* @param {string} obj.name - user's name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const BackupPDFStep = ({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name
|
||||||
|
}: DownloadBackupPDFStepProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
|
||||||
|
<p className="text-xl text-center font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />{t("signup.step4-message")}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col pb-2 bg-mineshaft-800 border border-mineshaft-600 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
|
||||||
|
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 lg:w-1/6 w-full md:min-w-[23rem] px-3 mx-auto">
|
||||||
|
<span className='mb-2'>{t("signup.step4-description1")} {t("signup.step4-description3")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center px-3 justify-center mt-0 md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||||
|
<div className="text-l py-1 text-lg w-full">
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await issueBackupKey({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
personalName: name,
|
||||||
|
setBackupKeyError: () => { },
|
||||||
|
setBackupKeyIssued: () => { }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
isFullWidth
|
||||||
|
className='h-12'
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
> Download PDF </Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { BackupPDFStep } from "./BackupPDFStep";
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import jsrp from "jsrp";
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
import { encodeBase64 } from "tweetnacl-util";
|
||||||
|
|
||||||
|
import InputField from "@app/components/basic/InputField";
|
||||||
|
import checkPassword from "@app/components/utilities/checks/checkPassword";
|
||||||
|
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||||
|
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
import { Button, Input } from "@app/components/v2";
|
||||||
|
import { useGetCommonPasswords } from "@app/hooks/api";
|
||||||
|
import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup";
|
||||||
|
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||||
|
import ProjectService from "@app/services/ProjectService";
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const client = new jsrp.client();
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
setPassword: (value: string) => void;
|
||||||
|
name: string;
|
||||||
|
providerOrganizationName: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Errors = {
|
||||||
|
length?: string,
|
||||||
|
upperCase?: string,
|
||||||
|
lowerCase?: string,
|
||||||
|
number?: string,
|
||||||
|
specialChar?: string,
|
||||||
|
repeatedChar?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the step of the sign up flow where people provife their name/surname and password
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} obj.verificationToken - the token which we use to verify the legitness of a user
|
||||||
|
* @param {string} obj.incrementStep - a function to move to the next signup step
|
||||||
|
* @param {string} obj.email - email of a user who is signing up
|
||||||
|
* @param {string} obj.password - user's password
|
||||||
|
* @param {string} obj.setPassword - function managing the state of user's password
|
||||||
|
* @param {string} obj.firstName - user's first name
|
||||||
|
* @param {string} obj.setFirstName - function managing the state of user's first name
|
||||||
|
* @param {string} obj.lastName - user's lastName
|
||||||
|
* @param {string} obj.setLastName - function managing the state of user's last name
|
||||||
|
*/
|
||||||
|
export const UserInfoSSOStep = ({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
providerOrganizationName,
|
||||||
|
password,
|
||||||
|
setPassword,
|
||||||
|
setStep,
|
||||||
|
providerAuthToken,
|
||||||
|
}: Props) => {
|
||||||
|
const { data: commonPasswords } = useGetCommonPasswords();
|
||||||
|
const [nameError, setNameError] = useState(false);
|
||||||
|
const [organizationName, setOrganizationName] = useState("");
|
||||||
|
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("providerOrganizationName: ", providerOrganizationName);
|
||||||
|
if (providerOrganizationName) {
|
||||||
|
setOrganizationName(providerOrganizationName);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Verifies if the information that the users entered (name, workspace)
|
||||||
|
// is there, and if the password matches the criteria.
|
||||||
|
const signupErrorCheck = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
let errorCheck = false;
|
||||||
|
if (!name) {
|
||||||
|
setNameError(true);
|
||||||
|
errorCheck = true;
|
||||||
|
} else {
|
||||||
|
setNameError(false);
|
||||||
|
}
|
||||||
|
if (!organizationName) {
|
||||||
|
setOrganizationNameError(true);
|
||||||
|
errorCheck = true;
|
||||||
|
} else {
|
||||||
|
setOrganizationNameError(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorCheck = checkPassword({
|
||||||
|
password,
|
||||||
|
commonPasswords,
|
||||||
|
setErrors
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!errorCheck) {
|
||||||
|
// Generate a random pair of a public and a private key
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
|
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||||
|
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||||
|
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||||
|
|
||||||
|
console.log("make");
|
||||||
|
console.log("email: ", email);
|
||||||
|
console.log("password: ", password);
|
||||||
|
|
||||||
|
client.init(
|
||||||
|
{
|
||||||
|
username: email,
|
||||||
|
password
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||||
|
try {
|
||||||
|
// TODO: moduralize into KeyService
|
||||||
|
const derivedKey = await deriveArgonKey({
|
||||||
|
password,
|
||||||
|
salt: result.salt,
|
||||||
|
mem: 65536,
|
||||||
|
time: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLen: 32
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
// create encrypted private key by encrypting the private
|
||||||
|
// key with the symmetric key [key]
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: privateKey,
|
||||||
|
secret: key
|
||||||
|
});
|
||||||
|
|
||||||
|
// create the protected key by encrypting the symmetric key
|
||||||
|
// [key] with the derived key
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: key.toString("hex"),
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await completeAccountInformationSignup({
|
||||||
|
email,
|
||||||
|
firstName: name.split(" ")[0],
|
||||||
|
lastName: name.split(" ").slice(1).join(" "),
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
providerAuthToken,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier,
|
||||||
|
organizationName
|
||||||
|
});
|
||||||
|
|
||||||
|
// unset signup JWT token and set JWT token
|
||||||
|
SecurityClient.setSignupToken("");
|
||||||
|
SecurityClient.setToken(response.token);
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
|
||||||
|
saveTokenToLocalStorage({
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const userOrgs = await getOrganizations();
|
||||||
|
const orgId = userOrgs[0]?._id;
|
||||||
|
const project = await ProjectService.initProject({
|
||||||
|
organizationId: orgId,
|
||||||
|
projectName: "Example Project"
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("orgData.id", orgId);
|
||||||
|
localStorage.setItem("projectData.id", project._id);
|
||||||
|
|
||||||
|
setStep(1);
|
||||||
|
} catch (error) {
|
||||||
|
setIsLoading(false);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16">
|
||||||
|
<p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
|
||||||
|
{t("signup.step3-message")}
|
||||||
|
</p>
|
||||||
|
<div className="h-full mx-auto mb-36 w-max rounded-xl py-6 md:px-8 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800">
|
||||||
|
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
|
||||||
|
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p>
|
||||||
|
<Input
|
||||||
|
placeholder="Jane Doe"
|
||||||
|
value={name}
|
||||||
|
disabled
|
||||||
|
isRequired
|
||||||
|
autoComplete="given-name"
|
||||||
|
className="h-12"
|
||||||
|
/>
|
||||||
|
{nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>}
|
||||||
|
</div>
|
||||||
|
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
|
||||||
|
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p>
|
||||||
|
<Input
|
||||||
|
placeholder="Infisical"
|
||||||
|
value={organizationName}
|
||||||
|
onChange={(e) => setOrganizationName(e.target.value)}
|
||||||
|
isRequired
|
||||||
|
className="h-12"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
{organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
|
||||||
|
<InputField
|
||||||
|
label={t("section.password.password")}
|
||||||
|
onChangeHandler={(pass: string) => {
|
||||||
|
setPassword(pass);
|
||||||
|
checkPassword({
|
||||||
|
password: pass,
|
||||||
|
commonPasswords,
|
||||||
|
setErrors
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
isRequired
|
||||||
|
error={Object.keys(errors).length > 0}
|
||||||
|
autoComplete="new-password"
|
||||||
|
id="new-password"
|
||||||
|
/>
|
||||||
|
{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="ml-1 flex flex-row items-top justify-start"
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faXmark}
|
||||||
|
className="text-md text-red ml-0.5 mr-2.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
{errors[key as keyof Errors]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||||
|
<div className="text-l py-1 text-lg w-full">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={signupErrorCheck}
|
||||||
|
size="sm"
|
||||||
|
isFullWidth
|
||||||
|
className='h-14'
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
isLoading={isLoading}
|
||||||
|
> {String(t("signup.signup"))} </Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { UserInfoSSOStep } from "./UserInfoSSOStep";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { BackupPDFStep } from "./BackupPDFStep";
|
||||||
|
export { UserInfoSSOStep } from "./UserInfoSSOStep";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SignupSSO } from "./SignupSSO";
|
||||||
Reference in New Issue
Block a user