User Registration Implementation on Website
Registration is the first user interaction with the authentication system. Here the user table structure, email verification logic, password policy, and bot protection are established. Most security issues that appear later stem from here.
User Table Structure
Minimal schema covering most scenarios:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255), -- NULL for OAuth registration
name VARCHAR(255),
email_verified_at TIMESTAMP,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
remember_token VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
Field password is nullable because user can register via social provider without password. status takes values pending (email unverified), active, banned, deleted (soft delete).
Password Hashing
bcrypt with cost factor 12 is the current standard. In PHP: password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]). In Node.js: bcrypt.hash(password, 12). Argon2id is preferable for security, but bcrypt is sufficient and universally supported.
Never store plaintext passwords, don't log form data, don't pass passwords in URL parameters. This is obvious but violated regularly.
Backend Validation
Frontend validation — for UX. Backend validation — for security. Both are mandatory, they don't replace each other.
// Laravel FormRequest
class RegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns', 'max:255', 'unique:users,email'],
'password' => ['required', 'min:8', 'max:72', 'confirmed', Password::defaults()],
'name' => ['required', 'string', 'max:255'],
];
}
}
email:rfc,dns checks format by RFC and MX record existence. Filters out non-existent domains before sending email. max:72 for password is bcrypt limitation (truncates strings longer than 72 bytes).
For password policy in Laravel: Password::min(8)->letters()->mixedCase()->numbers(). Don't overdo requirements — NIST SP 800-63B recommends length over complexity.
Email Verification
Without verification, anyone can register with someone else's email, receive notifications to wrong inbox, pollute database with garbage. Verification is mandatory everywhere email is used as identifier.
Verification token is a signed link with TTL:
// Generate link
$verifyUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addHours(24),
['id' => $user->id, 'hash' => sha1($user->email)]
);
// Handle verification
public function verify(Request $request): RedirectResponse
{
if (! hash_equals(sha1($request->user()->email), $request->hash)) {
abort(403);
}
$request->user()->markEmailAsVerified();
return redirect('/dashboard')->with('status', 'email-verified');
}
Temporary signed link is better than storing token in DB — no separate table needed, link is self-contained and expires automatically.
Bot Protection and Abuse
Rate limiting: max 5 registration attempts per IP per 10 minutes. In Laravel:
RateLimiter::for('register', function (Request $request) {
return Limit::perMinutes(10, 5)->by($request->ip());
});
Honeypot: hidden form field that bots fill, humans don't:
<input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off">
On backend: if website is not empty — silently reject.
CAPTCHA: reCAPTCHA v3 (score-based, no user interaction) or hCaptcha. Enable on anomalous activity, not by default — CAPTCHA reduces conversion.
Social Registration
OAuth registration via Google, GitHub, VK — users prefer it, no password to invent. Processing logic:
public function handleOAuthCallback(string $provider): RedirectResponse
{
$socialUser = Socialite::driver($provider)->user();
$user = User::where('email', $socialUser->getEmail())->first();
if ($user) {
// Already exists — link provider
$user->oauthProviders()->updateOrCreate(
['provider' => $provider],
['provider_id' => $socialUser->getId()]
);
} else {
// New user
$user = User::create([
'email' => $socialUser->getEmail(),
'name' => $socialUser->getName(),
'email_verified_at' => now(), // email already verified by OAuth provider
'status' => 'active',
]);
}
Auth::login($user);
return redirect('/dashboard');
}
Important: if OAuth email matches existing account with password — don't create duplicate, link provider to existing account.
Post-Registration Flow
After successful registration, typical scenario:
- Send welcome email with verification link
- Create user initial data (profile, default settings)
- Redirect to dashboard or "check email" page
- Optionally: onboarding wizard on first login
Email sent via queue, not synchronously — otherwise SMTP delay blocks user response.
Typical time for basic registration with email verification: 1–2 working days. With OAuth providers — add 1 day per provider.







