Skip to content

Commit

Permalink
Don't offer passkey authentication when no passkeys are available
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Jan 26, 2024
1 parent b9cac25 commit f04954b
Show file tree
Hide file tree
Showing 5 changed files with 8 additions and 66 deletions.
4 changes: 2 additions & 2 deletions app/api/auth/confirmIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function confirmIdentity(request: Request, props: ActionProps): Pro
let authenticationOptions = undefined;

const credentials = await retrieveCredentials(user);
if (credentials) {
if (credentials.length > 0) {
authenticationOptions = await generateAuthenticationOptions({
allowCredentials: credentials.map(credential => ({
id: credential.credentialId,
Expand All @@ -72,5 +72,5 @@ export async function confirmIdentity(request: Request, props: ActionProps): Pro
success: true,
activated: user.activated,
authenticationOptions
}
};
}
15 changes: 3 additions & 12 deletions app/registration/AuthenticationFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { SxProps, Theme } from '@mui/system';
import Dialog from '@mui/material/Dialog';

import type { User } from '@lib/auth/User';
import { callApi } from '@lib/callApi';

import { ActivationReminderDialog } from './authentication/ActivationReminderDialog';
import { IdentityDialog } from './authentication/IdentityDialog';
import { IdentityAccountDialog } from './authentication/IdentityAccountDialog';
Expand All @@ -25,17 +27,6 @@ import { RegisterConfirmDialog } from './authentication/RegisterConfirmDialog';
import { UsernameDialog } from './authentication/UsernameDialog';
import { validatePassword } from './authentication/PasswordField';

import type { ConfirmIdentityDefinition } from '@app/api/auth/confirmIdentity';
import type { PasswordChangeDefinition } from '@app/api/auth/passwordChange';
import type { PasswordResetDefinition } from '@app/api/auth/passwordReset';
import type { PasswordResetRequestDefinition } from '@app/api/auth/passwordResetRequest';
import type { RegisterDefinition } from '@app/api/auth/register';
import type { SignInPasskeyDefinition } from '@app/api/auth/signInPasskey';
import type { SignInPasswordDefinition } from '@app/api/auth/signInPassword';
import type { SignInPasswordUpdateDefinition } from '@app/api/auth/signInPasswordUpdate';
import type { SignOutDefinition } from '@app/api/auth/signOut';
import { callApi } from '@lib/callApi';

/**
* Styles used by the various components that make up the authentication flow.
*/
Expand Down Expand Up @@ -194,7 +185,7 @@ export function AuthenticationFlow(props: AuthenticationFlowProps) {
setUsername(username);

if (response.success && response.activated) {
if (response.authenticationOptions && browserSupportsWebAuthn()) {
if (!!response.authenticationOptions && browserSupportsWebAuthn()) {
try {
const result = await startAuthentication(response.authenticationOptions);
const verification = await callApi('post', '/api/auth/sign-in-passkey', {
Expand Down
2 changes: 1 addition & 1 deletion app/registration/authentication/IdentityPasswordDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function IdentityPasswordDialog(props: IdentityPasswordDialogProps) {
</Box>
<Box sx={{ pt: 2 }}>
<PasswordField name="updatedPassword" label="New password" type="password"
fullWidth size="small" required requireNumberSum
fullWidth size="small" required
autoComplete="new-password" />
</Box>
<Collapse in={!!error}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function LostPasswordResetDialog(props: LostPasswordResetDialogProps) {
</Collapse>
<Box sx={{ pt: 2 }}>
<PasswordField name="password" label="New password" type="password"
fullWidth size="small" required requireNumberSum
fullWidth size="small" required
autoFocus autoComplete="new-password" />
</Box>
</DialogContent>
Expand Down
51 changes: 1 addition & 50 deletions app/registration/authentication/PasswordField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ interface PasswordValidationState {
* Whether the password contains at least one number.
*/
passedNumberRequirement: boolean;

/**
* Whether the numbers in the password add up to 25.
*/
passedNumberSumRequirement: boolean;
}

/**
Expand Down Expand Up @@ -71,19 +66,6 @@ function validatePasswordNumberRequirement(password: string): boolean {
return /[0-9]/.test(password);
}

/**
* Validates that the given `password` contains numbers that add up to 25.
*/
function validatePasswordNumberSumRequirement(password: string): boolean {
const numbers = [ ...password.matchAll(/\d/g) ];

let total = 0;
for (const number of numbers)
total += parseInt(number[0]);

return total === 25;
}

/**
* Validates that the given `password` meets all requirements. When set, the `throwOnFailure` flag
* will trigger an exception to be thrown instead.
Expand All @@ -103,29 +85,17 @@ export function validatePassword(password: string, throwOnFailure?: boolean): bo
return validates;
}

/**
* Props accepted by the <PasswordField> component.
*/
export interface PasswordFieldProps extends TextFieldElementProps {
/**
* Whether the numbers in the password need to add up to a certain sum. This isn't a real
* requirement and will by bypassed by the validation function after being shown once.
*/
requireNumberSum?: boolean;
}

/**
* The <PasswordField> component asks the user to enter their (new) password. It validates the
* password automatically while the user it typing, while continuing to make the result available to
* the react-hook-for-mui parent.
*/
export function PasswordField({ requireNumberSum, ...props }: PasswordFieldProps) {
export function PasswordField(props: TextFieldElementProps) {
const [ password, setPassword ] = useState<string>(/* empty= */ '');
const [ state, setState ] = useState<PasswordValidationState>({
passedCasingRequirement: false,
passedLengthRequirement: false,
passedNumberRequirement: false,
passedNumberSumRequirement: false,
});

function onChange(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) {
Expand All @@ -134,19 +104,12 @@ export function PasswordField({ requireNumberSum, ...props }: PasswordFieldProps
passedCasingRequirement: validatePasswordCasingRequirement(password),
passedLengthRequirement: validatePasswordLengthRequirement(password),
passedNumberRequirement: validatePasswordNumberRequirement(password),
passedNumberSumRequirement: validatePasswordNumberSumRequirement(password),
};

setPassword(password);
setState(updatedState);
}

const failedAnyRequirement =
!state.passedCasingRequirement ||
!state.passedLengthRequirement ||
!state.passedNumberRequirement ||
(!state.passedNumberSumRequirement && requireNumberSum);

return (
<Box>
<TextFieldElement onChange={onChange}
Expand Down Expand Up @@ -176,18 +139,6 @@ export function PasswordField({ requireNumberSum, ...props }: PasswordFieldProps
Contains at least one number
</ListItemText>
</ListItem>
{ requireNumberSum &&
<ListItem disablePadding>
<ListItemIcon sx={{ minWidth: '32px' }}>
{ state.passedNumberSumRequirement &&
<CheckCircleIcon color="success" fontSize="small" /> }
{ !state.passedNumberSumRequirement &&
<CancelIcon color="error" fontSize="small" /> }
</ListItemIcon>
<ListItemText primaryTypographyProps={{ variant: 'body2' }}>
Numbers in the password add up to 25
</ListItemText>
</ListItem> }
<ListItem disablePadding>
<ListItemIcon sx={{ minWidth: '32px' }}>
{ state.passedCasingRequirement &&
Expand Down

0 comments on commit f04954b

Please sign in to comment.