mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-27 09:40:45 +00:00
Update
This commit is contained in:
@ -25,7 +25,6 @@
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
|
3438
backend/package-lock.json
generated
3438
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||
"@godaddy/terminus": "^4.12.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.49.0",
|
||||
"@sentry/tracing": "^7.48.0",
|
||||
@ -13,6 +14,7 @@
|
||||
"axios-retry": "^3.4.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.4.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
|
@ -2,7 +2,7 @@ import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Integration } from "../../models";
|
||||
import { EventService } from "../../services";
|
||||
import { eventPushSecrets, eventStartIntegration } from "../../events";
|
||||
import { eventStartIntegration } from "../../events";
|
||||
import Folder from "../../models/folder";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import { createOrganization as create } from "../../helpers/organization";
|
||||
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
||||
import { ACCEPTED, OWNER } from "../../variables";
|
||||
import { getSiteURL, getLicenseServerUrl } from "../../config";
|
||||
import { getLicenseServerUrl, getSiteURL } from "../../config";
|
||||
import { licenseServerKeyRequest } from "../../config/request";
|
||||
|
||||
export const getOrganizations = async (req: Request, res: Response) => {
|
||||
|
@ -9,8 +9,6 @@ import {
|
||||
} from "../../types/secret";
|
||||
const { ValidationError } = mongoose.Error;
|
||||
import {
|
||||
BadRequestError,
|
||||
InternalServerError,
|
||||
ValidationError as RouteValidationError,
|
||||
UnauthorizedRequestError
|
||||
} from "../../utils/errors";
|
||||
|
@ -5,8 +5,6 @@ import { ServiceAccount, ServiceTokenData, User } from "../../models";
|
||||
import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
|
||||
import { getSaltRounds } from "../../config";
|
||||
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
|
||||
|
@ -18,7 +18,7 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
let user, token, refreshToken;
|
||||
let user;
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
@ -119,7 +119,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
const token = tokens.token;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (await getLoopsApiKey()) {
|
||||
@ -159,7 +159,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
let user, token, refreshToken;
|
||||
let user;
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
@ -244,7 +244,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
const token = tokens.token;
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie("jid", tokens.refreshToken, {
|
||||
|
@ -3,10 +3,10 @@ import { Types } from "mongoose";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import {
|
||||
MembershipOrg,
|
||||
User,
|
||||
APIKeyData,
|
||||
TokenVersion
|
||||
MembershipOrg,
|
||||
TokenVersion,
|
||||
User
|
||||
} from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
|
||||
|
@ -179,10 +179,9 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
let key;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
key = await Key.findOne({
|
||||
const key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
}).populate("sender", "+publicKey");
|
||||
|
@ -12,6 +12,7 @@ import { standardRequest } from "../../config/request";
|
||||
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { TelemetryService } from "../../services";
|
||||
import { AuthProvider } from "../../models";
|
||||
|
||||
/**
|
||||
* 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)
|
||||
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
||||
|
||||
// initialize default organization and workspace
|
||||
await initializeDefaultOrg({
|
||||
organizationName,
|
||||
user,
|
||||
});
|
||||
if (user.authProvider !== AuthProvider.OKTA_SAML) {
|
||||
// initialize default organization and workspace
|
||||
await initializeDefaultOrg({
|
||||
organizationName,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
@ -174,7 +177,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
distinctId: email,
|
||||
properties: {
|
||||
email,
|
||||
attributionSource,
|
||||
...(attributionSource ? { attributionSource } : {})
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as secretController from "./secretController";
|
||||
import * as secretSnapshotController from "./secretSnapshotController";
|
||||
import * as organizationsController from "./organizationsController";
|
||||
import * as ssoController from "./ssoController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as actionController from "./actionController";
|
||||
import * as membershipController from "./membershipController";
|
||||
@ -10,6 +11,7 @@ export {
|
||||
secretController,
|
||||
secretSnapshotController,
|
||||
organizationsController,
|
||||
ssoController,
|
||||
workspaceController,
|
||||
actionController,
|
||||
membershipController,
|
||||
|
@ -178,6 +178,12 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
|
||||
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) => {
|
||||
const { taxId } = req.params;
|
||||
|
||||
@ -188,6 +194,12 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
|
||||
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) => {
|
||||
const { data: { invoices } } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`
|
||||
|
220
backend/src/ee/controllers/v1/ssoController.ts
Normal file
220
backend/src/ee/controllers/v1/ssoController.ts
Normal file
@ -0,0 +1,220 @@
|
||||
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";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
72
backend/src/ee/helpers/organizations.ts
Normal file
72
backend/src/ee/helpers/organizations.ts
Normal file
@ -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";
|
||||
|
||||
export {
|
||||
requireLicenseAuth,
|
||||
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 Log, { ILog } from "./log";
|
||||
import Action, { IAction } from "./action";
|
||||
import SSOConfig, { ISSOConfig } from "./ssoConfig";
|
||||
|
||||
export {
|
||||
SecretSnapshot,
|
||||
@ -15,4 +16,6 @@ export {
|
||||
ILog,
|
||||
Action,
|
||||
IAction,
|
||||
SSOConfig,
|
||||
ISSOConfig
|
||||
};
|
||||
|
@ -63,9 +63,10 @@ const logSchema = new Schema<ILog>(
|
||||
ipAddress: {
|
||||
type: String,
|
||||
},
|
||||
}, {
|
||||
timestamps: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
const Log = model<ILog>("Log", logSchema);
|
||||
|
82
backend/src/ee/models/ssoConfig.ts
Normal file
82
backend/src/ee/models/ssoConfig.ts
Normal file
@ -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 secretSnapshot from "./secretSnapshot";
|
||||
import organizations from "./organizations";
|
||||
import sso from "./sso";
|
||||
import workspace from "./workspace";
|
||||
import action from "./action";
|
||||
import cloudProducts from "./cloudProducts";
|
||||
@ -9,6 +10,7 @@ export {
|
||||
secret,
|
||||
secretSnapshot,
|
||||
organizations,
|
||||
sso,
|
||||
workspace,
|
||||
action,
|
||||
cloudProducts,
|
||||
|
101
backend/src/ee/routes/v1/sso.ts
Normal file
101
backend/src/ee/routes/v1/sso.ts
Normal file
@ -0,0 +1,101 @@
|
||||
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";
|
||||
import {
|
||||
getSiteURL
|
||||
} from "../../../config";
|
||||
|
||||
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
|
||||
}),
|
||||
async (req, res) => {
|
||||
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)}`);
|
||||
}
|
||||
);
|
||||
|
||||
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,7 @@ interface FeatureSet {
|
||||
customRateLimits: boolean;
|
||||
customAlerts: boolean;
|
||||
auditLogs: 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;
|
||||
has_used_trial: boolean;
|
||||
}
|
||||
|
46
backend/src/helpers/botOrg.ts
Normal file
46
backend/src/helpers/botOrg.ts
Normal file
@ -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"
|
||||
});
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import express from "express";
|
||||
import bodyParser from "body-parser";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require("express-async-errors");
|
||||
import helmet from "helmet";
|
||||
@ -20,6 +21,7 @@ import {
|
||||
organizations as eeOrganizationsRouter,
|
||||
secret as eeSecretRouter,
|
||||
secretSnapshot as eeSecretSnapshotRouter,
|
||||
sso as eeSSORouter,
|
||||
workspace as eeWorkspaceRouter
|
||||
} from "./ee/routes/v1";
|
||||
import {
|
||||
@ -40,21 +42,21 @@ import {
|
||||
signup as v1SignupRouter,
|
||||
userAction as v1UserActionRouter,
|
||||
user as v1UserRouter,
|
||||
workspace as v1WorkspaceRouter,
|
||||
webhooks as v1WebhooksRouter
|
||||
webhooks as v1WebhooksRouter,
|
||||
workspace as v1WorkspaceRouter
|
||||
} from "./routes/v1";
|
||||
import {
|
||||
auth as v2AuthRouter,
|
||||
signup as v2SignupRouter,
|
||||
users as v2UsersRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
secret as v2SecretRouter, // begin to phase out
|
||||
secrets as v2SecretsRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
serviceAccounts as v2ServiceAccountsRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
tags as v2TagsRouter
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
signup as v2SignupRouter,
|
||||
tags as v2TagsRouter,
|
||||
users as v2UsersRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
} from "./routes/v2";
|
||||
import {
|
||||
auth as v3AuthRouter,
|
||||
@ -77,6 +79,7 @@ const main = async () => {
|
||||
const app = express();
|
||||
app.enable("trust proxy");
|
||||
app.use(express.json());
|
||||
app.use(bodyParser.urlencoded({extended: true}));
|
||||
app.use(cookieParser());
|
||||
app.use(
|
||||
cors({
|
||||
@ -106,6 +109,7 @@ const main = async () => {
|
||||
app.use("/api/v1/workspace", eeWorkspaceRouter);
|
||||
app.use("/api/v1/action", eeActionRouter);
|
||||
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
||||
app.use("/api/v1/sso", eeSSORouter);
|
||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
||||
|
||||
// v1 routes (default)
|
||||
|
@ -18,7 +18,6 @@ const revokeAccess = async ({
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let deletedIntegrationAuth;
|
||||
// add any integration-specific revocation logic
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
@ -33,7 +32,7 @@ const revokeAccess = async ({
|
||||
break;
|
||||
}
|
||||
|
||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuth._id,
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Types } from "mongoose";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import {
|
||||
getAuthAPIKeyPayload,
|
||||
@ -51,6 +52,10 @@ const requireAuth = ({
|
||||
});
|
||||
|
||||
let authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
let authUserPayload: {
|
||||
user: IUser;
|
||||
tokenVersionId: Types.ObjectId;
|
||||
};
|
||||
switch (authMode) {
|
||||
case AUTH_MODE_SERVICE_ACCOUNT:
|
||||
authPayload = await getAuthSAAKPayload({
|
||||
@ -71,12 +76,12 @@ const requireAuth = ({
|
||||
req.user = authPayload;
|
||||
break;
|
||||
default:
|
||||
const { user, tokenVersionId } = await getAuthUserPayload({
|
||||
authUserPayload = await getAuthUserPayload({
|
||||
authTokenValue,
|
||||
});
|
||||
authPayload = user;
|
||||
req.user = user;
|
||||
req.tokenVersionId = tokenVersionId;
|
||||
authPayload = authUserPayload.user;
|
||||
req.user = authUserPayload.user;
|
||||
req.tokenVersionId = authUserPayload.tokenVersionId;
|
||||
break;
|
||||
}
|
||||
|
||||
|
98
backend/src/models/botOrg.ts
Normal file
98
backend/src/models/botOrg.ts
Normal file
@ -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 Bot, { IBot } from "./bot";
|
||||
import BotOrg, { IBotOrg } from "./botOrg";
|
||||
import BotKey, { IBotKey } from "./botKey";
|
||||
import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg";
|
||||
import Integration, { IIntegration } from "./integration";
|
||||
@ -30,6 +31,8 @@ export {
|
||||
IBackupPrivateKey,
|
||||
Bot,
|
||||
IBot,
|
||||
BotOrg,
|
||||
IBotOrg,
|
||||
BotKey,
|
||||
IBotKey,
|
||||
IncidentContactOrg,
|
||||
|
@ -2,6 +2,7 @@ import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export enum AuthProvider {
|
||||
GOOGLE = "google",
|
||||
OKTA_SAML = "okta-saml"
|
||||
}
|
||||
|
||||
export interface IUser extends Document {
|
||||
|
@ -2,7 +2,7 @@ import express from "express";
|
||||
const router = express.Router();
|
||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
|
||||
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";
|
||||
|
||||
router.post(
|
||||
|
@ -21,7 +21,8 @@ router.post(
|
||||
body("salt").exists().isString().trim().notEmpty(),
|
||||
body("verifier").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,
|
||||
signupController.completeAccountSignup,
|
||||
);
|
||||
|
12
backend/src/services/BotOrgService.ts
Normal file
12
backend/src/services/BotOrgService.ts
Normal file
@ -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 TelemetryService from "./TelemetryService";
|
||||
import BotService from "./BotService";
|
||||
import BotOrgService from "./BotOrgService";
|
||||
import EventService from "./EventService";
|
||||
import IntegrationService from "./IntegrationService";
|
||||
import TokenService from "./TokenService";
|
||||
@ -11,6 +12,7 @@ export {
|
||||
TelemetryService,
|
||||
DatabaseService,
|
||||
BotService,
|
||||
BotOrgService,
|
||||
EventService,
|
||||
IntegrationService,
|
||||
TokenService,
|
||||
|
1
backend/src/types/express/index.d.ts
vendored
1
backend/src/types/express/index.d.ts
vendored
@ -20,6 +20,7 @@ declare global {
|
||||
workspace: any;
|
||||
membership: any;
|
||||
targetMembership: any;
|
||||
isUserCompleted: boolean;
|
||||
providerAuthToken: any;
|
||||
organization: any;
|
||||
membershipOrg: any;
|
||||
|
@ -1,11 +1,14 @@
|
||||
import express from "express";
|
||||
import passport from "passport";
|
||||
import { Types } from "mongoose";
|
||||
import { AuthData } from "../interfaces/middleware";
|
||||
import {
|
||||
AuthProvider,
|
||||
ServiceAccount,
|
||||
ServiceTokenData,
|
||||
User,
|
||||
Organization,
|
||||
MembershipOrg
|
||||
} from "../models";
|
||||
import { createToken } from "../helpers/auth";
|
||||
import {
|
||||
@ -14,9 +17,14 @@ import {
|
||||
getJwtProviderAuthLifetime,
|
||||
getJwtProviderAuthSecret,
|
||||
} from "../config";
|
||||
import { getSSOConfigHelper } from "../ee/helpers/organizations";
|
||||
import { OrganizationNotFoundError } from "./errors";
|
||||
import { MEMBER, INVITED } from "../variables";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
|
||||
|
||||
// TODO: find a more optimal folder structure to store these types of functions
|
||||
|
||||
@ -39,7 +47,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns an object containing the user associated with the authentication data payload
|
||||
* @param {AuthData} authData - authentication data object
|
||||
@ -56,7 +63,7 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
||||
}
|
||||
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
return { user: authData.authPayload.user };
|
||||
return { user: authData.authPayload.user };0
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,6 +116,84 @@ const initializePassport = async () => {
|
||||
cb(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: "http://localhost:8080/api/v1/auth/callback/saml", // TODO: get rid of localhost:8080 here
|
||||
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) => {
|
||||
|
||||
const organization = await Organization.findById(req.ssoConfig.organization);
|
||||
|
||||
if (!organization) done(OrganizationNotFoundError());
|
||||
|
||||
const email = profile.email;
|
||||
const firstName = profile.firstName;
|
||||
const lastName = profile.lastName;
|
||||
|
||||
let user = await User.findOne({
|
||||
authProvider: AuthProvider.OKTA_SAML,
|
||||
email
|
||||
}).select("+publicKey");
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email,
|
||||
authProvider: AuthProvider.OKTA_SAML,
|
||||
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: user.firstName,
|
||||
lastName: user.lastName,
|
||||
organizationName: organization?.name,
|
||||
authProvider: user.authProvider,
|
||||
isUserCompleted
|
||||
},
|
||||
expiresIn: await getJwtProviderAuthLifetime(),
|
||||
secret: await getJwtProviderAuthSecret(),
|
||||
});
|
||||
|
||||
req.isUserCompleted = isUserCompleted;
|
||||
req.providerAuthToken = providerAuthToken;
|
||||
|
||||
done(null, profile);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -46,7 +46,7 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
|
||||
stack: error?.stack,
|
||||
});
|
||||
|
||||
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
export const ResourceNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? "resource_not_found",
|
||||
|
@ -7,9 +7,11 @@ import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
|
||||
import {
|
||||
BackupPrivateKey,
|
||||
Bot,
|
||||
BotOrg,
|
||||
ISecret,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Organization,
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
@ -137,6 +139,103 @@ export const backfillBots = async () => {
|
||||
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
|
||||
* has a secret blind index data
|
||||
|
@ -7,6 +7,7 @@ import { createTestUserForDevelopment } from "../addDevelopmentUser";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
import { validateEncryptionKeysConfig } from "./validateConfig";
|
||||
import {
|
||||
backfillBotOrgs,
|
||||
backfillBots,
|
||||
backfillEncryptionMetadata,
|
||||
backfillIntegration,
|
||||
@ -16,7 +17,11 @@ import {
|
||||
backfillServiceToken,
|
||||
backfillServiceTokenMultiScope
|
||||
} from "./backfillData";
|
||||
import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData";
|
||||
import {
|
||||
reencryptBotOrgKeys,
|
||||
reencryptBotPrivateKeys,
|
||||
reencryptSecretBlindIndexDataSalts
|
||||
} from "./reencryptData";
|
||||
import {
|
||||
getClientIdGoogle,
|
||||
getClientSecretGoogle,
|
||||
@ -72,6 +77,7 @@ export const setup = async () => {
|
||||
// backfilling data to catch up with new collections and updated fields
|
||||
await backfillSecretVersions();
|
||||
await backfillBots();
|
||||
await backfillBotOrgs();
|
||||
await backfillSecretBlindIndexData();
|
||||
await backfillEncryptionMetadata();
|
||||
await backfillSecretFolders();
|
||||
@ -82,6 +88,7 @@ export const setup = async () => {
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||
await reencryptBotPrivateKeys();
|
||||
await reencryptBotOrgKeys();
|
||||
await reencryptSecretBlindIndexDataSalts();
|
||||
|
||||
// initializing Sentry
|
||||
|
@ -1,6 +1,8 @@
|
||||
import {
|
||||
Bot,
|
||||
BotOrg,
|
||||
IBot,
|
||||
IBotOrg,
|
||||
ISecretBlindIndexData,
|
||||
SecretBlindIndexData,
|
||||
} from "../../models";
|
||||
@ -17,7 +19,7 @@ import {
|
||||
} 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
|
||||
*/
|
||||
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
|
||||
* 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>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ export const publicPaths = [
|
||||
"/signupinvite",
|
||||
"/pricing",
|
||||
"/signup",
|
||||
"/signup/sso",
|
||||
"/login",
|
||||
"/blog",
|
||||
"/docs",
|
||||
@ -18,8 +19,9 @@ export const publicPaths = [
|
||||
"/verify-email",
|
||||
"/password-reset",
|
||||
"/saml-sso",
|
||||
"/login/provider/success",
|
||||
"/login/provider/error"
|
||||
"/login/provider/success", // TODO: change
|
||||
"/login/provider/error", // TODO: change
|
||||
"/login/sso"
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
|
@ -5,6 +5,7 @@ export * from "./integrationAuth";
|
||||
export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./secretFolders";
|
||||
export * from "./secrets";
|
||||
export * from "./secretSnapshots";
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
BillingDetails,
|
||||
Invoice,
|
||||
@ -22,7 +20,7 @@ const organizationKeys = {
|
||||
getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const,
|
||||
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] 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 = () => {
|
||||
|
5
frontend/src/hooks/api/ssoConfig/index.tsx
Normal file
5
frontend/src/hooks/api/ssoConfig/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export {
|
||||
useGetSSOConfig,
|
||||
useCreateSSOConfig,
|
||||
useUpdateSSOConfig
|
||||
} from "./queries";
|
102
frontend/src/hooks/api/ssoConfig/queries.tsx
Normal file
102
frontend/src/hooks/api/ssoConfig/queries.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
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));
|
||||
}
|
||||
});
|
||||
};
|
@ -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,
|
||||
organizationName,
|
||||
providerAuthToken,
|
||||
attributionSource,
|
||||
...(attributionSource ? { attributionSource } : {})
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
27
frontend/src/pages/login/index.tsx
Normal file
27
frontend/src/pages/login/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
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>
|
||||
);
|
||||
}
|
30
frontend/src/pages/login/sso/index.tsx
Normal file
30
frontend/src/pages/login/sso/index.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import CodeInputStep from "@app/components/signup/CodeInputStep";
|
||||
import DownloadBackupPDF from "@app/components/signup/DonwloadBackupPDFStep";
|
||||
import EnterEmailStep from "@app/components/signup/EnterEmailStep";
|
||||
@ -13,10 +12,8 @@ import TeamInviteStep from "@app/components/signup/TeamInviteStep";
|
||||
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { useProviderAuth } from "@app/hooks/useProviderAuth";
|
||||
|
||||
import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode";
|
||||
import getOrganizations from "./api/organization/getOrgs";
|
||||
import checkEmailVerificationCode from "@app/pages/api/auth/CheckEmailVerificationCode";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
/**
|
||||
* @returns the signup page
|
||||
@ -35,15 +32,6 @@ export default function SignUp() {
|
||||
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
|
||||
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
||||
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(() => {
|
||||
const tryAuth = async () => {
|
||||
@ -121,7 +109,7 @@ export default function SignUp() {
|
||||
return (
|
||||
<UserInfoStep
|
||||
incrementStep={incrementStep}
|
||||
email={email || providerEmail}
|
||||
email={email}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
name={name}
|
||||
@ -130,7 +118,7 @@ export default function SignUp() {
|
||||
setOrganizationName={setOrganizationName}
|
||||
attributionSource={attributionSource}
|
||||
setAttributionSource={setAttributionSource}
|
||||
providerAuthToken={providerAuthToken}
|
||||
providerAuthToken={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -139,7 +127,7 @@ export default function SignUp() {
|
||||
return (
|
||||
<DownloadBackupPDF
|
||||
incrementStep={incrementStep}
|
||||
email={email || providerEmail}
|
||||
email={email}
|
||||
password={password}
|
||||
name={name}
|
||||
/>
|
27
frontend/src/pages/signup/sso/index.tsx
Normal file
27
frontend/src/pages/signup/sso/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
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>
|
||||
);
|
||||
}
|
86
frontend/src/views/Login/Login.tsx
Normal file
86
frontend/src/views/Login/Login.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
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>
|
||||
);
|
||||
}
|
68
frontend/src/views/Login/LoginSSO.tsx
Normal file
68
frontend/src/views/Login/LoginSSO.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
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>
|
||||
);
|
||||
}
|
203
frontend/src/views/Login/components/InitialStep/InitialStep.tsx
Normal file
203
frontend/src/views/Login/components/InitialStep/InitialStep.tsx
Normal file
@ -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='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')
|
||||
window.open('/api/v1/auth/redirect/okta/64b4b9166e76604655b5373e');
|
||||
}}
|
||||
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={() => {
|
||||
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";
|
@ -1,18 +1,17 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React, { useState } from "react";
|
||||
import ReactCodeInput from "react-code-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
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 attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useSendMfaToken } from "@app/hooks/api/auth";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import Error from "../basic/Error";
|
||||
import { Button } from "../v2";
|
||||
|
||||
// The style for the verification code input
|
||||
const props = {
|
||||
inputStyle: {
|
||||
@ -33,6 +32,12 @@ const props = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
}
|
||||
|
||||
interface VerifyMfaTokenError {
|
||||
response: {
|
||||
data: {
|
||||
@ -45,23 +50,12 @@ interface VerifyMfaTokenError {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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({
|
||||
export const MFAStep = ({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
}): JSX.Element {
|
||||
providerAuthToken
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||
@ -75,6 +69,10 @@ export default function MFAStep({
|
||||
const handleLoginMfa = async () => {
|
||||
try {
|
||||
if (mfaCode.length !== 6) {
|
||||
createNotification({
|
||||
text: "Please enter a 6-digit MFA code and try again",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -116,12 +114,25 @@ export default function MFAStep({
|
||||
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`);
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Failed to log in",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const error = err as VerifyMfaTokenError;
|
||||
createNotification({
|
||||
text: "Failed to log in",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
if (error?.response?.status === 500) {
|
||||
window.location.reload();
|
||||
@ -147,8 +158,8 @@ export default function MFAStep({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="mx-auto w-max md:px-8 pb-4 pt-4 md:mb-16">
|
||||
return (
|
||||
<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 my-1 flex justify-center font-semibold text-bunker-300">{email} </p>
|
||||
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
|
||||
@ -204,6 +215,6 @@ export default function MFAStep({
|
||||
</div>
|
||||
<p className="text-sm text-bunker-400 pb-2">{t("signup.step2-spam-alert")}</p>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
</form>
|
||||
);
|
||||
}
|
1
frontend/src/views/Login/components/MFAStep/index.tsx
Normal file
1
frontend/src/views/Login/components/MFAStep/index.tsx
Normal file
@ -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";
|
6
frontend/src/views/Login/components/index.tsx
Normal file
6
frontend/src/views/Login/components/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
export { InitialStep } from "./InitialStep";
|
||||
export { MFAStep } from "./MFAStep";
|
||||
export { SAMLSSOStep } from "./SAMLSSOStep";
|
||||
|
||||
// SSO-specific step
|
||||
export { PasswordStep } from "./PasswordStep";
|
2
frontend/src/views/Login/index.tsx
Normal file
2
frontend/src/views/Login/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export { Login } from "./Login";
|
||||
export { LoginSSO } from "./LoginSSO";
|
@ -7,7 +7,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input} from "@app/components/v2";
|
||||
Input
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetOrgBillingDetails,
|
||||
|
@ -29,7 +29,7 @@ export const PmtMethodsSection = () => {
|
||||
<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">
|
||||
Payment Methods
|
||||
Payment methods
|
||||
</h2>
|
||||
<Button
|
||||
onClick={handleAddPmtMethodBtnClick}
|
||||
|
@ -10,7 +10,8 @@ import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem} from "@app/components/v2";
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useAddOrgTaxId } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@ -138,22 +139,22 @@ export const TaxIDModal = ({
|
||||
defaultValue="eu_vat"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Type"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Type"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{taxIDTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{taxIDTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@ -163,9 +164,9 @@ export const TaxIDModal = ({
|
||||
name="value"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Value"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
OrgIncidentContactsSection,
|
||||
OrgNameChangeSection,
|
||||
OrgServiceAccountsTable
|
||||
} from "./components";
|
||||
import { OrgTabGroup } from "./components";
|
||||
|
||||
export const OrgSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -15,11 +11,7 @@ export const OrgSettingsPage = () => {
|
||||
<div className="mb-4">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
|
||||
</div>
|
||||
<OrgNameChangeSection />
|
||||
<OrgIncidentContactsSection />
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<OrgServiceAccountsTable />
|
||||
</div>
|
||||
<OrgTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
|
||||
export const OrgAuthTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<OrgSSOSection />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
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 } from "@app/components/v2";
|
||||
import { useOrganization } 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 { createNotification } = useNotificationContext();
|
||||
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||
const { mutateAsync } = useUpdateSSOConfig();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"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={() => handlePopUpOpen("addSSO")}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,252 @@
|
||||
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 {
|
||||
useGetSSOConfig,
|
||||
useCreateSSOConfig,
|
||||
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";
|
@ -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";
|
@ -17,7 +17,7 @@ export const OrgIncidentContactsSection = () => {
|
||||
] as const);
|
||||
|
||||
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">
|
||||
<p className="min-w-max text-xl font-semibold">
|
||||
{t("section.incident.incident-contacts")}
|
||||
|
@ -0,0 +1,39 @@
|
||||
import { Fragment } from "react"
|
||||
import { Tab } from "@headlessui/react"
|
||||
|
||||
import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general" },
|
||||
{ name: "SAML SSO", key: "tab-org-saml" }
|
||||
];
|
||||
|
||||
export const OrgTabGroup = () => {
|
||||
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>
|
||||
<Tab.Panel>
|
||||
<OrgAuthTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { OrgTabGroup } from "./OrgTabGroup";
|
@ -1,3 +1 @@
|
||||
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
|
||||
export { OrgNameChangeSection } from "./OrgNameChangeSection";
|
||||
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
|
||||
export { OrgTabGroup } from "./OrgTabGroup";
|
57
frontend/src/views/Signup/SignupSSO.tsx
Normal file
57
frontend/src/views/Signup/SignupSSO.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
UserInfoSSOStep,
|
||||
BackupPDFStep
|
||||
} from "./components";
|
||||
import jwt_decode from "jwt-decode";
|
||||
|
||||
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}`}
|
||||
organizationName={organizationName}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setStep={setStep}
|
||||
providerAuthToken={providerAuthToken}
|
||||
/>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<BackupPDFStep
|
||||
email={email}
|
||||
password={password}
|
||||
name={`${firstName} ${lastName}`}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderView()}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "react-i18next";
|
||||
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,296 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import crypto from "crypto";
|
||||
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 { 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";
|
||||
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 { setSignupTempToken } from "@app/reactQuery";
|
||||
|
||||
// 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;
|
||||
organizationName: 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,
|
||||
organizationName,
|
||||
password,
|
||||
setPassword,
|
||||
setStep,
|
||||
providerAuthToken,
|
||||
}: Props) => {
|
||||
const { data: commonPasswords } = useGetCommonPasswords();
|
||||
const [nameError, setNameError] = useState(false);
|
||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 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);
|
||||
|
||||
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={true}
|
||||
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}
|
||||
isRequired
|
||||
className="h-12"
|
||||
disabled={true}
|
||||
/>
|
||||
{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";
|
2
frontend/src/views/Signup/components/index.tsx
Normal file
2
frontend/src/views/Signup/components/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export { UserInfoSSOStep } from "./UserInfoSSOStep";
|
||||
export { BackupPDFStep } from "./BackupPDFStep";
|
1
frontend/src/views/Signup/index.tsx
Normal file
1
frontend/src/views/Signup/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { SignupSSO } from "./SignupSSO";
|
Reference in New Issue
Block a user