Skip to content

Commit

Permalink
Introduce biometrics expiry to reduce challenges on refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
ystxn committed Mar 31, 2024
1 parent 31cea18 commit e518fb6
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 21 deletions.
36 changes: 21 additions & 15 deletions src/core/session.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,17 @@ const Session = () => {
const isPublicEndpoint = () => [ '/login', '/register' ].indexOf(location.pathname) > -1;

useEffect(() => {
if (session !== undefined) {
console.debug('UE1: Session exists');
return;
}
console.debug('UE1: Session does not exist');
const token = window.localStorage.getItem('token');
if (!token) {
console.debug('UE1: No stored token');
console.debug('UE1: No stored token exists');
if (!isPublicEndpoint()) {
navigate('/login', { replace: true });
}
setLoading(false);
return;
}
let jwt = parseJwt(token);
console.debug('UE1: Stored token', jwt);
console.debug('UE1: Stored token exists:', jwt);
if (new Date().getTime() >= (jwt.exp * 1000)) {
console.debug('UE1: Stored token has expired');
window.localStorage.clear();
Expand All @@ -41,17 +36,24 @@ const Session = () => {
setLoading(false);
} else {
console.debug('UE1: Stored token still valid');
const biometrics = window.localStorage.getItem('biometrics');
const biometrics = JSON.parse(window.localStorage.getItem('biometrics'));

if (!biometrics) {
console.debug('UE1: Biometrics not enabled');
console.debug('UE1: Biometrics is not enabled');
setSession({ token, name: jwt.name, email: jwt.sub, admin: jwt.admin });
setLoading(false);
return;
}
console.debug('UE1: Biometrics is enabled');
if (new Date() < new Date(biometrics?.expiry)) {
console.debug('UE1: Biometrics not expired yet, skip challenge');
setSession({ token, name: jwt.name, email: jwt.sub, admin: jwt.admin });
setLoading(false);
return;
}
console.debug('UE1: Biometrics enabled');
console.debug('UE1: Biometrics expired, initiating challenge');
challenge((response) => {
const { id } = JSON.parse(biometrics);
let originalId = new Uint8Array(atob(id).split('').map((c) => c.charCodeAt(0)));
let originalId = new Uint8Array(atob(biometrics.id).split('').map((c) => c.charCodeAt(0)));

const publicKey = {
challenge: Uint8Array.from(response.token, c => c.charCodeAt(0)),
Expand All @@ -63,10 +65,14 @@ const Session = () => {
};
navigator.credentials.get({ publicKey }).then(
() => {
console.debug('UE1: Biometrics challenge successful');
console.debug('UE1: Biometrics challenge successful, unlocking session');
setSession({ token, name: jwt.name, email: jwt.sub, admin: jwt.admin });
showStatus('success', 'Session unlocked');
setLoading(false);
window.localStorage.setItem('biometrics', JSON.stringify({
...biometrics,
expiry: new Date((new Date()).getTime() + (5 * 60 * 1000)).toISOString(),
}));
},
(err) => {
console.debug('UE1: Biometrics challenge failed');
Expand All @@ -76,6 +82,7 @@ const Session = () => {
navigate('/login', { replace: true });
showStatus('error', 'Invalid biometric credentials');
setLoading(false);
console.debug('UE1: Biometrics challenge failed, logging');
}
);
});
Expand All @@ -84,7 +91,6 @@ const Session = () => {

useEffect(() => {
if (!session) {
console.debug('UE2: No session exists');
return;
}
console.debug('UE2: Session exists');
Expand All @@ -93,7 +99,7 @@ const Session = () => {
console.debug('UE2: Session still fresh');
return;
}
console.debug('UE2: Session not fresh');
console.debug('UE2: Session near expiry, initiating refresh');
refreshToken(({ token }) => {
console.debug(`UE2: Obtained new token: ${token}`);
window.localStorage.setItem('token', token);
Expand Down
4 changes: 2 additions & 2 deletions src/public/login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const Login = () => {
<form onSubmit={submit} autoComplete="off">
<Stack spacing={2}>
<Title>Login</Title>
<TextField required name="username" type="email" label="Email" inputProps={{ minLength: 7 }} />
<TextField required name="password" type="password" label="Password" inputProps={{ minLength: 8 }} />
<TextField required name="username" type="email" label="Email" inputProps={{ minLength: 7, autoComplete: 'username' }} />
<TextField required name="password" type="password" label="Password" inputProps={{ minLength: 8, autoComplete: 'password' }} />
<LoadingButton
type="submit"
endIcon={<LoginIcon />}
Expand Down
9 changes: 5 additions & 4 deletions src/settings/profile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const Profile = () => {
name: session.email,
displayName: session.name,
},
pubKeyCredParams: [{ type, alg: -7 }],
pubKeyCredParams: [{ type, alg: -7 }, { type, alg: -257 }],
authenticatorSelection: {
requireResidentKey: true,
userVerification: 'required',
Expand All @@ -96,7 +96,8 @@ const Profile = () => {
const attestation = {
id: credentialIdBase64,
clientDataJSON: arrayBufferToString(credential.response.clientDataJSON),
attestationObject: base64encode(credential.response.attestationObject)
attestationObject: base64encode(credential.response.attestationObject),
expiry: new Date().toISOString(),
};
localStorage.setItem('biometrics', JSON.stringify(attestation));
showStatus('success', 'Biometrics registered successfully')
Expand All @@ -115,8 +116,8 @@ const Profile = () => {
color={localStorage.getItem("biometrics") ? 'success' : 'info'}
/>
</Stack>
<TextField required name="displayName" label="Name" inputProps={{ minLength: 3 }} defaultValue={session.name} />
<TextField required name="username" type="email" label="Email" inputProps={{ minLength: 7 }} defaultValue={session.email} />
<TextField required name="displayName" label="Name" inputProps={{ minLength: 3, autoComplete: 'off' }} defaultValue={session.name} />
<TextField required name="username" type="email" label="Email" inputProps={{ minLength: 7, autoComplete: 'off' }} defaultValue={session.email} />
<Tooltip title="Enter only if you wish to change your password">
<TextField name="password" type="password" label="Current Password" inputProps={{ minLength: 8 }} autoComplete="new-password" />
</Tooltip>
Expand Down

0 comments on commit e518fb6

Please sign in to comment.