Best practices and security hardening for BetterAuth.
BetterAuth includes:
| Feature | Paseto V4 | JWT |
|---|---|---|
| Algorithm agility | ❌ Fixed (secure) | ⚠️ Vulnerable to confusion |
| "none" algorithm | ❌ Not possible | ⚠️ Possible vulnerability |
| Encryption | ✅ Built-in | ❌ Separate (JWE) |
| Key confusion | ❌ Not possible | ⚠️ RS/HS confusion attacks |
better_auth:
token:
# Short access tokens (15min - 1h)
lifetime: 3600
# Longer refresh tokens (7-30 days)
refresh_lifetime: 2592000
BetterAuth uses one-time-use refresh tokens:
BetterAuth uses Argon2id by default:
# config/packages/security.yaml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: argon2id
memory_cost: 65536 # 64MB
time_cost: 4
threads: 1
Recommended minimum:
use Symfony\Component\Validator\Constraints as Assert;
#[Assert\Length(min: 8, minMessage: 'Password must be at least 8 characters')]
#[Assert\NotCompromisedPassword(message: 'This password has been leaked')]
private string $password;
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
login:
policy: 'sliding_window'
limit: 5
interval: '1 minute'
register:
policy: 'sliding_window'
limit: 3
interval: '1 minute'
api:
policy: 'sliding_window'
limit: 60
interval: '1 minute'
use Symfony\Component\RateLimiter\RateLimiterFactory;
#[Route('/auth/login', methods: ['POST'])]
public function login(
Request $request,
RateLimiterFactory $loginLimiter
): JsonResponse {
$limiter = $loginLimiter->create($request->getClientIp());
if (!$limiter->consume()->isAccepted()) {
return $this->json([
'error' => 'Too many login attempts. Please try again later.'
], 429);
}
// Continue with login...
}
# config/packages/framework.yaml
framework:
session:
cookie_secure: true # HTTPS only
cookie_httponly: true # No JavaScript access
cookie_samesite: lax # CSRF protection
cookie_lifetime: 0 # Session cookie
BetterAuth regenerates session IDs on login:
// Session is regenerated automatically on successful authentication
$this->authManager->signIn($email, $password, $ip, $userAgent);
# config/packages/framework.yaml
framework:
trusted_proxies: '%env(TRUSTED_PROXIES)%'
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto']
// public/index.php
if ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null === 'https') {
$_SERVER['HTTPS'] = 'on';
}
# config/packages/nelmio_security.yaml
nelmio_security:
forced_ssl:
enabled: true
hsts_max_age: 31536000 # 1 year
hsts_subdomains: true
hsts_preload: true
use Symfony\Component\Validator\Validator\ValidatorInterface;
public function register(Request $request, ValidatorInterface $validator): JsonResponse
{
$data = json_decode($request->getContent(), true);
// Validate JSON
if (json_last_error() !== JSON_ERROR_NONE) {
return $this->json(['error' => 'Invalid JSON'], 400);
}
// Validate fields
$constraints = new Assert\Collection([
'email' => [
new Assert\NotBlank(),
new Assert\Email(['mode' => 'strict']),
new Assert\Length(['max' => 180]),
],
'password' => [
new Assert\NotBlank(),
new Assert\Length(['min' => 8, 'max' => 4096]),
new Assert\NotCompromisedPassword(),
],
]);
$errors = $validator->validate($data, $constraints);
if (count($errors) > 0) {
return $this->json(['error' => (string) $errors], 400);
}
// Continue...
}
// Always escape user data in responses
return $this->json([
'user' => [
'name' => htmlspecialchars($user->getName(), ENT_QUOTES, 'UTF-8'),
],
]);
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['X-RateLimit-Limit', 'X-RateLimit-Remaining']
max_age: 3600
paths:
'^/auth':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
'^/api':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
CORS_ALLOW_ORIGIN=https://myapp.com
# NEVER commit these values
BETTER_AUTH_SECRET=change_me_in_production_64_chars_minimum
DATABASE_URL=mysql://user:password@localhost/db
# Use environment-specific files
# .env.local (ignored by git)
# .env.prod.local (production secrets)
# 64 character hex string
php -r "echo bin2hex(random_bytes(32));"
# Or OpenSSL
openssl rand -hex 32
# config/packages/nelmio_security.yaml
nelmio_security:
content_type:
nosniff: true
xss_protection:
enabled: true
mode_block: true
clickjacking:
paths:
'^/.*': DENY
csp:
enabled: true
hosts: []
content_types: []
report_only: false
default_src: ["'self'"]
script_src: ["'self'"]
style_src: ["'self'", "'unsafe-inline'"]
img_src: ["'self'", 'data:']
<?php
namespace App\EventSubscriber;
use BetterAuth\Symfony\Event\BetterAuthEvents;
use Psr\Log\LoggerInterface;
class SecurityAuditSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger
) {}
public static function getSubscribedEvents(): array
{
return [
BetterAuthEvents::AUTHENTICATION_SUCCESS => 'onSuccess',
BetterAuthEvents::AUTHENTICATION_FAILURE => 'onFailure',
BetterAuthEvents::TOKEN_INVALID => 'onInvalidToken',
];
}
public function onSuccess($event): void
{
$this->logger->info('Authentication successful', [
'user_id' => $event->getUser()->getId(),
'ip' => $event->getRequest()->getClientIp(),
'user_agent' => $event->getRequest()->headers->get('User-Agent'),
]);
}
public function onFailure($event): void
{
$this->logger->warning('Authentication failed', [
'reason' => $event->getException()->getMessage(),
'ip' => $event->getRequest()->getClientIp(),
]);
}
public function onInvalidToken($event): void
{
$this->logger->warning('Invalid token detected', [
'ip' => $event->getRequest()->getClientIp(),
'path' => $event->getRequest()->getPathInfo(),
]);
}
}
# config/packages/security.yaml
security:
enable_authenticator_manager: true
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
better_auth:
id: BetterAuth\Symfony\Security\BetterAuthUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
auth:
pattern: ^/auth
stateless: true
security: false
api:
pattern: ^/
stateless: true
provider: better_auth
custom_authenticators:
- BetterAuth\Symfony\Security\BetterAuthAuthenticator
access_control:
- { path: ^/auth, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_USER }
How can I help you explore Laravel packages today?