In this lesson, we will add JWT authentication to our Student Management project.
In the previous lesson, the login API verified the user's email and password. Now, after successful authentication, the PHP API will create a JWT token and return it to the React Native application.
JWT stands for JSON Web Token. It is a compact token commonly used to represent authenticated information between a client and server.
Login
↓
Verify Email + Password
↓
Create JWT
↓
Return JWT
↓
React Native
↓
Protected API Requests
Our Student Management APIs should not be publicly accessible. The server needs a way to determine whether a request comes from an authenticated user.
Mobile App
↓
JWT Token
↓
PHP API
↓
Verify Token
↓
Allow Protected Operation
React Native
↓
POST /login.php
↓
PHP
↓
Find User
↓
password_verify()
↓
Create JWT
↓
JSON Response
↓
React Native
↓
Store Token
A JWT normally contains three parts separated by dots.
HEADER.PAYLOAD.SIGNATURE
A JWT header commonly contains the signing algorithm and token type.
{
"alg": "HS256",
"typ": "JWT"
}
The header is part of the token and is encoded using Base64URL.
The payload contains claims about the token.
{
"sub": 1,
"email": "rahul@example.com",
"role": "user",
"iat": 1760000000,
"exp": 1760003600
}
The payload should not contain sensitive information such as passwords.
The signature is created using the JWT header, payload, and a secret key or another signing mechanism depending on the selected algorithm.
Header
+
Payload
+
Secret Key
↓
Signature
The server uses the signing information to verify that the token is valid.
A common PHP library for working with JWT is
firebase/php-jwt.
Install it with Composer:
composer require firebase/php-jwt
Composer will create the required vendor files.
require_once __DIR__ .
'/vendor/autoload.php';
The Composer autoloader makes the JWT library classes available to PHP.
use Firebase\JWT\JWT;
We can then use the JWT class to create tokens.
The server needs a secret key when using a symmetric algorithm such as HS256.
$secretKey =
'CHANGE_THIS_TO_A_LONG_RANDOM_SECRET';
Useful claims for our project include:
| Claim | Purpose |
|---|---|
| sub | User identifier |
| User email | |
| role | User role |
| iat | Token creation time |
| exp | Token expiration time |
$issuedAt = time();
$expiresAt =
$issuedAt + (60 * 60);
In this example, the token is configured to expire after one hour.
$payload = [
'sub' => $user['id'],
'email' => $user['email'],
'role' => $user['role'],
'iat' => $issuedAt,
'exp' => $expiresAt
];
The sub claim identifies the authenticated user.
$token = JWT::encode(
$payload,
$secretKey,
'HS256'
);
The resulting token can be returned to the React Native application.
echo json_encode([
"success" => true,
"message" => "Login successful",
"token" => $token,
"data" => [
"id" => $user['id'],
"name" => $user['name'],
"email" => $user['email'],
"role" => $user['role']
]
]);
The password and password hash are not included in the response.
Email + Password
↓
Find User
↓
password_verify()
↓
Create Claims
↓
JWT::encode()
↓
JWT Token
↓
JSON Response
<?php
header(
"Content-Type: application/json"
);
require_once '../config/database.php';
require_once __DIR__ .
'/vendor/autoload.php';
use Firebase\JWT\JWT;
$secretKey =
'CHANGE_THIS_TO_A_LONG_RANDOM_SECRET';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
"success" => false,
"message" => "Method not allowed"
]);
exit;
}
$input = json_decode(
file_get_contents("php://input"),
true
);
$email = trim($input['email'] ?? '');
$password = $input['password'] ?? '';
if ($email === '' || $password === '') {
http_response_code(422);
echo json_encode([
"success" => false,
"message" =>
"Email and password are required"
]);
exit;
}
try {
$stmt = $pdo->prepare(
"SELECT id, name, email, password, role
FROM users
WHERE email = ?"
);
$stmt->execute([$email]);
$user = $stmt->fetch(
PDO::FETCH_ASSOC
);
if (!$user ||
!password_verify(
$password,
$user['password']
)) {
http_response_code(401);
echo json_encode([
"success" => false,
"message" =>
"Invalid email or password"
]);
exit;
}
$issuedAt = time();
$expiresAt =
$issuedAt + (60 * 60);
$payload = [
'sub' => $user['id'],
'email' => $user['email'],
'role' => $user['role'],
'iat' => $issuedAt,
'exp' => $expiresAt
];
$token = JWT::encode(
$payload,
$secretKey,
'HS256'
);
http_response_code(200);
echo json_encode([
"success" => true,
"message" => "Login successful",
"token" => $token,
"data" => [
"id" => $user['id'],
"name" => $user['name'],
"email" => $user['email'],
"role" => $user['role']
]
]);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
echo json_encode([
"success" => false,
"message" => "Server error"
]);
}
React Native will receive the token from the login API.
{
"success": true,
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIs...",
"data": {
"id": 1,
"name": "Rahul Kumar",
"email": "rahul@example.com",
"role": "user"
}
}
The token should then be stored so it can be used for protected requests.
interface LoginResponse {
success: boolean;
message: string;
token: string;
data: {
id: number;
name: string;
email: string;
role: string;
};
}
This interface describes the successful JWT login response.
const response =
await api.post<LoginResponse>(
"/login.php",
{
email,
password
}
);
const token =
response.data.token;
The token can now be stored locally.
React Native can use AsyncStorage for basic token persistence.
await AsyncStorage.setItem(
"authToken",
token
);
For applications with stronger security requirements, platform secure storage should be considered for sensitive credentials.
The token is normally sent using the Authorization header.
Authorization:
Bearer YOUR_JWT_TOKEN
Example with Axios:
const response =
await api.get(
"/students.php",
{
headers: {
Authorization:
`Bearer ${token}`
}
}
);
The protected PHP API needs to extract the Bearer token from the Authorization header.
$headers = getallheaders();
$authorization =
$headers['Authorization']
?? '';
if (
!preg_match(
'/Bearer\s(\S+)/',
$authorization,
$matches
)
) {
http_response_code(401);
echo json_encode([
"success" => false,
"message" =>
"Authentication required"
]);
exit;
}
$token = $matches[1];
The server must verify the token before allowing access to a protected endpoint.
Request
↓
Read Authorization Header
↓
Extract JWT
↓
Verify Signature
↓
Check Expiration
↓
Read User ID
↓
Allow Request
The actual verification code will be implemented in the upcoming protected API lessons.
Our JWT contains an exp claim.
'exp' => $expiresAt
Once the token expires, protected API requests should no longer accept it.
Expired JWT
↓
HTTP 401
↓
React Native
↓
Login Again
Send a POST request to:
https://example.com/api/login.php
Body:
{
"email": "rahul@example.com",
"password": "secret123"
}
The response should contain a JWT token when the credentials are valid.
Registration
↓
User Stored in MySQL
↓
Login
↓
Verify Password
↓
Create JWT
↓
React Native Stores Token
↓
Axios Request
↓
Authorization: Bearer JWT
↓
PHP Verifies JWT
↓
Protected Student API
JWT authentication is now part of our Student Management project.
In the next lesson, we will use the JWT-authenticated user to build the Student API.
sub claim can identify the user.exp claim defines token expiration.firebase/php-jwt can be used to create JWTs in PHP.Question: Which HTTP header is normally used to send a JWT token to a protected API?