mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 07:26:34 +00:00
Merge pull request #829 from sheensantoscapadngan/feature/enable-users-to-select-multi-auth-methods
Feature: enable users to select multi auth methods (backward compatible)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { User } from "../../models";
|
import { AuthMethod, User } from "../../models";
|
||||||
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
|
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
|
||||||
import { createToken } from "../../helpers/auth";
|
import { createToken } from "../../helpers/auth";
|
||||||
import { BadRequestError } from "../../utils/errors";
|
import { BadRequestError } from "../../utils/errors";
|
||||||
@@ -81,7 +81,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email
|
email,
|
||||||
|
authMethods: [AuthMethod.EMAIL]
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import crypto from "crypto";
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import {
|
import {
|
||||||
APIKeyData,
|
APIKeyData,
|
||||||
AuthProvider,
|
AuthMethod,
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
TokenVersion,
|
TokenVersion,
|
||||||
User
|
User
|
||||||
@@ -113,21 +113,26 @@ export const updateName = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update auth provider of the current user to [authProvider]
|
* Update auth method of the current user to [authMethods]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateAuthProvider = async (req: Request, res: Response) => {
|
export const updateAuthMethods = async (req: Request, res: Response) => {
|
||||||
const {
|
const {
|
||||||
authProvider
|
authMethods
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (
|
const hasSamlEnabled = req.user.authMethods
|
||||||
req.user?.authProvider === AuthProvider.OKTA_SAML
|
.some(
|
||||||
|| req.user?.authProvider === AuthProvider.AZURE_SAML
|
(authMethod: AuthMethod) => [
|
||||||
|| req.user?.authProvider === AuthProvider.JUMPCLOUD_SAML
|
AuthMethod.OKTA_SAML,
|
||||||
) {
|
AuthMethod.AZURE_SAML,
|
||||||
|
AuthMethod.JUMPCLOUD_SAML
|
||||||
|
].includes(authMethod)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasSamlEnabled) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||||
});
|
});
|
||||||
@@ -136,7 +141,7 @@ export const updateAuthProvider = async (req: Request, res: Response) => {
|
|||||||
const user = await User.findByIdAndUpdate(
|
const user = await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
authProvider
|
authMethods
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
@@ -148,6 +153,7 @@ export const updateAuthProvider = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return organizations that the current user is part of.
|
* Return organizations that the current user is part of.
|
||||||
* @param req
|
* @param req
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import * as bigintConversion from "bigint-conversion";
|
import * as bigintConversion from "bigint-conversion";
|
||||||
const jsrp = require("jsrp");
|
const jsrp = require("jsrp");
|
||||||
import { LoginSRPDetail, User } from "../../models";
|
import { LoginSRPDetail, User } from "../../models";
|
||||||
@@ -21,13 +20,13 @@ import {
|
|||||||
getJwtMfaLifetime,
|
getJwtMfaLifetime,
|
||||||
getJwtMfaSecret,
|
getJwtMfaSecret,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { AuthProvider } from "../../models/user";
|
import { AuthMethod } from "../../models/user";
|
||||||
|
|
||||||
declare module "jsonwebtoken" {
|
declare module "jsonwebtoken" {
|
||||||
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
|
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
authProvider: AuthProvider;
|
authProvider: AuthMethod;
|
||||||
isUserCompleted: boolean,
|
isUserCompleted: boolean,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,62 +38,54 @@ declare module "jsonwebtoken" {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
|
providerAuthToken,
|
||||||
|
clientPublicKey,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
clientPublicKey: string,
|
||||||
|
providerAuthToken?: string;
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
email,
|
||||||
|
}).select("+salt +verifier");
|
||||||
|
|
||||||
|
if (!user) throw new Error("Failed to find user");
|
||||||
|
|
||||||
|
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
||||||
|
await validateProviderAuthToken({
|
||||||
email,
|
email,
|
||||||
|
user,
|
||||||
providerAuthToken,
|
providerAuthToken,
|
||||||
clientPublicKey,
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
clientPublicKey: string,
|
|
||||||
providerAuthToken?: string;
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email,
|
|
||||||
}).select("+salt +verifier");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
|
|
||||||
await validateProviderAuthToken({
|
|
||||||
email,
|
|
||||||
user,
|
|
||||||
providerAuthToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier,
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
// generate server-side public key
|
|
||||||
const serverPublicKey = server.getPublicKey();
|
|
||||||
await LoginSRPDetail.findOneAndReplace({
|
|
||||||
email: email,
|
|
||||||
}, {
|
|
||||||
email,
|
|
||||||
userId: user.id,
|
|
||||||
clientPublicKey: clientPublicKey,
|
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
|
||||||
}, { upsert: true, returnNewDocument: false });
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serverPublicKey,
|
|
||||||
salt: user.salt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to start authentication process",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
// generate server-side public key
|
||||||
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
await LoginSRPDetail.findOneAndReplace({
|
||||||
|
email: email,
|
||||||
|
}, {
|
||||||
|
email,
|
||||||
|
userId: user.id,
|
||||||
|
clientPublicKey: clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
|
}, { upsert: true, returnNewDocument: false });
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,159 +96,151 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
try {
|
if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" });
|
||||||
if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" });
|
|
||||||
|
|
||||||
const { email, clientProof, providerAuthToken } = req.body;
|
const { email, clientProof, providerAuthToken } = req.body;
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
|
email,
|
||||||
|
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices");
|
||||||
|
|
||||||
|
if (!user) throw new Error("Failed to find user");
|
||||||
|
|
||||||
|
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
||||||
|
await validateProviderAuthToken({
|
||||||
email,
|
email,
|
||||||
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices");
|
user,
|
||||||
|
providerAuthToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||||
|
|
||||||
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
|
if (!loginSRPDetail) {
|
||||||
await validateProviderAuthToken({
|
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||||
email,
|
}
|
||||||
user,
|
|
||||||
providerAuthToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: loginSRPDetail.serverBInt,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||||
|
|
||||||
if (!loginSRPDetail) {
|
// compare server and client shared keys
|
||||||
return BadRequestError(Error("Failed to find login details for SRP"))
|
if (server.checkClientProof(clientProof)) {
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
if (user.isMfaEnabled) {
|
||||||
server.init(
|
// case: user has MFA enabled
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetail.serverBInt,
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
// generate temporary MFA token
|
||||||
if (server.checkClientProof(clientProof)) {
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
if (user.isMfaEnabled) {
|
userId: user._id.toString(),
|
||||||
// case: user has MFA enabled
|
},
|
||||||
|
expiresIn: await getJwtMfaLifetime(),
|
||||||
// generate temporary MFA token
|
secret: await getJwtMfaSecret(),
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
userId: user._id.toString(),
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtMfaLifetime(),
|
|
||||||
secret: await getJwtMfaSecret(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_MFA,
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
|
||||||
await sendMail({
|
|
||||||
template: "emailMfa.handlebars",
|
|
||||||
subjectLine: "Infisical MFA code",
|
|
||||||
recipients: [user.email],
|
|
||||||
substitutions: {
|
|
||||||
code,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
mfaEnabled: true,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkUserDevice({
|
|
||||||
user,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// issue tokens
|
const code = await TokenService.createToken({
|
||||||
const tokens = await issueAuthTokens({
|
type: TOKEN_EMAIL_MFA,
|
||||||
userId: user._id,
|
email,
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie("jid", tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: await getHttpsEnabled(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// case: user does not have MFA enablgged
|
// send MFA code [code] to [email]
|
||||||
// return (access) token in response
|
await sendMail({
|
||||||
|
template: "emailMfa.handlebars",
|
||||||
interface ResponseData {
|
subjectLine: "Infisical MFA code",
|
||||||
mfaEnabled: boolean;
|
recipients: [user.email],
|
||||||
encryptionVersion: any;
|
substitutions: {
|
||||||
protectedKey?: string;
|
code,
|
||||||
protectedKeyIV?: string;
|
},
|
||||||
protectedKeyTag?: string;
|
|
||||||
token: string;
|
|
||||||
publicKey?: string;
|
|
||||||
encryptedPrivateKey?: string;
|
|
||||||
iv?: string;
|
|
||||||
tag?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response: ResponseData = {
|
|
||||||
mfaEnabled: false,
|
|
||||||
encryptionVersion: user.encryptionVersion,
|
|
||||||
token: tokens.token,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
|
||||||
iv: user.iv,
|
|
||||||
tag: user.tag,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
user?.protectedKey &&
|
|
||||||
user?.protectedKeyIV &&
|
|
||||||
user?.protectedKeyTag
|
|
||||||
) {
|
|
||||||
response.protectedKey = user.protectedKey;
|
|
||||||
response.protectedKeyIV = user.protectedKeyIV
|
|
||||||
response.protectedKeyTag = user.protectedKeyTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginAction = await EELogService.createAction({
|
|
||||||
name: ACTION_LOGIN,
|
|
||||||
userId: user._id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loginAction && await EELogService.createLog({
|
return res.status(200).send({
|
||||||
userId: user._id,
|
mfaEnabled: true,
|
||||||
actions: [loginAction],
|
token,
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
|
||||||
ipAddress: req.realIP,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).send({
|
await checkUserDevice({
|
||||||
message: "Failed to authenticate. Try again?",
|
user,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers["user-agent"] ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueAuthTokens({
|
||||||
|
userId: user._id,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers["user-agent"] ?? "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie("jid", tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: await getHttpsEnabled(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// case: user does not have MFA enablgged
|
||||||
|
// return (access) token in response
|
||||||
|
|
||||||
|
interface ResponseData {
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
encryptionVersion: any;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
token: string;
|
||||||
|
publicKey?: string;
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
iv?: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ResponseData = {
|
||||||
|
mfaEnabled: false,
|
||||||
|
encryptionVersion: user.encryptionVersion,
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user?.protectedKey &&
|
||||||
|
user?.protectedKeyIV &&
|
||||||
|
user?.protectedKeyTag
|
||||||
|
) {
|
||||||
|
response.protectedKey = user.protectedKey;
|
||||||
|
response.protectedKeyIV = user.protectedKeyIV
|
||||||
|
response.protectedKeyTag = user.protectedKeyTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginAction = await EELogService.createAction({
|
||||||
|
name: ACTION_LOGIN,
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
loginAction && await EELogService.createLog({
|
||||||
|
userId: user._id,
|
||||||
|
actions: [loginAction],
|
||||||
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
|
ipAddress: req.realIP,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(response);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} catch (err) {
|
return res.status(400).send({
|
||||||
Sentry.setUser(null);
|
message: "Failed to authenticate. Try again?",
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
message: "Failed to authenticate. Try again?",
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { standardRequest } from "../../config/request";
|
|||||||
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
|
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
|
||||||
import { BadRequestError } from "../../utils/errors";
|
import { BadRequestError } from "../../utils/errors";
|
||||||
import { TelemetryService } from "../../services";
|
import { TelemetryService } from "../../services";
|
||||||
import { AuthProvider } from "../../models";
|
import { AuthMethod } from "../../models";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
@@ -117,7 +117,17 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
||||||
|
|
||||||
if (user.authProvider !== AuthProvider.OKTA_SAML) {
|
const hasSamlEnabled = user.authMethods
|
||||||
|
.some(
|
||||||
|
(authMethod: AuthMethod) =>
|
||||||
|
[
|
||||||
|
AuthMethod.OKTA_SAML,
|
||||||
|
AuthMethod.AZURE_SAML,
|
||||||
|
AuthMethod.JUMPCLOUD_SAML
|
||||||
|
].includes(authMethod)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSamlEnabled) { // TODO: modify this part
|
||||||
// initialize default organization and workspace
|
// initialize default organization and workspace
|
||||||
await initializeDefaultOrg({
|
await initializeDefaultOrg({
|
||||||
organizationName,
|
organizationName,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Types } from "mongoose";
|
|||||||
import { BotOrgService } from "../../../services";
|
import { BotOrgService } from "../../../services";
|
||||||
import { SSOConfig } from "../../models";
|
import { SSOConfig } from "../../models";
|
||||||
import {
|
import {
|
||||||
|
AuthMethod,
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
User
|
User
|
||||||
} from "../../../models";
|
} from "../../../models";
|
||||||
@@ -156,7 +157,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authProvider: ssoConfig.authProvider
|
authMethods: [ssoConfig.authProvider],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -167,9 +168,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$unset: {
|
authMethods: [AuthMethod.EMAIL],
|
||||||
authProvider: 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,9 +408,9 @@ export const validateProviderAuthToken = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
decodedToken.authProvider !== user.authProvider ||
|
!user.authMethods.includes(decodedToken.authMethod) ||
|
||||||
decodedToken.email !== email
|
decodedToken.email !== email
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid authentication credentials.")
|
throw new Error("Invalid authentication credentials.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
|
|||||||
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
|
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
|
||||||
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
|
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
|
||||||
import TokenData, { ITokenData } from "./tokenData";
|
import TokenData, { ITokenData } from "./tokenData";
|
||||||
import User, { AuthProvider, IUser } from "./user";
|
import User, { AuthMethod, IUser } from "./user";
|
||||||
import UserAction, { IUserAction } from "./userAction";
|
import UserAction, { IUserAction } from "./userAction";
|
||||||
import Workspace, { IWorkspace } from "./workspace";
|
import Workspace, { IWorkspace } from "./workspace";
|
||||||
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
|
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
|
||||||
@@ -28,7 +28,7 @@ import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
|
|||||||
import TokenVersion, { ITokenVersion } from "./tokenVersion";
|
import TokenVersion, { ITokenVersion } from "./tokenVersion";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AuthProvider,
|
AuthMethod,
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
IBackupPrivateKey,
|
IBackupPrivateKey,
|
||||||
Bot,
|
Bot,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Document, Schema, Types, model } from "mongoose";
|
import { Document, Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
export enum AuthProvider {
|
export enum AuthMethod {
|
||||||
EMAIL = "email",
|
EMAIL = "email",
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
GITHUB = "github",
|
GITHUB = "github",
|
||||||
@@ -11,8 +11,8 @@ export enum AuthProvider {
|
|||||||
|
|
||||||
export interface IUser extends Document {
|
export interface IUser extends Document {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
authId?: string;
|
authProvider?: AuthMethod;
|
||||||
authProvider?: AuthProvider;
|
authMethods: AuthMethod[];
|
||||||
email: string;
|
email: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
@@ -36,12 +36,17 @@ export interface IUser extends Document {
|
|||||||
|
|
||||||
const userSchema = new Schema<IUser>(
|
const userSchema = new Schema<IUser>(
|
||||||
{
|
{
|
||||||
authId: {
|
authProvider: { // TODO field: deprecate
|
||||||
type: String,
|
type: String,
|
||||||
|
enum: AuthMethod,
|
||||||
},
|
},
|
||||||
authProvider: {
|
authMethods: {
|
||||||
type: String,
|
type: [{
|
||||||
enum: AuthProvider,
|
type: String,
|
||||||
|
enum: AuthMethod,
|
||||||
|
}],
|
||||||
|
default: [AuthMethod.EMAIL],
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { body, param } from "express-validator";
|
|||||||
import { usersController } from "../../controllers/v2";
|
import { usersController } from "../../controllers/v2";
|
||||||
import { AuthMode } from "../../variables";
|
import { AuthMode } from "../../variables";
|
||||||
import {
|
import {
|
||||||
AuthProvider
|
AuthMethod
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
@@ -40,18 +40,22 @@ router.patch(
|
|||||||
usersController.updateName
|
usersController.updateName
|
||||||
);
|
);
|
||||||
|
|
||||||
router.patch(
|
router.put(
|
||||||
"/me/auth-provider",
|
"/me/auth-methods",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
||||||
}),
|
}),
|
||||||
body("authProvider").exists().isString().isIn([
|
body("authMethods").exists().isArray({
|
||||||
AuthProvider.EMAIL,
|
min: 1,
|
||||||
AuthProvider.GOOGLE,
|
}).custom((authMethods: AuthMethod[]) => {
|
||||||
AuthProvider.GITHUB
|
return authMethods.every(provider => [
|
||||||
]),
|
AuthMethod.EMAIL,
|
||||||
|
AuthMethod.GOOGLE,
|
||||||
|
AuthMethod.GITHUB
|
||||||
|
].includes(provider))
|
||||||
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
usersController.updateAuthProvider
|
usersController.updateAuthMethods,
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
|||||||
+29
-21
@@ -3,7 +3,7 @@ import passport from "passport";
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { AuthData } from "../interfaces/middleware";
|
import { AuthData } from "../interfaces/middleware";
|
||||||
import {
|
import {
|
||||||
AuthProvider,
|
AuthMethod,
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
Organization,
|
Organization,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
@@ -97,20 +97,19 @@ const initializePassport = async () => {
|
|||||||
email
|
email
|
||||||
}).select("+publicKey");
|
}).select("+publicKey");
|
||||||
|
|
||||||
if (user && user.authProvider !== AuthProvider.GOOGLE) {
|
|
||||||
done(InternalServerError());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email,
|
email,
|
||||||
authProvider: AuthProvider.GOOGLE,
|
authMethods: [AuthMethod.GOOGLE],
|
||||||
authId: profile.id,
|
|
||||||
firstName: profile.name.givenName,
|
firstName: profile.name.givenName,
|
||||||
lastName: profile.name.familyName
|
lastName: profile.name.familyName
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.authMethods.includes(AuthMethod.GOOGLE)) {
|
||||||
|
done(InternalServerError());
|
||||||
|
}
|
||||||
|
|
||||||
const isUserCompleted = !!user.publicKey;
|
const isUserCompleted = !!user.publicKey;
|
||||||
const providerAuthToken = createToken({
|
const providerAuthToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
@@ -118,7 +117,7 @@ const initializePassport = async () => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
authProvider: user.authProvider,
|
authMethod: AuthMethod.GOOGLE,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(req.query.state ? {
|
...(req.query.state ? {
|
||||||
callbackPort: req.query.state as string
|
callbackPort: req.query.state as string
|
||||||
@@ -150,20 +149,19 @@ const initializePassport = async () => {
|
|||||||
let user = await User.findOne({
|
let user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select("+publicKey");
|
}).select("+publicKey");
|
||||||
|
|
||||||
if (user && user.authProvider !== AuthProvider.GITHUB) {
|
|
||||||
done(InternalServerError());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email: email,
|
email: email,
|
||||||
authProvider: AuthProvider.GITHUB,
|
authMethods: [AuthMethod.GITHUB],
|
||||||
authId: profile.id,
|
|
||||||
firstName: profile.displayName,
|
firstName: profile.displayName,
|
||||||
lastName: ""
|
lastName: ""
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.authMethods.includes(AuthMethod.GITHUB)) {
|
||||||
|
done(InternalServerError());
|
||||||
|
}
|
||||||
|
|
||||||
const isUserCompleted = !!user.publicKey;
|
const isUserCompleted = !!user.publicKey;
|
||||||
const providerAuthToken = createToken({
|
const providerAuthToken = createToken({
|
||||||
@@ -172,7 +170,7 @@ const initializePassport = async () => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
authProvider: user.authProvider,
|
authMethod: AuthMethod.GITHUB,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(req.query.state ? {
|
...(req.query.state ? {
|
||||||
callbackPort: req.query.state as string
|
callbackPort: req.query.state as string
|
||||||
@@ -218,7 +216,7 @@ const initializePassport = async () => {
|
|||||||
audience: await getSiteURL()
|
audience: await getSiteURL()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ssoConfig.authProvider === AuthProvider.JUMPCLOUD_SAML) {
|
if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) {
|
||||||
samlConfig.wantAuthnResponseSigned = false;
|
samlConfig.wantAuthnResponseSigned = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,11 +241,21 @@ const initializePassport = async () => {
|
|||||||
}).select("+publicKey");
|
}).select("+publicKey");
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (!user.authProvider || user.authProvider === AuthProvider.EMAIL || user.authProvider === AuthProvider.GOOGLE) {
|
// if user does not have SAML enabled then update
|
||||||
|
const hasSamlEnabled = user.authMethods
|
||||||
|
.some(
|
||||||
|
(authMethod: AuthMethod) => [
|
||||||
|
AuthMethod.OKTA_SAML,
|
||||||
|
AuthMethod.AZURE_SAML,
|
||||||
|
AuthMethod.JUMPCLOUD_SAML
|
||||||
|
].includes(authMethod)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSamlEnabled) {
|
||||||
await User.findByIdAndUpdate(
|
await User.findByIdAndUpdate(
|
||||||
user._id,
|
user._id,
|
||||||
{
|
{
|
||||||
authProvider: req.ssoConfig.authProvider
|
authMethods: [req.ssoConfig.authProvider]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
@@ -279,7 +287,7 @@ const initializePassport = async () => {
|
|||||||
} else {
|
} else {
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email,
|
email,
|
||||||
authProvider: req.ssoConfig.authProvider,
|
authMethods: [req.ssoConfig.authProvider],
|
||||||
firstName,
|
firstName,
|
||||||
lastName
|
lastName
|
||||||
}).save();
|
}).save();
|
||||||
@@ -301,7 +309,7 @@ const initializePassport = async () => {
|
|||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationName: organization?.name,
|
organizationName: organization?.name,
|
||||||
authProvider: user.authProvider,
|
authMethod: req.ssoConfig.authProvider,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(req.body.RelayState ? {
|
...(req.body.RelayState ? {
|
||||||
callbackPort: req.body.RelayState as string
|
callbackPort: req.body.RelayState as string
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TrustedIP
|
TrustedIP
|
||||||
} from "../../ee/models";
|
} from "../../ee/models";
|
||||||
import {
|
import {
|
||||||
|
AuthMethod,
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
Bot,
|
Bot,
|
||||||
BotOrg,
|
BotOrg,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
Secret,
|
Secret,
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
|
User,
|
||||||
Workspace
|
Workspace
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
import { generateKeyPair } from "../../utils/crypto";
|
import { generateKeyPair } from "../../utils/crypto";
|
||||||
@@ -631,3 +633,40 @@ export const backfillTrustedIps = async () => {
|
|||||||
console.log("Backfill: Trusted IPs complete");
|
console.log("Backfill: Trusted IPs complete");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const backfillUserAuthMethods = async () => {
|
||||||
|
await User.updateMany(
|
||||||
|
{
|
||||||
|
authProvider: {
|
||||||
|
$exists: false
|
||||||
|
},
|
||||||
|
authMethods: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authMethods: [AuthMethod.EMAIL]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await User.updateMany(
|
||||||
|
{
|
||||||
|
authProvider: {
|
||||||
|
$exists: true
|
||||||
|
},
|
||||||
|
authMethods: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
authMethods: ["$authProvider"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unset: ["authProvider", "authId"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
backfillSecretVersions,
|
backfillSecretVersions,
|
||||||
backfillServiceToken,
|
backfillServiceToken,
|
||||||
backfillServiceTokenMultiScope,
|
backfillServiceTokenMultiScope,
|
||||||
backfillTrustedIps
|
backfillTrustedIps,
|
||||||
|
backfillUserAuthMethods
|
||||||
} from "./backfillData";
|
} from "./backfillData";
|
||||||
import {
|
import {
|
||||||
reencryptBotOrgKeys,
|
reencryptBotOrgKeys,
|
||||||
@@ -79,6 +80,7 @@ export const setup = async () => {
|
|||||||
await backfillIntegration();
|
await backfillIntegration();
|
||||||
await backfillServiceTokenMultiScope();
|
await backfillServiceTokenMultiScope();
|
||||||
await backfillTrustedIps();
|
await backfillTrustedIps();
|
||||||
|
await backfillUserAuthMethods();
|
||||||
|
|
||||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Props = {
|
|||||||
position?: "item-aligned" | "popper";
|
position?: "item-aligned" | "popper";
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
icon?: IconProp;
|
icon?: IconProp;
|
||||||
|
isMulti?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
|
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ export {
|
|||||||
useRevokeMySessions,
|
useRevokeMySessions,
|
||||||
useUpdateMfaEnabled,
|
useUpdateMfaEnabled,
|
||||||
useUpdateOrgUserRole,
|
useUpdateOrgUserRole,
|
||||||
useUpdateUserAuthProvider} from "./queries";
|
useUpdateUserAuthMethods
|
||||||
|
} from "./queries";
|
||||||
@@ -13,14 +13,14 @@ import {
|
|||||||
AddUserToWsDTO,
|
AddUserToWsDTO,
|
||||||
AddUserToWsRes,
|
AddUserToWsRes,
|
||||||
APIKeyData,
|
APIKeyData,
|
||||||
|
AuthMethod,
|
||||||
CreateAPIKeyRes,
|
CreateAPIKeyRes,
|
||||||
DeletOrgMembershipDTO,
|
DeletOrgMembershipDTO,
|
||||||
OrgUser,
|
OrgUser,
|
||||||
RenameUserDTO,
|
RenameUserDTO,
|
||||||
TokenVersion,
|
TokenVersion,
|
||||||
UpdateOrgUserRoleDTO,
|
UpdateOrgUserRoleDTO,
|
||||||
User
|
User} from "./types";
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
const userKeys = {
|
const userKeys = {
|
||||||
getUser: ["user"] as const,
|
getUser: ["user"] as const,
|
||||||
@@ -61,17 +61,18 @@ export const useRenameUser = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateUserAuthProvider = () => {
|
|
||||||
|
export const useUpdateUserAuthMethods = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
authProvider
|
authMethods
|
||||||
}: {
|
}: {
|
||||||
authProvider: string;
|
authMethods: AuthMethod[];
|
||||||
}) => {
|
}) => {
|
||||||
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
|
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-methods", {
|
||||||
authProvider
|
authMethods
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { UserWsKeyPair } from "../keys/types";
|
import { UserWsKeyPair } from "../keys/types";
|
||||||
|
|
||||||
export enum AuthProvider {
|
export enum AuthMethod {
|
||||||
EMAIL = "email",
|
EMAIL = "email",
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
OKTA_SAML = "okta-saml"
|
GITHUB = "github",
|
||||||
|
OKTA_SAML = "okta-saml",
|
||||||
|
AZURE_SAML = "azure-saml",
|
||||||
|
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
@@ -12,7 +15,8 @@ export type User = {
|
|||||||
email: string;
|
email: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
authProvider?: AuthProvider;
|
authProvider?: AuthMethod;
|
||||||
|
authMethods: AuthMethod[];
|
||||||
encryptionVersion?: number;
|
encryptionVersion?: number;
|
||||||
protectedKey?: string;
|
protectedKey?: string;
|
||||||
protectedKeyIV?: string;
|
protectedKeyIV?: string;
|
||||||
|
|||||||
+93
-94
@@ -1,30 +1,35 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { faGithub, faGoogle, IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import {
|
import { Switch } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
Select,
|
|
||||||
SelectItem} from "@app/components/v2";
|
|
||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
|
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
useUpdateUserAuthProvider
|
AuthMethod
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api/users/types";
|
||||||
|
|
||||||
const authMethods = [
|
interface AuthMethodOption {
|
||||||
{ label: "Email", value: "email" },
|
label: string,
|
||||||
{ label: "Google SSO", value: "google" },
|
value: AuthMethod,
|
||||||
{ label: "GitHub SSO", value: "github" },
|
icon: IconDefinition;
|
||||||
{ label: "Okta SAML", value: "okta-saml" },
|
}
|
||||||
{ label: "Azure SAML", value: "azure-saml" },
|
|
||||||
{ label: "JumpCloud SAML", value: "jumpcloud-saml" }
|
const authMethodOpts: AuthMethodOption[] = [
|
||||||
|
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope },
|
||||||
|
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle },
|
||||||
|
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const samlProviders = [AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.AZURE_SAML];
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
authMethod: yup.string().required("Auth method is required")
|
authMethods: yup.array().required("Auth method is required")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -32,108 +37,102 @@ export type FormData = yup.InferType<typeof schema>;
|
|||||||
export const AuthMethodSection = () => {
|
export const AuthMethodSection = () => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { mutateAsync, isLoading } = useUpdateUserAuthProvider();
|
const { mutateAsync } = useUpdateUserAuthMethods();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reset,
|
reset,
|
||||||
control,
|
setValue,
|
||||||
handleSubmit
|
watch,
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authMethod: user?.authProvider ?? "email"
|
authMethods: user.authMethods,
|
||||||
},
|
},
|
||||||
resolver: yupResolver(schema)
|
resolver: yupResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authMethods = watch("authMethods");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
reset({
|
reset({
|
||||||
authMethod: user?.authProvider ?? "email"
|
authMethods: user.authMethods,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
|
||||||
|
const hasSamlEnabled = user.authMethods
|
||||||
|
.some((authMethod: AuthMethod) => samlProviders.includes(authMethod));
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
if (hasSamlEnabled) {
|
||||||
authMethod
|
|
||||||
}: FormData) => {
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
authMethod === "okta-saml"
|
|
||||||
|| authMethod === "azure-saml"
|
|
||||||
|| authMethod === "jumpcloud-saml"
|
|
||||||
) {
|
|
||||||
createNotification({
|
|
||||||
text: "SAML authentication can only be configured in your organization settings",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
authProvider: authMethod
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully updated authentication method",
|
text: "SAML authentication can only be configured in your organization settings",
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update authentication method",
|
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newAuthMethods = value
|
||||||
|
? [...authMethods, authMethodOpt.value]
|
||||||
|
: authMethods.filter(auth => auth !== authMethodOpt.value);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
const newUser = await mutateAsync({
|
||||||
|
authMethods: newAuthMethods
|
||||||
|
});
|
||||||
|
|
||||||
|
setValue("authMethods", newUser.authMethods);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully enabled authentication method",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAuthMethods.length === 0) {
|
||||||
|
createNotification({
|
||||||
|
text: "You must keep at least 1 authentication method enabled",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = await mutateAsync({
|
||||||
|
authMethods: newAuthMethods
|
||||||
|
});
|
||||||
|
|
||||||
|
setValue("authMethods", newUser.authMethods);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully disabled authentication method",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
Authentication methods
|
||||||
>
|
|
||||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
|
||||||
Authentication Method
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className="max-w-md mb-4">
|
<p className="text-gray-400 mb-4">
|
||||||
<Controller
|
By enabling a SSO provider, you are allowing an account with that provider which uses the same email address as your existing Infisical account to be able to log in to Infisical.
|
||||||
control={control}
|
</p>
|
||||||
name="authMethod"
|
<div className="mb-4">
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
{user && authMethodOpts.map((authMethodOpt) => {
|
||||||
<FormControl
|
return (
|
||||||
className="mb-0"
|
<div className="flex p-4 items-center" key={`auth-method-${authMethodOpt.value}`}>
|
||||||
errorText={error?.message}
|
<div className="flex items-center">
|
||||||
isError={Boolean(error)}
|
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
|
||||||
>
|
</div>
|
||||||
<Select
|
<Switch
|
||||||
defaultValue={field.value}
|
id={`enable-${authMethodOpt.value}-auth`}
|
||||||
{...field}
|
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
|
||||||
onValueChange={(e) => onChange(e)}
|
isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
|
||||||
className="w-full bg-mineshaft-800 border border-mineshaft-600"
|
|
||||||
>
|
>
|
||||||
{authMethods.map((authMethod) => {
|
<p className="w-12 mr-4">{authMethodOpt.label}</p>
|
||||||
return (
|
</Switch>
|
||||||
<SelectItem
|
</div>
|
||||||
value={authMethod.value}
|
);
|
||||||
key={`auth-method-${authMethod.value}`}
|
})}
|
||||||
>
|
|
||||||
{authMethod.label}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
</div>
|
||||||
type="submit"
|
|
||||||
colorSchema="secondary"
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user