Een handleiding voor het beveiligen van headless systemen met JSON Web Tokens.
Inleiding: Waarom JWT voor Headless Beveiliging?

Headless betekent dat uw backend uitsluitend data levert, zonder presentatielaag—bijvoorbeeld een PHP-API die alleen JSON terugstuurt naar een mobiele app of JavaScript-frontend. In zo’n stateless omgeving werkt traditionele sessieauthenticatie niet soepel: de server moet elke request onafhankelijk verwerken zonder gebruikerssessies bij te houden.
JSON Web Tokens (JWT) bieden een compacte, self-contained en veilige manier om autorisatiegegevens mee te sturen. De client ontvangt na inloggen een token dat bij elke API-call wordt meegestuurd, waardoor uw backend eenvoudig kan verifiëren of de gebruiker geautoriseerd is.
De Anatomie van een JWT
- Header: Bevat metadata zoals het algoritme (
alg) en het type token (typ). - Payload: Bevat de “claims”—gegevens over de gebruiker (
sub), de issuer (iss), en de expiration (exp). U kunt hier ook rollen of permissies toevoegen. - Signature: Een cryptografische handtekening over header en payload, met uw geheime sleutel, om manipulatie te voorkomen.
Deel 1: De PHP Backend – Een JWT API Bouwen
In dit deel bouwen we de PHP-backend die tokens uitgeeft en valideert. We gebruiken firebase/php-jwt om JWT’s aan te maken en te controleren, en zorgen voor strikte claim- en algoritmevalidatie.
- Structuur:
api/login.php: Valideert inloggegevens en geeft een JWT (access + refresh) terug.api/refresh.php: Ververst het access token met een geldig refresh token.api/secure-data.php: Beveiligd endpoint; alleen toegankelijk met een geldig access token..env: Bevat uw geheime sleutels (JWT_SECRETenJWT_REFRESH_SECRET).
- Installatie:
composer require firebase/php-jwt
1. Token Genereren (login.php)
<?php
require __DIR__ . '/vendor/autoload.php';
use Firebase\JWT\JWT;
// Laad geheim uit .env
$secret = getenv('JWT_SECRET');
$refreshSecret= getenv('JWT_REFRESH_SECRET');
// Valideer gebruiker...
if (validUser($_POST['email'], $_POST['password'])) {
$now = time();
$accessPayload = [
'iss' => 'https://uw-domein.nl',
'aud' => 'https://uw-domein.nl',
'iat' => $now,
'exp' => $now + 900, // 15 minuten
'sub' => getUserId(),
'role'=> 'user'
];
$refreshPayload = [
'iss' => 'https://uw-domein.nl',
'aud' => 'https://uw-domein.nl',
'iat' => $now,
'exp' => $now + 604800, // 1 week
'sub' => getUserId()
];
$accessToken = JWT::encode($accessPayload, $secret, 'HS256');
$refreshToken = JWT::encode($refreshPayload, $refreshSecret, 'HS256');
echo json_encode(['accessToken'=>$accessToken, 'refreshToken'=>$refreshToken]);
exit;
}
http_response_code(401);
echo json_encode(['error'=>'Ongeldige inloggegevens']);
2. Token Valideren (secure-data.php)
<?php
require __DIR__ . '/vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$secret = getenv('JWT_SECRET');
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (preg_match('/Bearer\s(\S+)/', $auth, $m)) {
try {
$token = $m[1];
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
// Controleer iss en aud
if ($decoded->iss !== 'https://uw-domein.nl' || $decoded->aud !== 'https://uw-domein.nl') {
throw new Exception('Invalid token issuer or audience');
}
echo json_encode(['data'=>'Beveiligde informatie']);
exit;
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['error'=>'Toegang geweigerd']);
exit;
}
}
http_response_code(401);
echo json_encode(['error'=>'Geen token gevonden']);
3. Refresh Endpoint (refresh.php)
<?php
require __DIR__ . '/vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$refreshSecret = getenv('JWT_REFRESH_SECRET');
$body = json_decode(file_get_contents('php://input'), true);
try {
$decoded = JWT::decode($body['refreshToken'], new Key($refreshSecret, 'HS256'));
$now = time();
$newAccess = [
'iss'=>'https://uw-domein.nl','aud'=>'https://uw-domein.nl',
'iat'=>$now,'exp'=>$now+900,'sub'=>$decoded->sub
];
$token = JWT::encode($newAccess, getenv('JWT_SECRET'), 'HS256');
echo json_encode(['accessToken'=>$token]);
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['error'=>'Ongeldig refresh token']);
}
Deel 2: De Ionic Vue App – API Communicatie met Axios
In dit deel bouwen we de frontend in Ionic Vue. We installeren Axios, maken een service voor al onze API-calls inclusief refresh-token-rotatie, en implementeren veilige opslag.
1. Secure Storage & Axios Setup
Installeer eerst secure storage plugin voor Capacitor:
npm install @capacitor-community/secure-storage
// src/services/AxiosService.js
import axios from 'axios';
import { SecureStoragePlugin } from '@capacitor-community/secure-storage';
const API_URL = import.meta.env.VITE_API_URL || 'https://www.jouw-domein.nl/api';
const storage = new SecureStoragePlugin();
const axiosInstance = axios.create({ baseURL: API_URL, headers:{'Content-Type':'application/json'} });
axiosInstance.interceptors.request.use(async config => {
const token = await storage.get({ key:'accessToken' });
if (token.value) config.headers.Authorization = `Bearer ${token.value}`;
return config;
});
export default axiosInstance;
2. Login & Token Opslag (LoginPage.vue)
<script setup>
import { SecureStoragePlugin } from '@capacitor-community/secure-storage';
import axiosService from '@/services/AxiosService';
const storage = new SecureStoragePlugin();
async function handleLogin() {
const resp = await axiosService.post('/login.php', params);
await storage.set({ key:'accessToken', value:resp.data.accessToken });
await storage.set({ key:'refreshToken', value:resp.data.refreshToken });
router.push('/tabs/dashboard');
}
</script>
3. Refresh Logic
In uw interceptor kunt u bij 401 automatisch een refresh-call doen:
axiosInstance.interceptors.response.use(null, async error => {
if (error.response?.status === 401) {
const refresh = await storage.get({key:'refreshToken'});
const { data } = await axios.post('/refresh.php', { refreshToken: refresh.value });
await storage.set({key:'accessToken', value:data.accessToken});
error.config.headers.Authorization = `Bearer ${data.accessToken}`;
return axiosInstance.request(error.config);
}
return Promise.reject(error);
});
Best Practices & Veiligheid
- Gebruik altijd HTTPS voor transportbeveiliging.
- Bewaar tokens in veilige opslag (bv. Capacitor Secure Storage), niet in localStorage.
- Implementeer access- en refresh-tokenrotatie voor kortlevende sessies.
- Valideer altijd
iss,audenalgaan serverzijde. - Beperk token-payload tot minimaal noodzakelijke data.
Conclusie
Deze gids onderstreept dat echte beveiliging begint bij een “security-first” architectuur: korte levensduur tokens, dual-tokenstrategie, veilige client-opslag en strikte server-validatie. Met deze lagenaanpak bouwt u een veerkrachtige, productieklare headless applicatie die bestand is tegen moderne dreigingen.
```

Stappen om een PDF te downloaden en weer te geven in een Ionic Vue-app (Android/iOS):