Skip to main content

Captcha

Endpoints for generating and verifying captcha challenges.

GET /api/captcha

Generate a captcha image.

Authentication: Not required

Query Parameters:

ParameterTypeDefaultDescription
themestringlightTheme: light or dark

Response:

SVG image (Content-Type: image/svg+xml)

Cookie Set:

Sets an encrypted captcha cookie containing the expected answer.

Example Request:

curl https://app.example.com/api/captcha?theme=dark

Response:

<svg xmlns="http://www.w3.org/2000/svg" width="160" height="55">
<!-- SVG captcha image -->
</svg>

Captcha Configuration:

  • Size: 4 characters
  • Ignored Characters: 0oO1iIlLqQgG9S5sZz2 (to avoid confusion)
  • Noise: Level 1
  • Color: Yes (colored characters)
  • Background:
    • Light theme: #fff
    • Dark theme: #202125
  • Font Size: 45px
  • Dimensions: 160x55 pixels

POST /api/captcha/_verify

Verify a captcha response.

Authentication: Not required

Request Body:

FieldTypeRequiredDescription
responsestringYesUser's captcha answer

Example Request:

{
"response": "ABC7"
}

Response (Success):

{
"valid": true
}

Response (Failure):

{
"valid": false
}

Validation:

  • Compares user input against decrypted cookie value
  • Case-insensitive comparison
  • Clears the captcha cookie after verification

Captcha Flow

1. Request Captcha Image

GET /api/captcha?theme=light

Response: SVG image + encrypted cookie

2. Display Image to User

Render the SVG image in the UI and provide an input field.

3. Submit User Response

POST /api/captcha/_verify
{
"response": "ABC7"
}

Response:

{
"valid": true
}

4. Handle Result

  • If valid: true, proceed with protected action (e.g., login, registration)
  • If valid: false, request a new captcha and ask user to try again

Integration Examples

Login Form with Captcha

// 1. Load captcha on page load
async function loadCaptcha() {
const response = await fetch('/api/captcha?theme=dark');
const svg = await response.text();
document.getElementById('captcha-image').innerHTML = svg;
}

// 2. Verify captcha before login
async function login(username, password, captchaResponse) {
// First verify captcha
const verification = await fetch('/api/captcha/_verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: captchaResponse })
}).then(r => r.json());

if (!verification.valid) {
alert('Invalid captcha');
loadCaptcha(); // Reload captcha
return;
}

// Proceed with login
const loginResponse = await fetch('/api/login/local', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});

// Handle login response...
}

Registration Form

async function register(userData, captchaResponse) {
// Verify captcha first
const { valid } = await fetch('/api/captcha/_verify', {
method: 'POST',
body: JSON.stringify({ response: captchaResponse })
}).then(r => r.json());

if (!valid) {
throw new Error('Invalid captcha');
}

// Proceed with registration...
}

Use Cases

Prevent Automated Attacks

  • Brute Force Login - Rate limit login attempts with captcha
  • Account Enumeration - Prevent automated user discovery
  • Spam Registration - Block automated account creation

Security Triggers

Captcha can be selectively required:

  • After N failed login attempts
  • For registration from untrusted IPs
  • For password reset requests
  • When unusual activity is detected

Security Considerations

The captcha answer is encrypted before being stored in the cookie to prevent:

  • Client-side tampering
  • Cookie inspection revealing the answer

Single-Use

Captcha verification consumes the cookie. Each captcha image can only be verified once.

Case Insensitivity

Verification is case-insensitive to improve usability while maintaining security.

Accessibility

Consider providing:

  • Audio captcha alternative
  • Option to refresh captcha
  • Clear error messages
Best Practice

Only require captcha when necessary (e.g., after failed attempts) to maintain good UX while preventing abuse.

Customization

The character set excludes commonly confused characters (0oO1iIlLqQgG9S5sZz2) to reduce user frustration.