Skip to main content
Back to Blog
WCAG 2.2

WCAG 3.3.9 Accessible Authentication (Enhanced): Complete Implementation Guide

Master WCAG 3.3.9 Accessible Authentication (Enhanced) - Level AAA criterion. Learn how to eliminate all cognitive function tests from authentication, including object recognition.

AllAccessible
11 min read
WCAG 2.2AuthenticationSecurityLevel AAALogin
WCAG 3.3.9 Accessible Authentication (Enhanced): Complete Implementation Guide

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

  1. 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?
  2. Check for cognitive tests:

    • Any CAPTCHA (including image-based)?
    • Any password requirements?
    • Any security questions?
  3. 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:

Implementation Guides:

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.

Share this article