Multi-auth — multiple guard configurations
Concept
JWT (JSON Web Token) is a stateless authentication mechanism where all necessary information about the user is encoded in the token itself. The server signs the token — any server with the same secret can verify it without a database lookup.
JWT structure: Three Base64URL-encoded parts separated by dots: header.payload.signature.
- Header: Algorithm and token type:
{"alg":"HS256","typ":"JWT"}. - Payload: Claims — registered (
sub,iat,exp) and custom (role,user_id). - Signature:
HMAC(base64(header) + '.' + base64(payload), secret).
Stateless vs session: Sessions store auth state server-side (DB/Redis). JWT embeds state in the token. The server never stores the JWT — just verifies the signature.
Trade-offs:
- Pro: Scales horizontally without shared session storage. Verify on any server with the secret.
- Pro: Good for microservices — services can verify tokens independently.
- Con: Cannot revoke a JWT before expiry (no server-side state to invalidate). Mitigate with short expiry + refresh tokens.
- Con: Payload is Base64-encoded, NOT encrypted. Don't put sensitive data in JWT payload.
- Con: Larger than session cookies.
In Laravel: Not built-in. Use tymon/jwt-auth package or implement manually. For most Laravel APIs, use Sanctum — it's simpler and server-managed. JWT shines in microservice/polyglot architectures.
Refresh tokens: Short-lived access token (15m–1h) + long-lived refresh token. Client exchanges refresh token for new access token. Allows revocation of refresh tokens (stored server-side) while keeping access tokens stateless.
Code Example
<?php
// Manual JWT implementation (educational — use a package in production)
class JwtService
{
private string $secret;
private int $ttl = 3600; // 1 hour
public function __construct()
{
$this->secret = config('app.key');
}
public function encode(array $payload): string
{
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode(array_merge($payload, [
'iat' => time(),
'exp' => time() + $this->ttl,
])));
$sig = base64url_encode(hash_hmac('sha256', "$header.$payload", $this->secret, true));
return "$header.$payload.$sig";
}
public function decode(string $token): array
{
[$header, $payload, $sig] = explode('.', $token);
$expectedSig = base64url_encode(hash_hmac('sha256', "$header.$payload", $this->secret, true));
if (!hash_equals($expectedSig, $sig)) throw new \Exception('Invalid signature');
$data = json_decode(base64url_decode($payload), true);
if ($data['exp'] < time()) throw new \Exception('Token expired');
return $data;
}
}
// Using tymon/jwt-auth package
// composer require tymon/jwt-auth
// php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
// php artisan jwt:secret
class AuthController extends Controller
{
public function login(Request $request)
{
$credentials = $request->validate(['email' => 'required|email', 'password' => 'required']);
$token = auth('api')->attempt($credentials);
if (!$token) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);
}
public function refresh()
{
return response()->json([
'access_token' => auth('api')->refresh(),
]);
}
}