Skip to content

Commit

Permalink
fix: Correct credential storage check, update WebAuthn flow, add auth…
Browse files Browse the repository at this point in the history
… button on Login page
  • Loading branch information
wangjf8090 committed Aug 26, 2024
1 parent 32f16f8 commit 0e487c1
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 243 deletions.
41 changes: 5 additions & 36 deletions packages/itmat-ui-react/src/Fence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,14 @@ import { trpc } from './utils/trpc';

import { WebAuthnRegistrationComponent } from './utils/dmpWebauthn/webauthn.register';
import { WebAuthnAuthenticationComponent } from './utils/dmpWebauthn/webauthn.authenticate';
import { DeviceNicknameModal } from './utils/dmpWebauthn/webuathn.nickname';
import { DeviceNicknameComponent } from './utils/dmpWebauthn/webuathn.nickname';
import { useAuth } from './utils/dmpWebauthn/webauthn.context';


export const Fence: FunctionComponent = () => {
const {
showRegistrationDialog,
setShowRegistrationDialog,
credentials,
isUserLogin,
setIsUserLogin,
isWebauthAvailable,
useWebauthn,
setUseWebauthn,
showNicknameModal
setIsUserLogin
} = useAuth();

const whoAmI = trpc.user.whoAmI.useQuery();
Expand All @@ -48,26 +41,7 @@ export const Fence: FunctionComponent = () => {
} else {
setIsUserLogin(false);
}
if (isWebauthAvailable && credentials?.length === 0) {
setUseWebauthn('register');
} else if (isWebauthAvailable && (credentials?.length ?? 0) > 0) {
setUseWebauthn('authenticate');
} else {
setUseWebauthn('close');
}

}, [whoAmI.data, credentials]);

useEffect(() => {
if (!isWebauthAvailable) {
setShowRegistrationDialog(false);
} else if (useWebauthn === 'authenticate' && !isUserLogin) {
setShowRegistrationDialog(true); // Show the WebAuthn dialog
} else {
setShowRegistrationDialog(false);
}
}, [isWebauthAvailable, isUserLogin, useWebauthn, setShowRegistrationDialog]);

}, [whoAmI.data]);

