From 6dfb843b7efb7cf624e5e4775719b4e11c7db0c2 Mon Sep 17 00:00:00 2001 From: Arie Peterson <arie@greenhost.nl> Date: Fri, 16 Feb 2024 16:26:27 +0100 Subject: [PATCH] Separate buttons for admins for resetting TOTP and WebAuthn --- .../src/components/UserModal/UserModal.tsx | 66 +++++++++++++++++-- .../src/services/users/hooks/use-users.ts | 7 ++ frontend/src/services/users/redux/actions.ts | 20 +++++- .../src/services/users/transformations.ts | 8 +++ frontend/src/services/users/types.ts | 1 + 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/UserModal/UserModal.tsx b/frontend/src/components/UserModal/UserModal.tsx index a30529d9..2d8b31ef 100644 --- a/frontend/src/components/UserModal/UserModal.tsx +++ b/frontend/src/components/UserModal/UserModal.tsx @@ -20,6 +20,7 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP const [deleteModal, setDeleteModal] = useState(false); const [passwordLinkModal, setPasswordLinkModal] = useState(false); const [totpModal, setTotpModal] = useState(false); + const [webAuthnModal, setWebAuthnModal] = useState(false); const [isAdminRoleSelected, setAdminRoleSelected] = useState(true); const [isPersonalModal, setPersonalModal] = useState(false); const { @@ -35,6 +36,7 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP getRecoveryLinkUserById, clearSelectedUser, resetTotp, + resetWebAuthn, } = useUsers(); const { currentUser, isAdmin } = useAuth(); @@ -172,6 +174,17 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP const totpModalClose = () => setTotpModal(false); + const webAuthnModalOpen = () => { + if (userId) { + resetWebAuthn(userId); + clearSelectedUser(); + setUserId(userId); + } + setWebAuthnModal(true); + }; + + const webAuthnModalClose = () => setWebAuthnModal(false); + const handleDelete = () => { if (userId) { deleteUserById(userId); @@ -218,7 +231,7 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP ); }; - // Button to reset 2FA + // Button to reset TOTP const buttonTotp = () => { return ( userId && @@ -237,6 +250,25 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP ); }; + // Button to reset WebAuthn + const buttonWebAuthn = () => { + return ( + userId && + isAdmin && + user.webauthn && ( + <button + onClick={webAuthnModalOpen} + type="button" + className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm + font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-700" + > + Reset WebAuthn + <QrcodeIcon className="-mr-0.5 ml-2 h-4 w-4" aria-hidden="true" /> + </button> + ) + ); + }; + return ( <> <Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={userModalLoading} useCancelButton> @@ -392,10 +424,9 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP {buttonPasswordLink()} </div> </li> - {/* {user.totp && ( */} <li className="py-4"> <div className="flex items-center justify-between"> - <p className="leading-6 text-sm text-gray-500">Reset 2-factor authentication</p> + <p className="leading-6 text-sm text-gray-500">Reset TOTP</p> {user.totp ? ( <>{buttonTotp()}</> ) : ( @@ -403,12 +434,26 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP className="leading-6 text-sm text-gray-400 mb-4 sm:mb-0 inline-flex items-center px-4 py-2 font-medium rounded-md bg-gray-50" > - No 2FA set + No TOTP enrolled + </p> + )} + </div> + </li> + <li className="py-4"> + <div className="flex items-center justify-between"> + <p className="leading-6 text-sm text-gray-500">Reset WebAuthn</p> + {user.webauthn ? ( + <>{buttonWebAuthn()}</> + ) : ( + <p + className="leading-6 text-sm text-gray-400 mb-4 sm:mb-0 inline-flex items-center px-4 py-2 + font-medium rounded-md bg-gray-50" + > + No WebAuthn registered </p> )} </div> </li> - {/* )} */} {user.email !== currentUser?.email && ( <li className="py-4"> @@ -443,8 +488,15 @@ export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalP <InfoModal open={totpModal} onClose={totpModalClose} - title="Reset 2-Factor Authentication" - body="You have successfully removed the user's 2FA device." + title="Reset TOTP" + body="You have successfully removed the user's TOTP registration." + dynamicData="" + /> + <InfoModal + open={webAuthnModal} + onClose={webAuthnModalClose} + title="Reset WebAuthn" + body="You have successfully removed the user's WebAuthn registration." dynamicData="" /> </> diff --git a/frontend/src/services/users/hooks/use-users.ts b/frontend/src/services/users/hooks/use-users.ts index ebcbb917..41761e09 100644 --- a/frontend/src/services/users/hooks/use-users.ts +++ b/frontend/src/services/users/hooks/use-users.ts @@ -13,6 +13,7 @@ import { createBatchUsers, fetchRecoveryLink, resetTotpById, + resetWebAuthnById, } from '../redux'; import { getUserById, getRecoveryLink, getUserModalLoading, getUserslLoading } from '../redux/selectors'; @@ -63,6 +64,7 @@ export function useUsers() { function deleteUserById(id: string) { return dispatch(deleteUser(id)); } + function getRecoveryLinkUserById(id: string) { return dispatch(fetchRecoveryLink(id)); } @@ -71,6 +73,10 @@ export function useUsers() { return dispatch(resetTotpById(id)); } + function resetWebAuthn(id: string) { + return dispatch(resetWebAuthnById(id)); + } + return { users, user, @@ -89,5 +95,6 @@ export function useUsers() { clearSelectedUser, createUsers, resetTotp, + resetWebAuthn, }; } diff --git a/frontend/src/services/users/redux/actions.ts b/frontend/src/services/users/redux/actions.ts index 76e7eaad..e152cda7 100644 --- a/frontend/src/services/users/redux/actions.ts +++ b/frontend/src/services/users/redux/actions.ts @@ -13,6 +13,7 @@ import { transformUpdateMultipleUsers, transformRecoveryLink, transformTotp, + transformWebAuthn, } from '../transformations'; export enum UserActionTypes { @@ -27,6 +28,7 @@ export enum UserActionTypes { CREATE_BATCH_USERS = 'users/create_batch_users', UPDATE_MULTIPLE_USERS = '/users/multi-edit', RESET_TOTP = 'users/reset-totp-user', + RESET_WEBAUTHN = 'users/reset-webauthn-user', } export const setUsersLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => { @@ -240,7 +242,7 @@ export const fetchRecoveryLink = (id: string) => async (dispatch: Dispatch<any>) export const resetTotpById = (id: string) => async (dispatch: Dispatch<any>) => { try { const { data } = await performApiCall({ - path: `/users/${id}/reset_2fa`, + path: `/users/${id}/reset_totp`, method: 'POST', }); @@ -253,6 +255,22 @@ export const resetTotpById = (id: string) => async (dispatch: Dispatch<any>) => } }; +export const resetWebAuthnById = (id: string) => async (dispatch: Dispatch<any>) => { + try { + const { data } = await performApiCall({ + path: `/users/${id}/reset_webauthn`, + method: 'POST', + }); + + dispatch({ + type: UserActionTypes.RESET_WEBAUTHN, + payload: transformWebAuthn(data), + }); + } catch (err) { + console.error(err); + } +}; + export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => { dispatch(setUserModalLoading(true)); diff --git a/frontend/src/services/users/transformations.ts b/frontend/src/services/users/transformations.ts index 5fa7c84a..eee6b193 100644 --- a/frontend/src/services/users/transformations.ts +++ b/frontend/src/services/users/transformations.ts @@ -52,6 +52,13 @@ export const transformTotp = (data: any) => { return undefined; }; +export const transformWebAuthn = (data: any) => { + if (data.credentials !== undefined) { + return data.credentials.webauthn !== undefined; + } + return undefined; +}; + export const transformUser = (response: any): User => { return { id: response.id ?? '', @@ -61,6 +68,7 @@ export const transformUser = (response: any): User => { preferredUsername: response.preferredUsername ?? '', status: response.state ?? '', totp: transformTotp(response), + webauthn: transformWebAuthn(response), tags: response.stackspin_data.tags ?? '', admin: response.stackspin_data.stackspin_admin ?? '', meta: response.metadata_admin ?? '', diff --git a/frontend/src/services/users/types.ts b/frontend/src/services/users/types.ts index 89f54481..7efaab2b 100644 --- a/frontend/src/services/users/types.ts +++ b/frontend/src/services/users/types.ts @@ -6,6 +6,7 @@ export interface User { preferredUsername: string; status: string; totp?: boolean; + webauthn?: boolean; tags?: []; admin?: boolean; meta?: []; -- GitLab