mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Update login with multiple auth methods to toggle button and logic
This commit is contained in:
@@ -4,7 +4,7 @@ import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import {
|
||||
APIKeyData,
|
||||
AuthProvider,
|
||||
AuthMethod,
|
||||
MembershipOrg,
|
||||
TokenVersion,
|
||||
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 res
|
||||
* @returns
|
||||
*/
|
||||
export const updateAuthProvider = async (req: Request, res: Response) => {
|
||||
export const updateAuthMethods = async (req: Request, res: Response) => {
|
||||
const {
|
||||
authProvider
|
||||
authMethods
|
||||
} = req.body;
|
||||
|
||||
if (
|
||||
req.user?.authProvider === AuthProvider.OKTA_SAML
|
||||
|| req.user?.authProvider === AuthProvider.AZURE_SAML
|
||||
|| req.user?.authProvider === AuthProvider.JUMPCLOUD_SAML
|
||||
) {
|
||||
const hasSamlEnabled = req.user.authMethods
|
||||
.some(
|
||||
(authMethod: AuthMethod) => [
|
||||
AuthMethod.OKTA_SAML,
|
||||
AuthMethod.AZURE_SAML,
|
||||
AuthMethod.JUMPCLOUD_SAML
|
||||
].includes(authMethod)
|
||||
);
|
||||
|
||||
if (hasSamlEnabled) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||
});
|
||||
@@ -136,43 +141,7 @@ export const updateAuthProvider = async (req: Request, res: Response) => {
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
authProvider
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update auth provider of the current user to [authProvider]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateAuthProviders = async (req: Request, res: Response) => {
|
||||
const {
|
||||
authProviders
|
||||
} = req.body;
|
||||
|
||||
if (
|
||||
req.user?.authProvider === AuthProvider.OKTA_SAML
|
||||
|| req.user?.authProvider === AuthProvider.AZURE_SAML
|
||||
|| req.user?.authProvider === AuthProvider.JUMPCLOUD_SAML
|
||||
) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
authProviders
|
||||
authMethods
|
||||
},
|
||||
{
|
||||
new: true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
const jsrp = require("jsrp");
|
||||
import { LoginSRPDetail, User } from "../../models";
|
||||
@@ -21,13 +20,13 @@ import {
|
||||
getJwtMfaLifetime,
|
||||
getJwtMfaSecret,
|
||||
} from "../../config";
|
||||
import { AuthProvider } from "../../models/user";
|
||||
import { AuthMethod } from "../../models/user";
|
||||
|
||||
declare module "jsonwebtoken" {
|
||||
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
authProvider: AuthProvider;
|
||||
authProvider: AuthMethod;
|
||||
isUserCompleted: boolean,
|
||||
}
|
||||
}
|
||||
@@ -39,65 +38,54 @@ declare module "jsonwebtoken" {
|
||||
* @returns
|
||||
*/
|
||||
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,
|
||||
user,
|
||||
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");
|
||||
|
||||
const authProviders = [...(user.authProviders || [])];
|
||||
user.authProvider && authProviders.push(user.authProvider);
|
||||
|
||||
if (authProviders.length && !authProviders.includes(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,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -108,162 +96,151 @@ export const login1 = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
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,
|
||||
}).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 })
|
||||
|
||||
const authProviders = [...(user.authProviders || [])];
|
||||
user.authProvider && authProviders.push(user.authProvider);
|
||||
|
||||
if (authProviders.length && !authProviders.includes(AuthProvider.EMAIL)) {
|
||||
await validateProviderAuthToken({
|
||||
email,
|
||||
user,
|
||||
providerAuthToken,
|
||||
})
|
||||
}
|
||||
if (!loginSRPDetail) {
|
||||
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||
}
|
||||
|
||||
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) {
|
||||
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||
}
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetail.serverBInt,
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||
if (user.isMfaEnabled) {
|
||||
// case: user has MFA enabled
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
|
||||
if (user.isMfaEnabled) {
|
||||
// case: user has MFA enabled
|
||||
|
||||
// generate temporary MFA token
|
||||
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"] ?? "",
|
||||
// generate temporary MFA token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString(),
|
||||
},
|
||||
expiresIn: await getJwtMfaLifetime(),
|
||||
secret: await getJwtMfaSecret(),
|
||||
});
|
||||
|
||||
// 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(),
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email,
|
||||
});
|
||||
|
||||
// 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,
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: "emailMfa.handlebars",
|
||||
subjectLine: "Infisical MFA code",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
|
||||
loginAction && await EELogService.createLog({
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
ipAddress: req.realIP,
|
||||
return res.status(200).send({
|
||||
mfaEnabled: true,
|
||||
token,
|
||||
});
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?",
|
||||
await checkUserDevice({
|
||||
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) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?",
|
||||
});
|
||||
}
|
||||
|
||||
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 { BadRequestError } from "../../utils/errors";
|
||||
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
|
||||
@@ -117,7 +117,7 @@ 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
|
||||
|
||||
if (!user.authProviders?.includes(AuthProvider.OKTA_SAML)) {
|
||||
if (!user.authMethods?.includes(AuthMethod.OKTA_SAML)) {
|
||||
// initialize default organization and workspace
|
||||
await initializeDefaultOrg({
|
||||
organizationName,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Types } from "mongoose";
|
||||
import { BotOrgService } from "../../../services";
|
||||
import { SSOConfig } from "../../models";
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthMethod,
|
||||
MembershipOrg,
|
||||
User
|
||||
} from "../../../models";
|
||||
@@ -171,7 +171,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
authProviders: [AuthProvider.EMAIL],
|
||||
authProviders: [AuthMethod.EMAIL],
|
||||
$unset: {
|
||||
authProvider: 1,
|
||||
}
|
||||
|
||||
@@ -407,10 +407,8 @@ export const validateProviderAuthToken = async ({
|
||||
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
|
||||
);
|
||||
|
||||
let authProviders = [...(user.authProviders || []), user.authProvider];
|
||||
|
||||
if (
|
||||
!authProviders.includes(decodedToken.authProvider) ||
|
||||
!user.authMethods.includes(decodedToken.authProvider) ||
|
||||
decodedToken.email !== email
|
||||
) {
|
||||
throw new Error("Invalid authentication credentials.")
|
||||
|
||||
@@ -19,7 +19,7 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
|
||||
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
|
||||
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
|
||||
import TokenData, { ITokenData } from "./tokenData";
|
||||
import User, { AuthProvider, IUser } from "./user";
|
||||
import User, { AuthMethod, IUser } from "./user";
|
||||
import UserAction, { IUserAction } from "./userAction";
|
||||
import Workspace, { IWorkspace } from "./workspace";
|
||||
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
|
||||
@@ -28,7 +28,7 @@ import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
|
||||
import TokenVersion, { ITokenVersion } from "./tokenVersion";
|
||||
|
||||
export {
|
||||
AuthProvider,
|
||||
AuthMethod,
|
||||
BackupPrivateKey,
|
||||
IBackupPrivateKey,
|
||||
Bot,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export enum AuthProvider {
|
||||
export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
GITHUB = "github",
|
||||
@@ -11,9 +11,8 @@ export enum AuthProvider {
|
||||
|
||||
export interface IUser extends Document {
|
||||
_id: Types.ObjectId;
|
||||
authId?: string;
|
||||
authProvider?: AuthProvider;
|
||||
authProviders?: AuthProvider[];
|
||||
authProvider?: AuthMethod;
|
||||
authMethods: AuthMethod[];
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
@@ -37,17 +36,18 @@ export interface IUser extends Document {
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
{
|
||||
authId: {
|
||||
authProvider: { // TODO field: deprecate
|
||||
type: String,
|
||||
enum: AuthMethod,
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: AuthProvider,
|
||||
authMethods: {
|
||||
type: [{
|
||||
type: String,
|
||||
enum: AuthMethod,
|
||||
}],
|
||||
default: [AuthMethod.EMAIL],
|
||||
required: true
|
||||
},
|
||||
authProviders: [{
|
||||
type: String,
|
||||
enum: AuthProvider,
|
||||
}],
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { body, param } from "express-validator";
|
||||
import { usersController } from "../../controllers/v2";
|
||||
import { AuthMode } from "../../variables";
|
||||
import {
|
||||
AuthProvider
|
||||
AuthMethod
|
||||
} from "../../models";
|
||||
|
||||
router.get(
|
||||
@@ -40,36 +40,22 @@ router.patch(
|
||||
usersController.updateName
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/me/auth-provider",
|
||||
router.put(
|
||||
"/me/auth-methods",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
||||
}),
|
||||
body("authProvider").exists().isString().isIn([
|
||||
AuthProvider.EMAIL,
|
||||
AuthProvider.GOOGLE,
|
||||
AuthProvider.GITHUB
|
||||
]),
|
||||
validateRequest,
|
||||
usersController.updateAuthProvider
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/me/auth-providers",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||
}),
|
||||
body("authProviders").exists().isArray({
|
||||
body("authMethods").exists().isArray({
|
||||
min: 1,
|
||||
}).custom((authProviders: AuthProvider[]) => {
|
||||
return authProviders.every(provider => [
|
||||
AuthProvider.EMAIL,
|
||||
AuthProvider.GOOGLE,
|
||||
AuthProvider.GITHUB
|
||||
}).custom((authMethods: AuthMethod[]) => {
|
||||
return authMethods.every(provider => [
|
||||
AuthMethod.EMAIL,
|
||||
AuthMethod.GOOGLE,
|
||||
AuthMethod.GITHUB
|
||||
].includes(provider))
|
||||
}),
|
||||
validateRequest,
|
||||
usersController.updateAuthProviders,
|
||||
usersController.updateAuthMethods,
|
||||
);
|
||||
|
||||
router.get(
|
||||
|
||||
@@ -3,7 +3,7 @@ import passport from "passport";
|
||||
import { Types } from "mongoose";
|
||||
import { AuthData } from "../interfaces/middleware";
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthMethod,
|
||||
MembershipOrg,
|
||||
Organization,
|
||||
ServiceAccount,
|
||||
@@ -100,16 +100,13 @@ const initializePassport = async () => {
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email,
|
||||
authProviders: [AuthProvider.GOOGLE],
|
||||
authId: profile.id,
|
||||
authMethods: [AuthMethod.GOOGLE],
|
||||
firstName: profile.name.givenName,
|
||||
lastName: profile.name.familyName
|
||||
}).save();
|
||||
}
|
||||
|
||||
const authProviders = [...(user.authProviders || []), user.authProvider];
|
||||
|
||||
if (!authProviders.includes(AuthProvider.GOOGLE)) {
|
||||
if (!user.authMethods.includes(AuthMethod.GOOGLE)) {
|
||||
done(InternalServerError());
|
||||
}
|
||||
|
||||
@@ -120,7 +117,7 @@ const initializePassport = async () => {
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
authProvider: AuthProvider.GOOGLE,
|
||||
authMethod: AuthMethod.GOOGLE,
|
||||
isUserCompleted,
|
||||
...(req.query.state ? {
|
||||
callbackPort: req.query.state as string
|
||||
@@ -156,16 +153,13 @@ const initializePassport = async () => {
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email: email,
|
||||
authProviders: [AuthProvider.GITHUB],
|
||||
authId: profile.id,
|
||||
authMethods: [AuthMethod.GITHUB],
|
||||
firstName: profile.displayName,
|
||||
lastName: ""
|
||||
}).save();
|
||||
}
|
||||
|
||||
const authProviders = [...(user.authProviders || []), user.authProvider];
|
||||
|
||||
if (!authProviders.includes(AuthProvider.GITHUB)) {
|
||||
if (!user.authMethods.includes(AuthMethod.GITHUB)) {
|
||||
done(InternalServerError());
|
||||
}
|
||||
|
||||
@@ -176,7 +170,7 @@ const initializePassport = async () => {
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
authProvider: AuthProvider.GITHUB,
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
isUserCompleted,
|
||||
...(req.query.state ? {
|
||||
callbackPort: req.query.state as string
|
||||
@@ -222,7 +216,7 @@ const initializePassport = async () => {
|
||||
audience: await getSiteURL()
|
||||
});
|
||||
|
||||
if (ssoConfig.authProvider === AuthProvider.JUMPCLOUD_SAML) {
|
||||
if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) {
|
||||
samlConfig.wantAuthnResponseSigned = false;
|
||||
}
|
||||
|
||||
@@ -247,11 +241,21 @@ const initializePassport = async () => {
|
||||
}).select("+publicKey");
|
||||
|
||||
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(
|
||||
user._id,
|
||||
{
|
||||
authProviders: [req.ssoConfig.authProvider]
|
||||
authMethods: [req.ssoConfig.authProvider]
|
||||
},
|
||||
{
|
||||
new: true
|
||||
@@ -283,7 +287,7 @@ const initializePassport = async () => {
|
||||
} else {
|
||||
user = await new User({
|
||||
email,
|
||||
authProviders: [req.ssoConfig.authProvider],
|
||||
authMethods: [req.ssoConfig.authProvider],
|
||||
firstName,
|
||||
lastName
|
||||
}).save();
|
||||
@@ -305,7 +309,7 @@ const initializePassport = async () => {
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: organization?.name,
|
||||
authProvider: req.ssoConfig.authProvider,
|
||||
authMethod: req.ssoConfig.authProvider,
|
||||
isUserCompleted,
|
||||
...(req.body.RelayState ? {
|
||||
callbackPort: req.body.RelayState as string
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TrustedIP
|
||||
} from "../../ee/models";
|
||||
import {
|
||||
AuthMethod,
|
||||
BackupPrivateKey,
|
||||
Bot,
|
||||
BotOrg,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
User,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { generateKeyPair } from "../../utils/crypto";
|
||||
@@ -631,3 +633,40 @@ export const backfillTrustedIps = async () => {
|
||||
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,
|
||||
backfillServiceToken,
|
||||
backfillServiceTokenMultiScope,
|
||||
backfillTrustedIps
|
||||
backfillTrustedIps,
|
||||
backfillUserAuthMethods
|
||||
} from "./backfillData";
|
||||
import {
|
||||
reencryptBotOrgKeys,
|
||||
@@ -79,6 +80,7 @@ export const setup = async () => {
|
||||
await backfillIntegration();
|
||||
await backfillServiceTokenMultiScope();
|
||||
await backfillTrustedIps();
|
||||
await backfillUserAuthMethods();
|
||||
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||
|
||||
@@ -17,6 +17,5 @@ export {
|
||||
useRevokeMySessions,
|
||||
useUpdateMfaEnabled,
|
||||
useUpdateOrgUserRole,
|
||||
useUpdateUserAuthProvider,
|
||||
useUpdateUserAuthProviders,
|
||||
useUpdateUserAuthMethods
|
||||
} from "./queries";
|
||||
@@ -13,14 +13,14 @@ import {
|
||||
AddUserToWsDTO,
|
||||
AddUserToWsRes,
|
||||
APIKeyData,
|
||||
AuthMethod,
|
||||
CreateAPIKeyRes,
|
||||
DeletOrgMembershipDTO,
|
||||
OrgUser,
|
||||
RenameUserDTO,
|
||||
TokenVersion,
|
||||
UpdateOrgUserRoleDTO,
|
||||
User
|
||||
} from "./types";
|
||||
User} from "./types";
|
||||
|
||||
const userKeys = {
|
||||
getUser: ["user"] as const,
|
||||
@@ -61,39 +61,18 @@ export const useRenameUser = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserAuthProvider = () => {
|
||||
|
||||
export const useUpdateUserAuthMethods = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
authProvider
|
||||
authMethods
|
||||
}: {
|
||||
authProvider: string;
|
||||
authMethods: AuthMethod[];
|
||||
}) => {
|
||||
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
|
||||
authProvider
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useUpdateUserAuthProviders = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
authProviders
|
||||
}: {
|
||||
authProviders: string[];
|
||||
}) => {
|
||||
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-providers", {
|
||||
authProviders
|
||||
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-methods", {
|
||||
authMethods
|
||||
});
|
||||
|
||||
return user;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
|
||||
export enum AuthProvider {
|
||||
export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
OKTA_SAML = "okta-saml"
|
||||
GITHUB = "github",
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||
}
|
||||
|
||||
export type User = {
|
||||
@@ -12,8 +15,8 @@ export type User = {
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
authProvider?: AuthProvider;
|
||||
authProviders?: AuthProvider[];
|
||||
authProvider?: AuthMethod;
|
||||
authMethods: AuthMethod[];
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import { useEffect } from "react";
|
||||
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 * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
Checkbox
|
||||
} from "@app/components/v2";
|
||||
import { Switch } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
||||
import {
|
||||
useUpdateUserAuthProviders
|
||||
} from "@app/hooks/api";
|
||||
AuthMethod
|
||||
} from "@app/hooks/api/users/types";
|
||||
|
||||
const authMethodList = [
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "Google SSO", value: "google" },
|
||||
{ label: "GitHub SSO", value: "github" },
|
||||
{ label: "Okta SAML", value: "okta-saml" },
|
||||
{ label: "Azure SAML", value: "azure-saml" },
|
||||
{ label: "JumpCloud SAML", value: "jumpcloud-saml" }
|
||||
interface AuthMethodOption {
|
||||
label: string,
|
||||
value: AuthMethod,
|
||||
icon: IconDefinition;
|
||||
}
|
||||
|
||||
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({
|
||||
authMethods: yup.array().required("Auth method is required")
|
||||
});
|
||||
@@ -31,108 +37,101 @@ export type FormData = yup.InferType<typeof schema>;
|
||||
export const AuthMethodSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { user } = useUser();
|
||||
const { mutateAsync, isLoading } = useUpdateUserAuthProviders();
|
||||
|
||||
const defaultAuthMethods = user.authProviders?.length ?
|
||||
user.authProviders :
|
||||
[user?.authProvider ?? "email"];
|
||||
const { mutateAsync } = useUpdateUserAuthMethods();
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
authMethods: defaultAuthMethods,
|
||||
authMethods: user.authMethods,
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
|
||||
const selectedAuthMethods = watch("authMethods");
|
||||
const authMethods = watch("authMethods");
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset({
|
||||
authMethods: defaultAuthMethods,
|
||||
authMethods: user.authMethods,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
|
||||
const hasSamlEnabled = user.authMethods
|
||||
.some((authMethod: AuthMethod) => samlProviders.includes(authMethod));
|
||||
|
||||
const onFormSubmit = async ({
|
||||
authMethods
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (
|
||||
authMethods.includes("okta-saml")
|
||||
|| authMethods.includes("azure-saml")
|
||||
|| authMethods.includes("jumpcloud-saml")
|
||||
) {
|
||||
createNotification({
|
||||
text: "SAML authentication can only be configured in your organization settings",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
authProviders: authMethods
|
||||
});
|
||||
|
||||
if (hasSamlEnabled) {
|
||||
createNotification({
|
||||
text: "Successfully updated authentication method",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update authentication method",
|
||||
text: "SAML authentication can only be configured in your organization settings",
|
||||
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 (
|
||||
<form
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||
Authentication Method
|
||||
<div 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">
|
||||
Authentication methods
|
||||
</h2>
|
||||
<div className="max-w-md mb-4">
|
||||
{
|
||||
authMethodList.map(authMethod => (
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id={`auth-method-id-${authMethod.label}`}
|
||||
key={`auth-method-${authMethod.label}`}
|
||||
isChecked={selectedAuthMethods.includes(authMethod.value)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setValue("authMethods", [
|
||||
...selectedAuthMethods,
|
||||
authMethod.value
|
||||
])
|
||||
} else {
|
||||
setValue("authMethods", selectedAuthMethods.filter(auth => auth !== authMethod.value))
|
||||
}
|
||||
}}>
|
||||
{authMethod.label}
|
||||
</Checkbox>
|
||||
))
|
||||
}
|
||||
<p className="text-gray-400 mb-4">
|
||||
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.
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
{user && authMethodOpts.map((authMethodOpt) => {
|
||||
return (
|
||||
<div className="flex p-4 items-center" key={`auth-method-${authMethodOpt.value}`}>
|
||||
<div className="flex items-center w-20 mr-4">
|
||||
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
|
||||
<p>{authMethodOpt.label}</p>
|
||||
</div>
|
||||
<Switch
|
||||
id={`enable-${authMethodOpt.value}-auth`}
|
||||
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
|
||||
isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user