Skip to Content
PatternsAuthentication

Authentication

Authentication patterns for the Mukoko ecosystem. These patterns ensure consistent, secure, and accessible auth flows across all apps.

Sign in page

The standard sign-in layout uses a centered card on a muted background:

<div className="flex min-h-screen items-center justify-center bg-muted px-4"> <Card className="w-full max-w-sm"> <CardHeader className="text-center"> <MukokoLogo size={32} /> <CardTitle className="mt-4">Sign in to mukoko</CardTitle> </CardHeader> <CardContent> <form className="space-y-4"> <Field> <FieldLabel>Email address</FieldLabel> <Input type="email" autoComplete="email" /> </Field> <Field> <FieldLabel>Password</FieldLabel> <Input type="password" autoComplete="current-password" /> </Field> <Button className="w-full">Sign in</Button> </form> <p className="mt-4 text-center text-sm text-muted-foreground"> Do not have an account?{" "} <a href="/signup" className="text-foreground underline underline-offset-2"> Create one </a> </p> </CardContent> </Card> </div>

Sign up page

Sign up follows the same card layout with additional fields:

<form className="space-y-4"> <Field> <FieldLabel>Full name</FieldLabel> <Input type="text" autoComplete="name" /> </Field> <Field> <FieldLabel>Email address</FieldLabel> <Input type="email" autoComplete="email" /> </Field> <Field> <FieldLabel>Password</FieldLabel> <Input type="password" autoComplete="new-password" /> <FieldDescription>At least 8 characters</FieldDescription> </Field> <Button className="w-full">Create account</Button> </form>

Name field

Use a single “Full name” field. Do not split into first/last name — African naming conventions are diverse and do not always fit the first/last pattern. See Inclusive Language for details.

Form validation

Use react-hook-form with zod for client-side validation:

const signInSchema = z.object({ email: z.string().email("Enter a valid email address"), password: z.string().min(1, "Password is required"), }) const signUpSchema = z.object({ name: z.string().min(1, "Name is required"), email: z.string().email("Enter a valid email address"), password: z.string().min(8, "Password must be at least 8 characters"), })

Show validation errors inline using the FormMessage component.

Error handling

Authentication errors need specific, helpful messages:

SituationMessage
Wrong credentials”Incorrect email or password. Try again or reset your password.”
Account locked”Too many attempts. Your account is locked for 15 minutes.”
Email not verified”Check your email for a verification link.”
Network error”Could not connect. Check your internet and try again.”

Never reveal whether an email exists in the system. Always use generic messages like “Incorrect email or password” rather than “No account with this email.”

Accessibility

  • All inputs must have associated labels (<FieldLabel>)
  • Use autoComplete attributes for password managers
  • The form must be navigable with keyboard only
  • Error messages must be announced to screen readers (use aria-live)
  • The submit button must indicate loading state during form submission
<Button className="w-full" disabled={isSubmitting}> {isSubmitting ? ( <> <Spinner className="size-4" /> Signing in... </> ) : ( "Sign in" )} </Button>

Session management

  • Store session tokens in HTTP-only cookies, not localStorage
  • Implement token refresh to avoid frequent re-authentication
  • Provide a clear sign-out action in the user menu
  • Show a confirmation when signing out of all devices

Password reset flow

  1. User clicks “Forgot password?” on the sign-in page
  2. User enters their email address
  3. System sends a reset email (do not confirm whether the email exists)
  4. User clicks the link and enters a new password
  5. User is redirected to sign in with the new password

Each step uses the same centered card layout for visual consistency.

Last updated on