useEffect(() => {
if (isAnyLoading) {
Expand All @@ -94,13 +68,8 @@ export const Fence: FunctionComponent = () => {
<Route path='/register' element={<RegisterNewUser />} />
<Route path='/register_webauthn' element={<WebAuthnRegistrationComponent />} />
<Route path='/authenticate_webauthn' element={<WebAuthnAuthenticationComponent />} />
<Route path='*' element={
<>
{component}
{showRegistrationDialog && <WebAuthnAuthenticationComponent />}
{showNicknameModal && <DeviceNicknameModal />}
</>
} />
<Route path='/nickname_webauthn' element={<DeviceNicknameComponent />} />
<Route path='*' element={component} />
</Routes>
);
};
Expand Down
40 changes: 38 additions & 2 deletions packages/itmat-ui-react/src/components/login/login.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FunctionComponent } from 'react';
import { NavLink } from 'react-router-dom';
import { NavLink, useNavigate} from 'react-router-dom';
import css from './login.module.css';
import { Input, Form, Button, Alert, Checkbox, message, Image } from 'antd';
import { Input, Form, Button, Alert, Checkbox, message, Image, Divider} from 'antd';
import { trpc } from '../../utils/trpc';
import LoadSpinner from '../reusable/loadSpinner';
import { useAuth } from '../../utils/dmpWebauthn/webauthn.context';
import { UserOutlined, KeyOutlined} from '@ant-design/icons';

export const LoginBox: FunctionComponent = () => {
const login = trpc.user.login.useMutation({
Expand All @@ -18,6 +20,9 @@ export const LoginBox: FunctionComponent = () => {
const getCurrentSubPath = trpc.domain.getCurrentSubPath.useQuery();
const getCurrentDomain = trpc.domain.getCurrentDomain.useQuery();

const navigate = useNavigate();
const { isWebauthAvailable, credentials } = useAuth(); // Accessing webauthn availability and credentials

if (getCurrentSubPath.isLoading || getCurrentDomain.isLoading) {
return <>
<div className='page_ariane'>Loading...</div>
Expand All @@ -32,6 +37,10 @@ export const LoginBox: FunctionComponent = () => {
</>;
}

const handleAuthLogin = () => {
navigate('/authenticate_webauthn');
};

return (
<div className={css.login_wrapper}>
<div className={css.login_box}>
Expand Down Expand Up @@ -76,6 +85,33 @@ export const LoginBox: FunctionComponent = () => {

</Form>
</div>

{/* Conditionally render the "Login with Authenticator" button */}
{(isWebauthAvailable && credentials && credentials.length > 0) && (
<>
<Divider plain>Or</Divider>
<Button
type="default"
onClick={handleAuthLogin}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
maxWidth: '280px',
margin: '0 auto',
borderColor: '#007bff',
color: '#007bff'
}}
>
<div style={{ display: 'flex', alignItems: 'center', marginRight: '8px' }}>
<UserOutlined style={{ fontSize: '16px' }} />
<KeyOutlined style={{ fontSize: '10px', marginLeft: '1px' }} />
</div>
Login with an authenticator
</Button>
</>
)}
<br />
<br />
<br />
Expand Down
73 changes: 40 additions & 33 deletions packages/itmat-ui-react/src/components/profile/webauthn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import React, { FunctionComponent, useState, useEffect } from 'react';
import { Alert, Button, Table, Modal, List, message } from 'antd';
import { AuthenticatorDevice } from '@itmat-broker/itmat-types';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../utils/dmpWebauthn/webauthn.context';
import css from './profile.module.css';
import LoadSpinner from '../reusable/loadSpinner';
import { trpc } from '../../utils/trpc';
import { useAuth } from '../../utils/dmpWebauthn/webauthn.context';

export const MyWebauthn: FunctionComponent = () => {
const navigate = useNavigate();
const { setShowRegistrationDialog } = useAuth();
const [devices, setDevices] = useState<AuthenticatorDevice[]>([]);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [selectedDevice, setSelectedDevice] = useState<AuthenticatorDevice | null>(null);
Expand All @@ -18,15 +17,44 @@ export const MyWebauthn: FunctionComponent = () => {

const { data, isLoading, isError, refetch } = trpc.webauthn.getWebauthnRegisteredDevices.useQuery();

// getWebAuthnID query to remove the webauthn_id when no devices remain
const { refetch: fetchWebAuthnID } = trpc.webauthn.getWebauthnID.useQuery(undefined, {
enabled: false,
onSuccess: (data) => {
if (!data) {
void message.warning('No Authenticator ID found for the user.');
return;
}
const webauthnID = data.id;
const updatedCredentials = credentials ? credentials.filter(id => id !== webauthnID) : [];
setCredentials(updatedCredentials); // Remove the ID from credentials
},
onError: () => {
void message.error('Failed to fetch WebAuthn ID.');
}
});
const { credentials, setCredentials } = useAuth(); // Access credentials from context

const deleteDeviceMutation = trpc.webauthn.deleteWebauthnRegisteredDevices.useMutation();
const updateDeviceNameMutation = trpc.webauthn.updateWebauthnDeviceName.useMutation();

useEffect(() => {
if (data) {
const fetchedDevices = data as AuthenticatorDevice[];
setDevices(fetchedDevices);
}
}, [data]);
const handleFetchWebAuthnID = async () => {
if (data) {
const fetchedDevices = data as AuthenticatorDevice[];
setDevices(fetchedDevices);

// If no devices exist, trigger the fetchWebAuthnID to remove the ID from credentials
if (fetchedDevices.length === 0) {
await fetchWebAuthnID(); // Trigger the query to remove the WebAuthn ID
}
}
};

handleFetchWebAuthnID().catch(() => {
void message.error('Error fetching Authenticator ID.');
});
}, [data, fetchWebAuthnID]);

const handleDeleteDevice = async () => {
if (selectedDevice) {
Expand Down Expand Up @@ -56,7 +84,6 @@ export const MyWebauthn: FunctionComponent = () => {
};

const handleNavigateToRegister = () => {
setShowRegistrationDialog(true);
navigate('/register_webauthn');
};

Expand Down Expand Up @@ -119,6 +146,7 @@ export const MyWebauthn: FunctionComponent = () => {
<>
<span style={{ marginRight: '10px' }}>{record.name || 'N/A'}</span>
<Button
type='primary'
onClick={() => handleEditClick(record.id, record.name || '')}
>
Edit
Expand All @@ -133,36 +161,15 @@ export const MyWebauthn: FunctionComponent = () => {
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (name: string | undefined) => (name ? name : 'N/A')
},
{
title: 'Credential ID',
dataIndex: 'credentialID',
key: 'credentialID'
},
{
title: 'Counter',
dataIndex: 'counter',
key: 'counter'
},
{
title: 'Transports',
dataIndex: 'transports',
key: 'transports',
render: (transports: string[]) => transports.join(', ')
render: renderSetNameColumn
},

{
title: 'Origin',
dataIndex: 'origin',
key: 'origin',
render: (origin: string | undefined) => (origin ? origin : 'N/A') // Show N/A if origin is null or undefined
},
{
title: 'Set Name',
dataIndex: '',
key: 'setName',
render: renderSetNameColumn
},
{
title: 'Action',
dataIndex: 'action',
Expand All @@ -187,7 +194,7 @@ export const MyWebauthn: FunctionComponent = () => {
<div className={css['overview-header']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div className={css['overview-icon']}></div>
<div>WebAuthn Devices</div>
<div>Authenticator Devices</div>
</div>
<div>
<Button type="primary" onClick={handleNavigateToRegister}>
Expand All @@ -209,7 +216,7 @@ export const MyWebauthn: FunctionComponent = () => {
/>
) : (
<div style={{ width: '100%' }}>
<p style={{ textAlign: 'left' }}>No WebAuthn devices found for this user.</p>
<p style={{ textAlign: 'left' }}>No Authenticator devices found for this user.</p>
</div>
)}
</div>
Expand Down
12 changes: 7 additions & 5 deletions packages/itmat-ui-react/src/components/scaffold/mainMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ export const MainMenuBar: FunctionComponent = () => {
window.location.reload();
}
});
const { useWebauthn } = useAuth(); // Access the WebAuthn state from context
if (whoAmI.isLoading) {
const { isWebauthAvailable} = useAuth(); // Access the WebAuthn state from context
const fetchedDevices = trpc.webauthn.getWebauthnRegisteredDevices.useQuery();

if (whoAmI.isLoading || fetchedDevices.isLoading) {
return <LoadSpinner />;
}
if (whoAmI.isError) {
if (whoAmI.isError || fetchedDevices.isError) {
return <p>
An error occured, please contact your administrator
</p>;
Expand Down Expand Up @@ -52,9 +54,9 @@ export const MainMenuBar: FunctionComponent = () => {

{/* Check if WebAuthn registration is needed and show another warning */}
{
useWebauthn === 'register' &&
(isWebauthAvailable && fetchedDevices.data.length === 0) &&
(
<Tooltip title="You could register a WebAuthn account in this device.">
<Tooltip title="You could register a Authenticator on this device.">
<ExclamationCircleOutlined twoToneColor="#ff0000" style={{ marginLeft: '8px' }} />
</Tooltip>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { localForage };

export function useLocalForage<D>(key: string, initialValue: D, errorHandler?: ErrorHandler) {
const [storedValue, setStoredValue] = useState<D | null>(initialValue);
const [isLoading, setIsLoading] = useState(true); // Add loading state
const _errorHandler = useRef(
(typeof errorHandler == 'undefined' || errorHandler == null) ? defaultErrorHandler : errorHandler
);
Expand All @@ -28,9 +29,12 @@ export function useLocalForage<D>(key: string, initialValue: D, errorHandler?: E
setStoredValue(value == null ? initialValue : value);
} catch (e) {
error(e as Error);
} finally {
setIsLoading(false); // Done fetching
}
})().catch((e) => {
error(e as Error);
setIsLoading(false); // Ensure loading ends in case of an error
});
}, [initialValue, key]);

Expand Down Expand Up @@ -62,5 +66,5 @@ export function useLocalForage<D>(key: string, initialValue: D, errorHandler?: E
});
}, [key]);

return [storedValue, setValue, removeValue] as const;
return [storedValue, setValue, removeValue, isLoading] as const;
}
Loading

0 comments on commit 0e487c1

Please sign in to comment.