Merge pull request #772 from Infisical/switch-to-google-sso

Add user support for changing authentication methods
This commit is contained in:
BlackMagiq
2023-07-22 12:38:22 +07:00
committed by GitHub
22 changed files with 264 additions and 31 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -1,6 +1,7 @@
import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml"
}

View File

@@ -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({

View File

@@ -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",

View File

@@ -13,4 +13,6 @@ export {
useLogoutUser,
useRegisterUserAction,
useRevokeMySessions,
useUpdateOrgUserRole} from "./queries";
useUpdateOrgUserRole,
useUpdateUserAuthProvider
} from "./queries";

View File

@@ -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,

View File

@@ -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;

View File

@@ -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"

View File

@@ -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

View File

@@ -24,7 +24,7 @@ export const OrgTabGroup = () => {
if (isRoleSufficient) {
tabs.push(
{ name: "SAML SSO", key: "tab-org-saml" }
{ name: "Authentication", key: "tab-org-auth" }
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export { AuthMethodSection } from "./AuthMethodSection";

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export { PersonalAuthTab } from "./PersonalAuthTab";

View File

@@ -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>
);

View File

@@ -0,0 +1 @@
export { PersonalGeneralTab } from "./PersonalGeneralTab";

View File

@@ -1 +0,0 @@
export { PersonalSecurityTab } from "./PersonalSecurityTab";

View File

@@ -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 />

View File

@@ -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();

View File

@@ -1 +1 @@
export { SecuritySection } from "./SecuritySection";
export { MFASection } from "./MFASection";

View File

@@ -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