mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #772 from Infisical/switch-to-google-sso
Add user support for changing authentication 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 res
|
||||
* @returns
|
||||
*/
|
||||
export const updateName = async (req: Request, res: Response) => {
|
||||
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body;
|
||||
req.user.firstName = firstName;
|
||||
req.user.lastName = lastName || "";
|
||||
const {
|
||||
firstName,
|
||||
lastName
|
||||
}: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
} = req.body;
|
||||
|
||||
await req.user.save();
|
||||
|
||||
const user = req.user;
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
firstName,
|
||||
lastName: lastName ?? ""
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
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.
|
||||
* @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.authProvider) {
|
||||
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
|
||||
await validateProviderAuthToken({
|
||||
email,
|
||||
user,
|
||||
@@ -117,7 +117,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
if (user.authProvider) {
|
||||
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
|
||||
await validateProviderAuthToken({
|
||||
email,
|
||||
user,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export enum AuthProvider {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
OKTA_SAML = "okta-saml"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_JWT,
|
||||
} from "../../variables";
|
||||
import {
|
||||
AuthProvider
|
||||
} from "../../models";
|
||||
|
||||
router.get(
|
||||
"/me",
|
||||
@@ -34,11 +37,25 @@ router.patch(
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||
}),
|
||||
body("firstName").exists(),
|
||||
body("firstName").exists().isString(),
|
||||
body("lastName").isString(),
|
||||
validateRequest,
|
||||
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(
|
||||
"/me/organizations",
|
||||
requireAuth({
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
},
|
||||
"password": {
|
||||
"password": "Password",
|
||||
"change": "Change password",
|
||||
"change": "Change Password",
|
||||
"current": "Current password",
|
||||
"current-wrong": "The current password may be wrong",
|
||||
"new": "New password",
|
||||
|
||||
@@ -13,4 +13,6 @@ export {
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
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) =>
|
||||
useQuery({
|
||||
queryKey: userKeys.userAction,
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
|
||||
export enum AuthProvider {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
OKTA_SAML = "okta-saml"
|
||||
}
|
||||
|
||||
export type User = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
authProvider?: AuthProvider;
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { FormEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import axios from "axios"
|
||||
|
||||
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>
|
||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||
</div>
|
||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
@@ -172,7 +172,7 @@ export const InitialStep = ({
|
||||
>
|
||||
{t("login.continue-with-google")}
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
|
||||
@@ -26,7 +26,7 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
"upgradePlan",
|
||||
"addSSO"
|
||||
] as const);
|
||||
|
||||
|
||||
const handleSamlSSOToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
@@ -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="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Configuration
|
||||
SAML SSO Configuration
|
||||
</h2>
|
||||
{!isLoading && (
|
||||
<Button
|
||||
|
||||
@@ -24,7 +24,7 @@ export const OrgTabGroup = () => {
|
||||
|
||||
if (isRoleSufficient) {
|
||||
tabs.push(
|
||||
{ name: "SAML SSO", key: "tab-org-saml" }
|
||||
{ name: "Authentication", key: "tab-org-auth" }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,18 +1,14 @@
|
||||
import { ChangeLanguageSection } from "../ChangeLanguageSection";
|
||||
import { ChangePasswordSection } from "../ChangePasswordSection";
|
||||
import { EmergencyKitSection } from "../EmergencyKitSection";
|
||||
import { SecuritySection } from "../SecuritySection";
|
||||
import { SessionsSection } from "../SessionsSection";
|
||||
import { UserNameSection } from "../UserNameSection";
|
||||
|
||||
export const PersonalSecurityTab = () => {
|
||||
export const PersonalGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<UserNameSection />
|
||||
<ChangeLanguageSection />
|
||||
<SecuritySection />
|
||||
<SessionsSection />
|
||||
<ChangePasswordSection />
|
||||
<EmergencyKitSection />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { PersonalGeneralTab } from "./PersonalGeneralTab";
|
||||
@@ -1 +0,0 @@
|
||||
export { PersonalSecurityTab } from "./PersonalSecurityTab";
|
||||
@@ -2,10 +2,12 @@ import { Fragment } from "react"
|
||||
import { Tab } from "@headlessui/react"
|
||||
|
||||
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
|
||||
import { PersonalSecurityTab } from "../PersonalSecurityTab";
|
||||
import { PersonalAuthTab } from "../PersonalAuthTab";
|
||||
import { PersonalGeneralTab } from "../PersonalGeneralTab";
|
||||
|
||||
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" }
|
||||
];
|
||||
|
||||
@@ -28,7 +30,10 @@ export const PersonalTabGroup = () => {
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<PersonalSecurityTab />
|
||||
<PersonalGeneralTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PersonalAuthTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PersonalAPIKeyTab />
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useGetUser } from "../../../../hooks/api";
|
||||
import { User } from "../../../../hooks/api/types";
|
||||
import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled";
|
||||
|
||||
export const SecuritySection = () => {
|
||||
export const MFASection = () => {
|
||||
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
|
||||
const { data: user } = useGetUser();
|
||||
const { createNotification } = useNotificationContext();
|
||||
@@ -1 +1 @@
|
||||
export { SecuritySection } from "./SecuritySection";
|
||||
export { MFASection } from "./MFASection";
|
||||
@@ -55,7 +55,7 @@ export const UserNameSection = (): JSX.Element => {
|
||||
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">
|
||||
User name
|
||||
Name
|
||||
</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
|
||||
Reference in New Issue
Block a user