Skip to main content
POST
/
recover
curl -X POST "http://localhost:8080/recover" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com"
  }'
{
  "message": "If an account with that email exists, we've sent a password recovery link."
}
Send a password recovery email to users who have forgotten their password. This endpoint initiates the password reset flow by sending a secure recovery link via email.
This endpoint always returns success (200) to prevent email enumeration attacks, even if the email doesn’t exist.
curl -X POST "http://localhost:8080/recover" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com"
  }'

Request Body

email
string
required
Email address of the user requesting password recovery.
redirect_to
string
URL to redirect to after password recovery verification. If not provided, uses the default redirect URL.
captcha_token
string
Captcha token for verification if captcha is enabled.

Response

message
string
Success message (always returned regardless of email existence)
{
  "message": "If an account with that email exists, we've sent a password recovery link."
}

Error Responses

{
  "code": 400,
  "msg": "Invalid email format",
  "details": "Please provide a valid email address"
}

Password Recovery Flow

1

Request Recovery

User submits their email address to the recovery endpoint
2

Email Sent

If the email exists, a recovery email is sent with a secure token
3

User Clicks Link

User clicks the recovery link in their email
4

Verification

The recovery token is verified and user is redirected to reset form
5

Password Reset

User sets a new password using the verify endpoint

Recovery Email Template

The recovery email should include:
<!DOCTYPE html>
<html>
<head>
  <title>Reset Your Password - Strike</title>
</head>
<body>
  <div style="max-width: 600px; margin: 0 auto; padding: 20px;">
    <h1>Reset Your Password</h1>
    
    <p>Hi there,</p>
    
    <p>We received a request to reset your password for your Strike account.</p>
    
    <a href="{{.RecoveryURL}}" 
       style="background-color: #0D9373; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
      Reset Password
    </a>
    
    <p>Or copy and paste this link into your browser:</p>
    <p>{{.RecoveryURL}}</p>
    
    <p>This link will expire in 1 hour for security reasons.</p>
    
    <p>If you didn't request this password reset, you can safely ignore this email.</p>
    
    <p>Best regards,<br>The Strike Team</p>
  </div>
</body>
</html>

Implementation Examples

React Password Recovery Form

import { useState } from 'react';

function PasswordRecoveryForm() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const [sent, setSent] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const response = await fetch('/api/auth/recover', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Recovery failed');
      }

      setSent(true);
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  if (sent) {
    return (
      <div className="recovery-success">
        <h2>Check your email</h2>
        <p>
          If an account with <strong>{email}</strong> exists, 
          we've sent a password recovery link.
        </p>
        <p>
          Please check your email and click the link to reset your password.
        </p>
        <p>
          <small>
            Didn't receive the email? Check your spam folder or{' '}
            <button 
              onClick={() => setSent(false)}
              className="link-button"
            >
              try again
            </button>
          </small>
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="recovery-form">
      <h2>Reset Password</h2>
      <p>
        Enter your email address and we'll send you a link to reset your password.
      </p>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      <div className="form-group">
        <label htmlFor="email">Email Address</label>
        <input
          type="email"
          id="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter your email"
          required
        />
      </div>

      <button type="submit" disabled={loading || !email}>
        {loading ? 'Sending...' : 'Send Recovery Email'}
      </button>

      <p>
        Remember your password? <a href="/login">Sign in</a>
      </p>
    </form>
  );
}

export default PasswordRecoveryForm;

Password Reset Form (After Email Click)

import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';

