Modern APIs need authentication that works across web, mobile, and third-party applications. JSON Web Tokens (JWT) provide stateless, scalable authentication perfect for RESTful APIs. At ZIRA Software, JWT authentication powers APIs serving millions of requests daily.
Why JWT for APIs?
Traditional session-based auth problems:
- Server must store session data
- Doesn't scale horizontally
- CORS complications
- Mobile apps struggle with cookies
JWT advantages:
- Stateless (no server-side storage)
- Scales horizontally
- Works across domains
- Perfect for mobile/SPA
- Contains user data (claims)
- Industry standard
What is JWT?
A JWT is a compact, URL-safe token consisting of three parts:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Structure:
Header.Payload.Signature
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload (claims):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516325422
}
Signature:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Installing JWT Auth
composer require tymon/jwt-auth:^1.0
Publish Configuration
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
Generate Secret Key
php artisan jwt:secret
This adds JWT_SECRET to your .env file.
Configure User Model
<?php namespace App;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements JWTSubject
{
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*/
public function getJWTCustomClaims()
{
return [
'email' => $this->email,
'name' => $this->name
];
}
}
Configure Auth Guard
// config/auth.php
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
Authentication Controller
<?php namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Facades\JWTAuth;
use App\User;
class AuthController extends Controller
{
/**
* Register new user
*/
public function register(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password),
]);
$token = JWTAuth::fromUser($user);
return response()->json([
'user' => $user,
'token' => $token
], 201);
}
/**
* Login user
*/
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json([
'error' => 'Invalid credentials'
], 401);
}
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60
]);
}
/**
* Get authenticated user
*/
public function me()
{
return response()->json(auth()->user());
}
/**
* Logout user
*/
public function logout()
{
auth()->logout();
return response()->json([
'message' => 'Successfully logged out'
]);
}
/**
* Refresh token
*/
public function refresh()
{
return response()->json([
'token' => auth()->refresh(),
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60
]);
}
}
API Routes
// routes/api.php
Route::group(['prefix' => 'auth'], function() {
Route::post('register', 'AuthController@register');
Route::post('login', 'AuthController@login');
Route::group(['middleware' => 'auth:api'], function() {
Route::get('me', 'AuthController@me');
Route::post('logout', 'AuthController@logout');
Route::post('refresh', 'AuthController@refresh');
});
});
Route::group(['middleware' => 'auth:api'], function() {
Route::apiResource('posts', 'PostController');
Route::apiResource('comments', 'CommentController');
});
Making API Requests
Registration
curl -X POST http://api.app/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "secret123",
"password_confirmation": "secret123"
}'
Response:
{
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
Login
curl -X POST http://api.app/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "secret123"
}'
Accessing Protected Routes
curl -X GET http://api.app/auth/me \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
Refreshing Token
curl -X POST http://api.app/auth/refresh \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
Frontend Integration
JavaScript/jQuery
// Login
$.ajax({
url: 'http://api.app/auth/login',
type: 'POST',
data: {
email: 'john@example.com',
password: 'secret123'
},
success: function(response) {
// Store token
localStorage.setItem('token', response.token);
}
});
// Authenticated request
$.ajax({
url: 'http://api.app/api/posts',
type: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
success: function(posts) {
console.log(posts);
}
});
Angular
// Auth Service
app.factory('AuthService', ['$http', '$window', function($http, $window) {
return {
login: function(credentials) {
return $http.post('/auth/login', credentials)
.then(function(response) {
$window.localStorage.setItem('token', response.data.token);
});
},
logout: function() {
$window.localStorage.removeItem('token');
},
getToken: function() {
return $window.localStorage.getItem('token');
}
};
}]);
// HTTP Interceptor
app.factory('AuthInterceptor', ['$window', function($window) {
return {
request: function(config) {
var token = $window.localStorage.getItem('token');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
},
responseError: function(response) {
if (response.status === 401) {
$window.location = '/login';
}
return response;
}
};
}]);
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
}]);
React
// API Helper
const API_URL = 'http://api.app';
const headers = () => ({
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
});
export const login = async (email, password) => {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
}
return data;
};
export const getPosts = async () => {
const response = await fetch(`${API_URL}/api/posts`, {
headers: headers()
});
return response.json();
};
Token Expiration & Refresh
// config/jwt.php
'ttl' => env('JWT_TTL', 60), // 60 minutes
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), // 2 weeks
Handling expired tokens:
fetch('/api/posts', {
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => {
if (response.status === 401) {
// Token expired, refresh it
return refreshToken().then(newToken => {
// Retry original request
return fetch('/api/posts', {
headers: {
'Authorization': 'Bearer ' + newToken
}
});
});
}
return response.json();
});
function refreshToken() {
return fetch('/auth/refresh', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => response.json())
.then(data => {
localStorage.setItem('token', data.token);
return data.token;
});
}
Blacklisting Tokens
Invalidate tokens on logout:
// Enable blacklist
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
// Logout
public function logout()
{
try {
JWTAuth::invalidate(JWTAuth::getToken());
return response()->json([
'message' => 'Successfully logged out'
]);
} catch (JWTException $e) {
return response()->json([
'error' => 'Failed to logout'
], 500);
}
}
Custom Claims
Add custom data to JWT payload:
public function getJWTCustomClaims()
{
return [
'user_id' => $this->id,
'email' => $this->email,
'role' => $this->role,
'permissions' => $this->permissions->pluck('name'),
'subscription' => [
'plan' => $this->subscription->plan,
'expires_at' => $this->subscription->expires_at
]
];
}
Access claims:
$payload = JWTAuth::parseToken()->getPayload();
$userId = $payload->get('user_id');
$role = $payload->get('role');
$permissions = $payload->get('permissions');
Middleware for Role-Based Access
<?php namespace App\Http\Middleware;
use Closure;
class CheckRole
{
public function handle($request, Closure $next, $role)
{
if (!$request->user() || !$request->user()->hasRole($role)) {
return response()->json(['error' => 'Forbidden'], 403);
}
return $next($request);
}
}
// Register in Kernel.php
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// Use in routes
Route::get('admin/users', 'AdminController@users')
->middleware('auth:api', 'role:admin');
Security Best Practices
1. Use HTTPS in production:
if (app()->environment('production')) {
URL::forceScheme('https');
}
2. Strong secret key:
# Generate with:
php artisan jwt:secret
3. Short token lifetime:
'ttl' => 60, // 1 hour
4. Validate on every request:
Route::middleware('auth:api')->get('/user', function () {
return auth()->user();
});
5. Sanitize custom claims:
public function getJWTCustomClaims()
{
return [
'email' => $this->email,
// Don't include sensitive data like passwords!
];
}
6. Implement rate limiting:
Route::middleware('throttle:60,1')->group(function() {
Route::post('login', 'AuthController@login');
});
Testing JWT Authentication
class AuthTest extends TestCase
{
public function testUserCanRegister()
{
$response = $this->json('POST', '/auth/register', [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'secret123',
'password_confirmation' => 'secret123'
]);
$response->assertStatus(201)
->assertJsonStructure(['user', 'token']);
}
public function testUserCanLogin()
{
$user = factory(User::class)->create([
'password' => bcrypt('secret123')
]);
$response = $this->json('POST', '/auth/login', [
'email' => $user->email,
'password' => 'secret123'
]);
$response->assertStatus(200)
->assertJsonStructure(['token']);
}
public function testAuthenticatedUserCanAccessProtectedRoute()
{
$user = factory(User::class)->create();
$token = JWTAuth::fromUser($user);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token
])->json('GET', '/auth/me');
$response->assertStatus(200)
->assertJson(['email' => $user->email]);
}
}
Troubleshooting
"Token could not be parsed from the request":
- Ensure header is
Authorization: Bearer <token> - Check token is valid JWT format
"Token has expired":
- Refresh token or login again
- Check
JWT_TTLconfiguration
"The token has been blacklisted":
- Token was invalidated (logout)
- Login again to get new token
Conclusion
JWT provides robust, scalable authentication for modern APIs. At ZIRA Software, JWT authentication has enabled us to build APIs serving web, mobile, and third-party integrations seamlessly.
The stateless nature of JWT means your API scales horizontally without session management headaches. Implement it correctly, follow security best practices, and you'll have authentication that just works.
Building an API? Contact ZIRA Software to discuss JWT authentication, API security, and scalable backend architecture for your application.