
WCAG 3.3.9 Accessible Authentication (Enhanced): Complete Implementation Guide
WCAG 3.3.9 Accessible Authentication (Enhanced) is a new Level AAA success criterion introduced in WCAG 2.2. It's the strictest version of accessible authentication, prohibiting ALL cognitive function tests—even object recognition CAPTCHAs.
This is a Level AAA requirement, meaning it's optional but represents the gold standard for accessible authentication.
What Does WCAG 3.3.9 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
Key Difference from 3.3.8 (Level AA):
- 3.3.8 (AA): Allows object recognition (identifying images) and personal content
- 3.3.9 (AAA): Prohibits ALL cognitive tests, including object recognition
In Plain English: You cannot require users to remember passwords, solve puzzles, transcribe text, OR identify objects in images—unless you provide an easier alternative that requires zero cognitive effort.
What's Prohibited:
- ❌ Remembering passwords (unless password manager available)
- ❌ CAPTCHA text transcription
- ❌ CAPTCHA object recognition ("Select all cars")
- ❌ Math problems
- ❌ Security questions
- ❌ Identifying personal photos (even user-provided ones)
What's Allowed:
- ✅ Biometric authentication (fingerprint, Face ID)
- ✅ Hardware tokens (YubiKey)
- ✅ Magic links via email/SMS
- ✅ Passkeys (WebAuthn)
- ✅ OAuth (Sign in with Google/Apple)
Why This Matters
Beyond Level AA:
Even object recognition CAPTCHAs (allowed at AA level) present barriers:
User with cognitive disability encounters image CAPTCHA
→ Must identify all images with traffic lights
→ Some images ambiguous (is that a stoplight or street light?)
→ User makes mistakes, must retry multiple times
→ Frustration, possible lockout
Who Benefits from AAA:
- Users with intellectual disabilities: Struggle with any cognitive tests
- Users with severe memory impairments: Cannot recall anything
- Users with dyslexia: Image interpretation can be challenging
- Non-native speakers: May not recognize cultural objects
- Everyone in crisis: When you need urgent access, even simple tests are barriers
Difference from Level AA (3.3.8)
Object Recognition: AA vs AAA
Scenario: Image CAPTCHA showing various objects
Level AA (3.3.8):
<!-- ALLOWED at AA: Object recognition CAPTCHA -->
<div class="captcha">
<p>Select all images containing cars:</p>
<div class="image-grid">
<button><img src="car1.jpg" alt="Street with car"></button>
<button><img src="tree.jpg" alt="Tree in park"></button>
<button><img src="car2.jpg" alt="Car in driveway"></button>
</div>
</div>
✅ AA Compliant (object recognition is exempted)
Level AAA (3.3.9): ❌ AAA Non-Compliant (object recognition is prohibited)
Personal Content: AA vs AAA
Scenario: Identifying user-uploaded photos
Level AA (3.3.8):
<!-- ALLOWED at AA: Personal content recognition -->
<div>
<p>Select the photo you uploaded:</p>
<button><img src="user-photo.jpg" alt="Your vacation photo"></button>
<button><img src="decoy1.jpg" alt="Decoy photo 1"></button>
</div>
✅ AA Compliant (personal content is exempted)
Level AAA (3.3.9): ❌ AAA Non-Compliant (still requires cognitive recognition)
✅ AAA-Compliant Authentication Methods
1. Biometric Authentication (WebAuthn/Passkeys)
Zero cognitive load:
<div class="auth-options">
<h2>Log In</h2>
<!-- PASSKEY: AAA COMPLIANT -->
<button
id="passkey-signin"
class="btn-primary">
<svg aria-hidden="true"><!-- fingerprint icon --></svg>
Sign in with Passkey
</button>
<p class="help-text">
Use your fingerprint, face, or device PIN
</p>
</div>
<script>
// WebAuthn passkey authentication
document.getElementById('passkey-signin').addEventListener('click', async () => {
try {
// Get challenge from server
const challenge = await fetch('/auth/challenge').then(r => r.json());
// Authenticate with biometric
const credential = await navigator.credentials.get({
publicKey: {
challenge: Uint8Array.from(challenge.challenge, c => c.charCodeAt(0)),
rpId: window.location.hostname,
userVerification: 'required', // Biometric required
timeout: 60000
}
});
// Send to server
const response = await fetch('/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: credential.id,
rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
response: {
authenticatorData: btoa(String.fromCharCode(...new Uint8Array(credential.response.authenticatorData))),
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),
signature: btoa(String.fromCharCode(...new Uint8Array(credential.response.signature)))
}
})
});
if (response.ok) {
window.location.href = '/dashboard';
}
} catch (error) {
console.error('Passkey auth failed:', error);
showError('Authentication failed. Please try again.');
}
});
</script>
Why AAA Compliant: No memory, no puzzles, no recognition—just biometric scan.
2. Email Magic Link (Zero Cognitive Load)
<form id="magic-link-form">
<h2>Log In</h2>
<p>Enter your email address:</p>
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
autofocus>
</div>
<button type="submit" class="btn-primary">
Send Login Link
</button>
<div id="success" hidden>
<h3>✅ Check your email!</h3>
<p>We've sent a login link to <strong id="sent-to"></strong></p>
<p>Click the link to log in. It 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;
const response = await fetch('/auth/magic-link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
if (response.ok) {
document.getElementById('sent-to').textContent = email;
document.getElementById('success').hidden = false;
e.target.hidden = true;
}
});
</script>
Server implementation:
// Node.js/Express example
app.post('/auth/magic-link', async (req, res) => {
const { email } = req.body;
// Generate cryptographically secure token
const token = crypto.randomBytes(32).toString('hex');
// Store with expiration
await db.tokens.create({
email,
token,
type: 'magic_link',
expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 minutes
});
// Send email
await sendEmail({
to: email,
subject: 'Your Login Link',
html: `
<h2>Log In to Your Account</h2>
<p>Click the button below to log in:</p>
<p>
<a href="${process.env.BASE_URL}/auth/verify/${token}"
style="display: inline-block; padding: 12px 24px; background: #0066cc; color: white; text-decoration: none; border-radius: 4px;">
Log In
</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 });
});
// Verify magic link
app.get('/auth/verify/:token', async (req, res) => {
const { token } = req.params;
const record = await db.tokens.findOne({
where: {
token,
type: 'magic_link',
used: false,
expiresAt: { $gt: new Date() }
}
});
if (!record) {
return res.status(400).send('Invalid or expired link');
}
// Mark as used
await record.update({ used: true });
// Create session
req.session.userEmail = record.email;
req.session.authenticatedAt = new Date();
res.redirect('/dashboard');
});
Why AAA Compliant: User only needs to access their email—no memory, no recognition.
3. SMS One-Time Code (Low Cognitive Load)
<div id="auth-flow">
<!-- Step 1: Enter phone number -->
<div id="phone-step">
<h2>Log In</h2>
<label for="phone">Phone Number</label>
<input
type="tel"
id="phone"
autocomplete="tel"
placeholder="+1 (555) 123-4567"
required>
<button id="send-code">Send Code</button>
</div>
<!-- Step 2: Enter code (automatically sent) -->
<div id="code-step" hidden>
<h2>Enter Code</h2>
<p>We sent a code to <strong id="phone-display"></strong></p>
<div class="otp-inputs">
<!-- 6-digit code, auto-advance -->
<input type="text" maxlength="1" class="otp-digit" autocomplete="one-time-code" pattern="[0-9]" inputmode="numeric">
<input type="text" maxlength="1" class="otp-digit" pattern="[0-9]" inputmode="numeric">
<input type="text" maxlength="1" class="otp-digit" pattern="[0-9]" inputmode="numeric">
<input type="text" maxlength="1" class="otp-digit" pattern="[0-9]" inputmode="numeric">
<input type="text" maxlength="1" class="otp-digit" pattern="[0-9]" inputmode="numeric">
<input type="text" maxlength="1" class="otp-digit" pattern="[0-9]" inputmode="numeric">
</div>
<p><button id="resend-code" class="btn-link">Resend Code</button></p>
</div>
</div>
<style>
.otp-inputs {
display: flex;
gap: 8px;
justify-content: center;
}
.otp-digit {
width: 48px;
height: 56px;
font-size: 24px;
text-align: center;
border: 2px solid #ccc;
border-radius: 8px;
}
.otp-digit:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
border-color: #0066cc;
}
</style>
<script>
const phoneStep = document.getElementById('phone-step');
const codeStep = document.getElementById('code-step');
const sendCodeBtn = document.getElementById('send-code');
const otpInputs = document.querySelectorAll('.otp-digit');
// Send code
sendCodeBtn.addEventListener('click', async () => {
const phone = document.getElementById('phone').value;
const response = await fetch('/auth/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone })
});
if (response.ok) {
document.getElementById('phone-display').textContent = phone;
phoneStep.hidden = true;
codeStep.hidden = false;
otpInputs[0].focus();
}
});
// Auto-advance inputs
otpInputs.forEach((input, index) => {
input.addEventListener('input', (e) => {
if (e.target.value && index < otpInputs.length - 1) {
otpInputs[index + 1].focus();
}
// Auto-submit when all digits entered
if (index === otpInputs.length - 1 && e.target.value) {
verifyCode();
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && index > 0) {
otpInputs[index - 1].focus();
}
});
});
async function verifyCode() {
const code = Array.from(otpInputs).map(i => i.value).join('');
const response = await fetch('/auth/verify-otp', {
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.');
otpInputs.forEach(i => i.value = '');
otpInputs[0].focus();
}
}
</script>
Why AAA Compliant: Code is sent automatically, user just copies it (minimal cognitive load).
4. OAuth (Sign in with...)
<div class="auth-options">
<h2>Log In</h2>
<!-- OAuth providers (AAA COMPLIANT) -->
<button onclick="signInWithGoogle()" class="btn-oauth btn-google">
<img src="/icons/google.svg" alt="" aria-hidden="true">
Sign in with Google
</button>
<button onclick="signInWithApple()" class="btn-oauth btn-apple">
<img src="/icons/apple.svg" alt="" aria-hidden="true">
Sign in with Apple
</button>
<button onclick="signInWithMicrosoft()" class="btn-oauth btn-microsoft">
<img src="/icons/microsoft.svg" alt="" aria-hidden="true">
Sign in with Microsoft
</button>
</div>
<script>
function signInWithGoogle() {
window.location.href = '/auth/google';
}
function signInWithApple() {
window.location.href = '/auth/apple';
}
function signInWithMicrosoft() {
window.location.href = '/auth/microsoft';
}
</script>
Server implementation (using Passport.js):
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
},
(accessToken, refreshToken, profile, done) => {
// Find or create user
User.findOrCreate({ googleId: profile.id }, (err, user) => {
return done(err, user);
});
}
));
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
Why AAA Compliant: User already logged into Google/Apple/Microsoft—no additional cognitive test.
Implementation Checklist
Remove ALL Cognitive Tests
- No password requirements (unless mechanism provided)
- No CAPTCHA of any kind (text OR image)
- No security questions
- No math problems
- No object recognition
- No personal content identification
Provide AAA-Compliant Alternatives
Choose at least one:
- Biometric authentication (passkeys, WebAuthn)
- Magic link via email
- One-time code via SMS
- OAuth (Sign in with Google/Apple/Microsoft)
- Hardware security key (YubiKey)
Enable Password Managers (if passwords used)
-
autocomplete="current-password"on password fields -
autocomplete="username"on email/username fields - Allow pasting
- No JavaScript blocking autofill
Testing for 3.3.9 Compliance
Manual Testing
-
Attempt to log in without any cognitive effort:
- Can you log in using only biometric?
- Can you log in by clicking email link?
- Can you log in with OAuth?
-
Check for cognitive tests:
- Any CAPTCHA (including image-based)?
- Any password requirements?
- Any security questions?
-
Verify password manager support:
- Does browser autofill work?
- Can you paste passwords?
- No JavaScript blocking?
Automated Testing
// Test for AAA authentication compliance
function test339Compliance() {
const violations = [];
// Check for CAPTCHA (any type)
const captchas = document.querySelectorAll([
'img[src*="captcha"]',
'iframe[src*="recaptcha"]',
'iframe[src*="hcaptcha"]',
'[class*="captcha"]',
'[id*="captcha"]'
].join(','));
if (captchas.length > 0) {
violations.push({
issue: 'CAPTCHA found (prohibited at AAA)',
count: captchas.length,
fix: 'Use passkey, magic link, or OAuth instead'
});
}
// Check for password field without autocomplete
const passwordFields = document.querySelectorAll('input[type="password"]');
passwordFields.forEach(field => {
const autocomplete = field.getAttribute('autocomplete');
if (autocomplete === 'off' || !autocomplete.includes('password')) {
violations.push({
issue: 'Password field blocks autocomplete',
element: field,
fix: 'Set autocomplete="current-password"'
});
}
});
// Check for security questions
const securityQuestions = document.querySelectorAll([
'input[name*="security"]',
'input[name*="question"]',
'label:contains("mother")',
'label:contains("pet")',
'label:contains("street")'
].join(','));
if (securityQuestions.length > 0) {
violations.push({
issue: 'Security questions found (prohibited at AAA)',
count: securityQuestions.length,
fix: 'Replace with magic link or OTP'
});
}
console.log(`\n===== WCAG 3.3.9 AAA Authentication Test =====`);
if (violations.length > 0) {
console.log(`🔴 Found ${violations.length} AAA violations:`);
console.table(violations);
} else {
console.log(`✅ Authentication meets 3.3.9 AAA standards`);
}
return violations;
}
// Run test
test339Compliance();
When to Implement AAA (3.3.9)
Highly Recommended For:
✅ Government services (serving all citizens) ✅ Healthcare portals (patients with diverse abilities) ✅ Educational platforms (students with disabilities) ✅ Banking/financial services (elderly users, accessibility requirements) ✅ Sites specifically serving users with disabilities
Consider Carefully For:
⚠️ High-security applications (balance security vs. accessibility) ⚠️ Legacy systems (may require significant refactoring) ⚠️ Limited budget (AAA can be more expensive to implement)
How AllAccessible Helps
AllAccessible detects authentication barriers and suggests AAA-compliant alternatives:
Detection:
- Identifies all CAPTCHAs (including image-based)
- Detects blocked password managers
- Finds security questions
- Checks for cognitive tests
Recommendations:
- Suggests passkey implementation
- Provides magic link code samples
- Recommends OAuth providers
- Offers compliance roadmap
Start Free Trial - Achieve AAA authentication - starting at $10/month.
Related Resources
📖 Master WCAG 2.2: Complete WCAG 2.2 Compliance Guide
Related Success Criteria:
- 3.3.8 Accessible Authentication (Minimum) - Level AA version
- 3.3.7 Redundant Entry - Form efficiency
- 1.1.1 Non-text Content - CAPTCHA alternatives
Implementation Guides:
- WebAuthn/Passkey Complete Guide
- Magic Link Authentication Best Practices
- OAuth Integration for Accessibility
Summary
WCAG 3.3.9 Accessible Authentication (Enhanced) Requirements:
- ✅ Level AAA (optional gold standard)
- ✅ Prohibits ALL cognitive function tests
- ✅ Stricter than 3.3.8 AA (which allows object recognition)
- ✅ Must provide zero-cognitive-load alternatives
AAA-Compliant Methods:
- Biometric authentication (passkeys, WebAuthn)
- Magic links (email/SMS)
- OAuth (Sign in with Google/Apple/Microsoft)
- Hardware tokens
- NOT compliant: Any CAPTCHA, security questions, passwords without manager support
Quick Implementation:
<!-- Passkey (AAA compliant) -->
<button onclick="authenticateWithPasskey()">
Sign in with Passkey
</button>
<!-- Magic link (AAA compliant) -->
<button onclick="sendMagicLink()">
Email Login Link
</button>
<!-- OAuth (AAA compliant) -->
<button onclick="signInWithGoogle()">
Sign in with Google
</button>
Test by: Attempting to authenticate without any cognitive tests (no memory, no recognition, no puzzles).
Ready for gold-standard accessible authentication? AllAccessible guides AAA implementation - starting at $10/month.