function PasswordResetForm() {
  const [searchParams] = useSearchParams();
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);

  const token = searchParams.get('token');
  const type = searchParams.get('type');

  useEffect(() => {
    if (!token || type !== 'recovery') {
      setError('Invalid or missing recovery token');
    }
  }, [token, type]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (password !== confirmPassword) {
      setError('Passwords do not match');
      return;
    }

    if (password.length < 8) {
      setError('Password must be at least 8 characters');
      return;
    }

    setLoading(true);
    setError('');

    try {
      const response = await fetch('/api/auth/verify', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          type: 'recovery',
          token: token,
          password: password
        }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Password reset failed');
      }

      const authData = await response.json();
      
      // Store tokens and redirect
      localStorage.setItem('access_token', authData.access_token);
      localStorage.setItem('refresh_token', authData.refresh_token);
      
      setSuccess(true);
      
      // Redirect to dashboard after a short delay
      setTimeout(() => {
        window.location.href = '/dashboard';
      }, 2000);
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  if (success) {
    return (
      <div className="reset-success">
        <h2>Password Reset Successful!</h2>
        <p>Your password has been updated. Redirecting to dashboard...</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="reset-form">
      <h2>Set New Password</h2>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      <div className="form-group">
        <label htmlFor="password">New Password</label>
        <input
          type="password"
          id="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Enter new password"
          minLength={8}
          required
        />
        <small>
          Password must be at least 8 characters with uppercase, lowercase, number, and special character.
        </small>
      </div>

      <div className="form-group">
        <label htmlFor="confirmPassword">Confirm Password</label>
        <input
          type="password"
          id="confirmPassword"
          value={confirmPassword}
          onChange={(e) => setConfirmPassword(e.target.value)}
          placeholder="Confirm new password"
          required
        />
      </div>

      <button type="submit" disabled={loading || !password || !confirmPassword}>
        {loading ? 'Updating...' : 'Update Password'}
      </button>
    </form>
  );
}

export default PasswordResetForm;

Node.js Backend Handler

const express = require('express');
const { body, validationResult } = require('express-validator');

const router = express.Router();

// Password recovery request
router.post('/recover', [
  body('email').isEmail().normalizeEmail()
], async (req, res) => {
  try {
    // Check validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        msg: 'Invalid email format',
        details: 'Please provide a valid email address'
      });
    }

    // Call Strike Auth Service
    const response = await fetch(`${process.env.AUTH_SERVICE_URL}/recover`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(req.body),
    });

    // Always return success to prevent email enumeration
    res.json({
      message: "If an account with that email exists, we've sent a password recovery link."
    });

    // Log recovery attempt (but don't reveal if email exists)
    console.log(`Password recovery requested for: ${req.body.email}`);

  } catch (error) {
    console.error('Recovery error:', error);
    
    // Still return success to prevent information disclosure
    res.json({
      message: "If an account with that email exists, we've sent a password recovery link."
    });
  }
});

module.exports = router;

Security Features

  • Email Enumeration Protection: Always returns success regardless of email existence
  • Token Expiration: Recovery tokens expire after 1 hour
  • Single Use: Recovery tokens can only be used once
  • Rate Limiting: Prevents abuse and spam
  • Secure Tokens: Cryptographically secure random tokens

Rate Limiting

This endpoint is rate limited to prevent abuse:
Limit TypeLimitWindow
Per Email3 requests15 minutes
Per IP10 requests15 minutes

Best Practices

  • Always return success to prevent email enumeration
  • Use secure, random tokens with sufficient entropy
  • Implement proper token expiration (1 hour recommended)
  • Log recovery attempts for security monitoring
  • Require strong passwords for reset
  • Provide clear instructions in recovery emails
  • Include troubleshooting tips for common issues
  • Offer alternative recovery methods if available
  • Show helpful error messages without revealing sensitive info
  • Use reputable email service providers
  • Implement proper email authentication (SPF, DKIM, DMARC)
  • Monitor delivery rates and bounce rates
  • Provide clear sender identification

Testing

Unit Tests

describe('POST /recover', () => {
  test('should always return success for valid email format', async () => {
    const response = await request(app)
      .post('/recover')
      .send({
        email: 'test@example.com'
      })
      .expect(200);

    expect(response.body.message).toContain('sent a password recovery link');
  });

  test('should return success even for non-existent email', async () => {
    const response = await request(app)
      .post('/recover')
      .send({
        email: 'nonexistent@example.com'
      })
      .expect(200);

    expect(response.body.message).toContain('sent a password recovery link');
  });

  test('should reject invalid email format', async () => {
    await request(app)
      .post('/recover')
      .send({
        email: 'invalid-email'
      })
      .expect(400);
  });

  test('should respect rate limits', async () => {
    const email = 'test@example.com';
    
    // Send multiple requests
    for (let i = 0; i < 5; i++) {
      await request(app)
        .post('/recover')
        .send({ email });
    }

    // Should be rate limited
    await request(app)
      .post('/recover')
      .send({ email })
      .expect(429);
  });
});
I