mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 11:27:32 +00:00
Add user support for changing auth methods
This commit is contained in:
@@ -81,25 +81,62 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current user's name [firstName, lastName].
|
* Update name of the current user to [firstName, lastName].
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateName = async (req: Request, res: Response) => {
|
export const updateName = async (req: Request, res: Response) => {
|
||||||
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body;
|
const {
|
||||||
req.user.firstName = firstName;
|
firstName,
|
||||||
req.user.lastName = lastName || "";
|
lastName
|
||||||
|
}: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
await req.user.save();
|
const user = await User.findByIdAndUpdate(
|
||||||
|
req.user._id.toString(),
|
||||||
const user = req.user;
|
{
|
||||||
|
firstName,
|
||||||
|
lastName: lastName ?? ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update auth provider of the current user to [authProvider]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateAuthProvider = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
authProvider
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const user = await User.findByIdAndUpdate(
|
||||||
|
req.user._id.toString(),
|
||||||
|
{
|
||||||
|
authProvider
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return organizations that the current user is part of.
|
* Return organizations that the current user is part of.
|
||||||
* @param req
|
* @param req
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
if (!user) throw new Error("Failed to find user");
|
||||||
|
|
||||||
if (user.authProvider) {
|
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
|
||||||
await validateProviderAuthToken({
|
await validateProviderAuthToken({
|
||||||
email,
|
email,
|
||||||
user,
|
user,
|
||||||
@@ -117,7 +117,7 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
if (!user) throw new Error("Failed to find user");
|
||||||
|
|
||||||
if (user.authProvider) {
|
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
|
||||||
await validateProviderAuthToken({
|
await validateProviderAuthToken({
|
||||||
email,
|
email,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Document, Schema, Types, model } from "mongoose";
|
import { Document, Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
export enum AuthProvider {
|
export enum AuthProvider {
|
||||||
|
EMAIL = "email",
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
OKTA_SAML = "okta-saml"
|
OKTA_SAML = "okta-saml"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
AUTH_MODE_API_KEY,
|
AUTH_MODE_API_KEY,
|
||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
} from "../../variables";
|
} from "../../variables";
|
||||||
|
import {
|
||||||
|
AuthProvider
|
||||||
|
} from "../../models";
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/me",
|
"/me",
|
||||||
@@ -34,11 +37,25 @@ router.patch(
|
|||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||||
}),
|
}),
|
||||||
body("firstName").exists(),
|
body("firstName").exists().isString(),
|
||||||
|
body("lastName").isString(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
usersController.updateName
|
usersController.updateName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/me/auth-provider",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||||
|
}),
|
||||||
|
body("authProvider").exists().isString().isIn([
|
||||||
|
AuthProvider.EMAIL,
|
||||||
|
AuthProvider.GOOGLE
|
||||||
|
]),
|
||||||
|
validateRequest,
|
||||||
|
usersController.updateAuthProvider
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/me/organizations",
|
"/me/organizations",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
|||||||
@@ -227,7 +227,7 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"change": "Change password",
|
"change": "Change Password",
|
||||||
"current": "Current password",
|
"current": "Current password",
|
||||||
"current-wrong": "The current password may be wrong",
|
"current-wrong": "The current password may be wrong",
|
||||||
"new": "New password",
|
"new": "New password",
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ export {
|
|||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useRevokeMySessions,
|
useRevokeMySessions,
|
||||||
useUpdateOrgUserRole} from "./queries";
|
useUpdateOrgUserRole,
|
||||||
|
useUpdateUserAuthProvider
|
||||||
|
} from "./queries";
|
||||||
|
|||||||
@@ -58,6 +58,27 @@ export const useRenameUser = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateUserAuthProvider = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
authProvider
|
||||||
|
}: {
|
||||||
|
authProvider: string;
|
||||||
|
}) => {
|
||||||
|
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
|
||||||
|
authProvider
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getUser);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetUserAction = (action: string) =>
|
export const useGetUserAction = (action: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: userKeys.userAction,
|
queryKey: userKeys.userAction,
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { UserWsKeyPair } from "../keys/types";
|
import { UserWsKeyPair } from "../keys/types";
|
||||||
|
|
||||||
|
export enum AuthProvider {
|
||||||
|
EMAIL = "email",
|
||||||
|
GOOGLE = "google",
|
||||||
|
OKTA_SAML = "okta-saml"
|
||||||
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
email?: string;
|
email?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
authProvider?: AuthProvider;
|
||||||
encryptionVersion?: number;
|
encryptionVersion?: number;
|
||||||
protectedKey?: string;
|
protectedKey?: string;
|
||||||
protectedKeyIV?: string;
|
protectedKeyIV?: string;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { FormEvent, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
|
|
||||||
import Error from "@app/components/basic/Error";
|
import Error from "@app/components/basic/Error";
|
||||||
@@ -159,7 +159,7 @@ export const InitialStep = ({
|
|||||||
<span className='px-4 text-sm text-bunker-400'>or</span>
|
<span className='px-4 text-sm text-bunker-400'>or</span>
|
||||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||||
</div>
|
</div>
|
||||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@@ -172,7 +172,7 @@ export const InitialStep = ({
|
|||||||
>
|
>
|
||||||
{t("login.continue-with-google")}
|
{t("login.continue-with-google")}
|
||||||
</Button>
|
</Button>
|
||||||
</div> */}
|
</div>
|
||||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const OrgSSOSection = (): JSX.Element => {
|
|||||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex items-center mb-8">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||||
Configuration
|
SAML SSO Configuration
|
||||||
</h2>
|
</h2>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const OrgTabGroup = () => {
|
|||||||
|
|
||||||
if (isRoleSufficient) {
|
if (isRoleSufficient) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
{ name: "SAML SSO", key: "tab-org-saml" }
|
{ name: "Authentication", key: "tab-org-auth" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
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,
|
||||||
|
Select,
|
||||||
|
SelectItem} from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
|
import {
|
||||||
|
useUpdateUserAuthProvider
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
const authMethods = [
|
||||||
|
{ label: "Email", value: "email" },
|
||||||
|
{ label: "Google SSO", value: "google" },
|
||||||
|
{ label: "Okta SAML 2.0", value: "okta-saml" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
authMethod: yup.string().required("Auth method is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
export const AuthMethodSection = () => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { user } = useUser();
|
||||||
|
const { mutateAsync, isLoading } = useUpdateUserAuthProvider();
|
||||||
|
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<FormData>({
|
||||||
|
defaultValues: {
|
||||||
|
authMethod: user?.authProvider ?? "email"
|
||||||
|
},
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
reset({
|
||||||
|
authMethod: user?.authProvider ?? "email"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
authMethod
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
if (authMethod === "okta-saml") {
|
||||||
|
createNotification({
|
||||||
|
text: "Okta SAML 2.0 can only be configured in your organization settings",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
authProvider: authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated authentication method",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update authentication method",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
</h2>
|
||||||
|
<div className="max-w-md mb-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="authMethod"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{authMethods.map((authMethod) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={authMethod.value}
|
||||||
|
key={`auth-method-${authMethod.value}`}
|
||||||
|
>
|
||||||
|
{authMethod.label}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { AuthMethodSection } from "./AuthMethodSection";
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { AuthMethodSection } from "../AuthMethodSection";
|
||||||
|
import { ChangePasswordSection } from "../ChangePasswordSection";
|
||||||
|
import { MFASection } from "../SecuritySection";
|
||||||
|
|
||||||
|
export const PersonalAuthTab = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MFASection />
|
||||||
|
<AuthMethodSection />
|
||||||
|
<ChangePasswordSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PersonalAuthTab } from "./PersonalAuthTab";
|
||||||
+1
-5
@@ -1,18 +1,14 @@
|
|||||||
import { ChangeLanguageSection } from "../ChangeLanguageSection";
|
import { ChangeLanguageSection } from "../ChangeLanguageSection";
|
||||||
import { ChangePasswordSection } from "../ChangePasswordSection";
|
|
||||||
import { EmergencyKitSection } from "../EmergencyKitSection";
|
import { EmergencyKitSection } from "../EmergencyKitSection";
|
||||||
import { SecuritySection } from "../SecuritySection";
|
|
||||||
import { SessionsSection } from "../SessionsSection";
|
import { SessionsSection } from "../SessionsSection";
|
||||||
import { UserNameSection } from "../UserNameSection";
|
import { UserNameSection } from "../UserNameSection";
|
||||||
|
|
||||||
export const PersonalSecurityTab = () => {
|
export const PersonalGeneralTab = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UserNameSection />
|
<UserNameSection />
|
||||||
<ChangeLanguageSection />
|
<ChangeLanguageSection />
|
||||||
<SecuritySection />
|
|
||||||
<SessionsSection />
|
<SessionsSection />
|
||||||
<ChangePasswordSection />
|
|
||||||
<EmergencyKitSection />
|
<EmergencyKitSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PersonalGeneralTab } from "./PersonalGeneralTab";
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { PersonalSecurityTab } from "./PersonalSecurityTab";
|
|
||||||
+8
-3
@@ -2,10 +2,12 @@ import { Fragment } from "react"
|
|||||||
import { Tab } from "@headlessui/react"
|
import { Tab } from "@headlessui/react"
|
||||||
|
|
||||||
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
|
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
|
||||||
import { PersonalSecurityTab } from "../PersonalSecurityTab";
|
import { PersonalAuthTab } from "../PersonalAuthTab";
|
||||||
|
import { PersonalGeneralTab } from "../PersonalGeneralTab";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", key: "tab-account-security" },
|
{ name: "General", key: "tab-account-general" },
|
||||||
|
{ name: "Authentication", key: "tab-account-auth" },
|
||||||
{ name: "API Keys", key: "tab-account-api-keys" }
|
{ name: "API Keys", key: "tab-account-api-keys" }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -28,7 +30,10 @@ export const PersonalTabGroup = () => {
|
|||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<PersonalSecurityTab />
|
<PersonalGeneralTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<PersonalAuthTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<PersonalAPIKeyTab />
|
<PersonalAPIKeyTab />
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import { useGetUser } from "../../../../hooks/api";
|
|||||||
import { User } from "../../../../hooks/api/types";
|
import { User } from "../../../../hooks/api/types";
|
||||||
import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled";
|
import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled";
|
||||||
|
|
||||||
export const SecuritySection = () => {
|
export const MFASection = () => {
|
||||||
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
|
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
|
||||||
const { data: user } = useGetUser();
|
const { data: user } = useGetUser();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
@@ -1 +1 @@
|
|||||||
export { SecuritySection } from "./SecuritySection";
|
export { MFASection } from "./MFASection";
|
||||||
+1
-1
@@ -55,7 +55,7 @@ export const UserNameSection = (): JSX.Element => {
|
|||||||
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"
|
||||||
>
|
>
|
||||||
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
|
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
|
||||||
User name
|
Name
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 max-w-md">
|
<div className="mb-2 max-w-md">
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
Reference in New Issue
Block a user