
WCAG 3.3.8 Accessible Authentication (Minimum): Complete Implementation Guide
WCAG 3.3.8 Accessible Authentication (Minimum) is a new Level AA success criterion introduced in WCAG 2.2. It prohibits requiring users to solve cognitive function tests (like CAPTCHA) or remember information (like complex passwords) for authentication, unless specific alternatives are provided.
This is a Level AA requirement, meaning compliance is legally required under ADA, Section 508, and the European Accessibility Act (EAA).
What Does WCAG 3.3.8 Require?
Official Definition:
A cognitive function test (such as remembering a password or solving a puzzle) is not required for any step in an authentication process unless that step provides at least one of the following:
- Alternative: Another authentication method that does not rely on a cognitive function test
- Mechanism: A mechanism is available to assist the user in completing the cognitive function test
- Object Recognition: The cognitive function test is to recognize objects
- Personal Content: The cognitive function test is to identify non-text content the user provided to the website
In Plain English: Don't make users remember complex passwords, solve math problems, transcribe distorted text (CAPTCHA), or complete puzzles to log inβunless you provide easier alternatives like password managers, biometric login, or magic links.
Key Points:
- β Prohibits cognitive function tests without alternatives
- β Password managers must be allowed (autocomplete enabled)
- β Biometric authentication is compliant
- β Email/SMS magic links are compliant
- β Object recognition (selecting images) is allowed
Why This Matters
Real-World Impact:
Problem Scenario
User with cognitive disability tries to log in
β Required to remember 12-character complex password
β Password manager disabled (autocomplete="off")
β Must transcribe distorted CAPTCHA text
β Can't complete login
β Locked out of account
Who This Helps:
- Users with cognitive disabilities: Memory impairments, dyslexia
- Users with intellectual disabilities: Difficulty with complex tasks
- Older adults: Age-related memory decline
- Users with anxiety disorders: Stress impairs memory
- Users with ADHD: Working memory challenges
- Everyone: Password fatigue is universal
Statistics:
- Average person has 100+ online accounts
- 51% of people reuse passwords because they can't remember unique ones
- 30% of users abandon cart after failed CAPTCHA
- Cognitive tests disproportionately affect users with disabilities
Common Failure Patterns
β Failure 1: Password Manager Blocked
<!-- FAILS 3.3.8: Blocks password managers -->
<form action="/login" method="post">
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
autocomplete="off"> <!-- BLOCKS password manager -->
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
autocomplete="off" <!-- BLOCKS password manager -->
onpaste="return false"> <!-- BLOCKS pasting -->
<button type="submit">Log In</button>
</form>
Problem: Users must remember password, no mechanism to assist.
β Failure 2: CAPTCHA Without Alternative
<!-- FAILS 3.3.8: CAPTCHA with no alternative -->
<form action="/login" method="post">
<input type="text" name="username" autocomplete="username">
<input type="password" name="password" autocomplete="current-password">
<!-- Traditional CAPTCHA (cognitive function test) -->
<div class="captcha">
<img src="/captcha/distorted-text.png" alt="">
<label for="captcha-input">Enter the text above:</label>
<input type="text" id="captcha-input" name="captcha" required>
</div>
<button type="submit">Log In</button>
</form>
Problem: Requires transcribing distorted text, no alternative method.
β Failure 3: Security Questions
<!-- FAILS 3.3.8: Requires remembering obscure information -->
<form action="/reset-password" method="post">
<label>What was your first pet's name?</label>
<input type="text" name="answer1" required>
<label>What street did you grow up on?</label>
<input type="text" name="answer2" required>
<label>What was your mother's maiden name?</label>
<input type="text" name="answer3" required>
<button type="submit">Reset Password</button>
</form>
Problem: Requires remembering information without assistance.
β Solution 1: Enable Password Managers
Always allow autocomplete and pasting:
<form action="/login" method="post">
<div class="form-group">
<label for="username">Username or Email</label>
<input
type="text"
id="username"
name="username"
autocomplete="username"
required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
autocomplete="current-password"
required>
</div>
<div class="form-actions">
<button type="submit">Log In</button>
<a href="/forgot-password">Forgot password?</a>
</div>
</form>
Key Points:
- β
autocomplete="username"enables password manager - β
autocomplete="current-password"enables password fill - β
No
autocomplete="off"anywhere - β Pasting allowed
- β No JavaScript preventing autofill
β Solution 2: Passwordless Authentication (Email Magic Link)
Eliminate password requirement entirely:
<form id="magic-link-form" action="/auth/magic-link" method="post">
<h2>Log In</h2>
<p>Enter your email address and we'll send you a login link:</p>
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required>
</div>
<button type="submit">Send Login Link</button>
<div id="success-message" hidden>
<p>β
Check your email for a login link!</p>
<p>The link expires in 15 minutes.</p>
</div>
</form>
<script>
document.getElementById('magic-link-form').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
try {
const response = await fetch('/auth/magic-link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
if (response.ok) {
document.getElementById('success-message').hidden = false;
}
} catch (error) {
alert('Error sending login link. Please try again.');
}
});
</script>
Server-side implementation (Node.js example):
// POST /auth/magic-link
app.post('/auth/magic-link', async (req, res) => {
const { email } = req.body;
// Generate secure token
const token = crypto.randomBytes(32).toString('hex');
// Store token with expiration (15 minutes)
await db.magicLinks.create({
email,
token,
expiresAt: new Date(Date.now() + 15 * 60 * 1000)
});
// Send email with login link
const loginUrl = `${process.env.BASE_URL}/auth/verify?token=${token}`;
await sendEmail({
to: email,
subject: 'Your Login Link',
html: `
<p>Click the link below to log in:</p>
<p><a href="${loginUrl}">Log In to Your Account</a></p>
<p>This link expires in 15 minutes.</p>
<p>If you didn't request this, ignore this email.</p>
`
});
res.json({ success: true });
});
// GET /auth/verify?token=...
app.get('/auth/verify', async (req, res) => {
const { token } = req.query;
const magicLink = await db.magicLinks.findOne({
where: {
token,
expiresAt: { $gt: new Date() },
used: false
}
});
if (!magicLink) {
return res.status(400).send('Invalid or expired link');
}
// Mark as used
await magicLink.update({ used: true });
// Create session
req.session.userId = magicLink.email;
res.redirect('/dashboard');
});
β Solution 3: Biometric Authentication (WebAuthn)
Use passkeys/biometric login:
<div class="login-container">
<h2>Log In</h2>
<!-- Passkey/Biometric option (COMPLIANT) -->
<button
id="passkey-login"
class="btn-primary"
type="button">
π Sign in with Passkey
</button>
<div class="divider">
<span>OR</span>
</div>
<!-- Traditional password (with password manager support) -->
<form id="password-login" action="/login" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="username" required>
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password" required>
<button type="submit">Log In with Password</button>
</form>
</div>
<script>
// WebAuthn passkey login
document.getElementById('passkey-login').addEventListener('click', async () => {
try {
// Request authentication
const credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32), // Get from server
rpId: window.location.hostname,
userVerification: 'preferred'
}
});
// Send credential to server for verification
const response = await fetch('/auth/passkey', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
response: {
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
authenticatorData: arrayBufferToBase64(credential.response.authenticatorData),
signature: arrayBufferToBase64(credential.response.signature)
}
})
});
if (response.ok) {
window.location.href = '/dashboard';
}
} catch (error) {
console.error('Passkey authentication failed:', error);
alert('Passkey login failed. Please try password login.');
}
});
function arrayBufferToBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
</script>
β Solution 4: SMS One-Time Password (OTP)
Send temporary code instead of requiring password:
<form id="phone-auth-form">
<h2>Log In</h2>
<div id="phone-step">
<label for="phone">Phone Number</label>
<input
type="tel"
id="phone"
name="phone"
autocomplete="tel"
placeholder="+1 (555) 123-4567"
required>
<button type="button" id="send-code-btn">Send Code</button>
</div>
<div id="code-step" hidden>
<p>Enter the 6-digit code sent to your phone:</p>
<div class="code-inputs">
<input type="text" maxlength="1" class="code-digit" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" class="code-digit" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" class="code-digit" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" class="code-digit" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" class="code-digit" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" class="code-digit" inputmode="numeric" pattern="[0-9]">
</div>
<button type="button" id="verify-code-btn">Verify Code</button>
<button type="button" id="resend-code-btn" class="btn-link">Resend Code</button>
</div>
</form>
<script>
const phoneStep = document.getElementById('phone-step');
const codeStep = document.getElementById('code-step');
const sendCodeBtn = document.getElementById('send-code-btn');
const verifyCodeBtn = document.getElementById('verify-code-btn');
const codeInputs = document.querySelectorAll('.code-digit');
sendCodeBtn.addEventListener('click', async () => {
const phone = document.getElementById('phone').value;
const response = await fetch('/auth/send-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone })
});
if (response.ok) {
phoneStep.hidden = true;
codeStep.hidden = false;
codeInputs[0].focus();
}
});
// Auto-advance to next input
codeInputs.forEach((input, index) => {
input.addEventListener('input', (e) => {
if (e.target.value && index < codeInputs.length - 1) {
codeInputs[index + 1].focus();
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && index > 0) {
codeInputs[index - 1].focus();
}
});
});
verifyCodeBtn.addEventListener('click', async () => {
const code = Array.from(codeInputs).map(input => input.value).join('');
const response = await fetch('/auth/verify-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: document.getElementById('phone').value,
code
})
});
if (response.ok) {
window.location.href = '/dashboard';
} else {
alert('Invalid code. Please try again.');
}
});
</script>
β Solution 5: Accessible CAPTCHA Alternatives
Use invisible bot detection instead of visual puzzles:
<form action="/login" method="post">
<input type="email" name="email" autocomplete="username">
<input type="password" name="password" autocomplete="current-password">
<!-- Invisible reCAPTCHA (COMPLIANT - no cognitive test) -->
<script src="https://www.google.com/recaptcha/api.js"></script>
<button
class="g-recaptcha"
data-sitekey="your_site_key"
data-callback='onSubmit'
data-action='submit'>
Log In
</button>
</form>
<script>
function onSubmit(token) {
// Token automatically submitted with form
document.querySelector('form').submit();
}
</script>
Alternative: Honeypot field (invisible to humans):
<form action="/login" method="post">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="username">
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password">
<!-- Honeypot field (hidden from humans, visible to bots) -->
<div style="position: absolute; left: -9999px;" aria-hidden="true">
<label for="website">Website (leave blank)</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<button type="submit">Log In</button>
</form>
Server-side honeypot check:
app.post('/login', (req, res) => {
// If honeypot field is filled, it's a bot
if (req.body.website) {
return res.status(400).send('Bot detected');
}
// Continue with normal authentication
authenticateUser(req.body.email, req.body.password);
});
Allowed Cognitive Tests
Object Recognition (COMPLIANT)
<!-- Selecting images is allowed -->
<div class="image-captcha">
<p>Select all images containing cars:</p>
<div class="image-grid">
<button type="button" class="image-option">
<img src="/captcha/car1.jpg" alt="Street with car">
</button>
<button type="button" class="image-option">
<img src="/captcha/tree.jpg" alt="Tree in park">
</button>
<button type="button" class="image-option">
<img src="/captcha/car2.jpg" alt="Car in driveway">
</button>
<!-- etc. -->
</div>
</div>
Why allowed: Object recognition (identifying objects in images) is specifically exempted.
Personal Content (COMPLIANT)
<!-- Identifying user's own photos is allowed -->
<div class="personal-photo-auth">
<p>Select the photo you uploaded when creating your account:</p>
<div class="photo-grid">
<button><img src="/user-uploads/photo1.jpg" alt="Photo option 1"></button>
<button><img src="/user-uploads/photo2.jpg" alt="Photo option 2"></button>
<button><img src="/decoy/photo3.jpg" alt="Photo option 3"></button>
</div>
</div>
Why allowed: Recognizing personal content the user provided is exempted.
Implementation Checklist
Password-Based Login
- Enable autocomplete on username/email field
- Enable autocomplete on password field
- Allow pasting into password field
- Don't block password managers with JavaScript
- Provide "Forgot Password" option
Alternative Authentication Methods
- Implement at least one of:
- Magic link (email)
- Passkey/WebAuthn
- SMS OTP
- Biometric (Touch ID, Face ID)
- OAuth (Sign in with Google/Apple/etc.)
Bot Prevention
- Use invisible CAPTCHA or honeypot
- Don't require transcribing distorted text
- Don't require solving puzzles
- Don't require math problems
Account Recovery
- Don't require security questions without mechanism
- Provide email/SMS recovery option
- Allow account recovery via support team
Testing for 3.3.8 Compliance
Manual Testing
-
Test password manager:
- Try logging in with browser's built-in password manager
- Try third-party password managers (1Password, LastPass)
- Verify autofill works
-
Test alternative methods:
- Attempt login with each alternative method
- Verify they work without cognitive tests
-
Test CAPTCHA:
- If CAPTCHA present, verify it doesn't require transcribing text
- Check for audio or object recognition alternatives
Automated Testing
// Test for 3.3.8 compliance
function test338Compliance() {
const violations = [];
// Check password fields
const passwordFields = document.querySelectorAll('input[type="password"]');
passwordFields.forEach(field => {
// Check autocomplete
const autocomplete = field.getAttribute('autocomplete');
if (autocomplete === 'off' || autocomplete === 'false') {
violations.push({
element: field,
issue: 'Password field blocks autocomplete',
fix: 'Set autocomplete="current-password"'
});
}
// Check for paste blocking
const onpaste = field.getAttribute('onpaste');
if (onpaste && onpaste.includes('false')) {
violations.push({
element: field,
issue: 'Password field blocks pasting',
fix: 'Remove onpaste="return false"'
});
}
});
// Check for text CAPTCHA
const captchaImages = document.querySelectorAll('img[src*="captcha"]');
if (captchaImages.length > 0) {
const hasCaptchaInput = document.querySelector('input[name*="captcha"]');
if (hasCaptchaInput) {
violations.push({
element: hasCaptchaInput,
issue: 'Traditional CAPTCHA requires transcribing text',
fix: 'Use invisible reCAPTCHA or alternative bot detection'
});
}
}
console.log(`\n===== WCAG 3.3.8 Accessible Authentication Test =====`);
if (violations.length > 0) {
console.log(`π΄ Found ${violations.length} violations:`);
console.table(violations);
} else {
console.log(`β
No 3.3.8 violations found`);
}
return violations;
}
// Run test
test338Compliance();
Common Mistakes to Avoid
β Mistake 1: Blocking Autocomplete "For Security"
<!-- WRONG -->
<input type="password" autocomplete="off">
Problem: Makes authentication harder for users with cognitive disabilities.
β Fix: Enable autocomplete - it's secure when implemented properly.
β Mistake 2: Complex Password Requirements Without Manager Support
<!-- WRONG: Complex requirement + blocked manager -->
<input type="password" autocomplete="off">
<p>Password must be 12+ characters with uppercase, lowercase, numbers, and symbols</p>
β Fix: Allow password manager to generate and store complex password.
β Mistake 3: Only Traditional CAPTCHA
<!-- WRONG: No alternative -->
<img src="/captcha.png">
<input type="text" placeholder="Enter text above">
β Fix: Use invisible CAPTCHA or provide alternative authentication method.
How AllAccessible Helps
AllAccessible automatically detects authentication barriers:
Automatic Detection:
- Scans login forms for blocked autocomplete
- Identifies paste-blocking JavaScript
- Detects traditional CAPTCHA without alternatives
- Checks for cognitive function test requirements
Recommended Fixes:
- Suggests enabling autocomplete attributes
- Recommends passwordless alternatives
- Provides WebAuthn/passkey implementation guide
- Identifies accessible CAPTCHA solutions
Start Free Trial - Make authentication accessible - starting at $10/month.
Related Resources
π Master WCAG 2.2: Complete WCAG 2.2 Compliance Guide
Related Success Criteria:
- 3.3.9 Accessible Authentication (Enhanced)
- 3.3.7 Redundant Entry
- 1.1.1 Non-text Content - CAPTCHA alternatives
Implementation Guides:
Summary
WCAG 3.3.8 Accessible Authentication (Minimum) Requirements:
- β Level AA (legally required since EAA June 28, 2025)
- β No cognitive function tests without alternatives
- β Must allow password managers (autocomplete enabled)
- β Provide alternative methods (magic link, biometric, OTP)
- β Exceptions: Object recognition, personal content
Quick Implementation:
<!-- Enable password manager -->
<input type="password" autocomplete="current-password">
<!-- Provide passwordless alternative -->
<button onclick="sendMagicLink()">Email Login Link</button>
<!-- Use invisible CAPTCHA -->
<script src="https://www.google.com/recaptcha/api.js"></script>
Test by: Attempting to log in with password manager and alternative authentication methods.
Need help implementing accessible authentication? AllAccessible automatically detects and fixes these issues - starting at $10/month.