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 presentatie­laag—bijvoorbeeld een PHP-API die alleen JSON terugstuurt naar een mobiele app of JavaScript-frontend. In zo’n stateless omgeving werkt traditionele sessie­authenticatie niet soepel: de server moet elke request onafhankelijk verwerken zonder gebruikers­sessies bij te houden.

JSON Web Tokens (JWT) bieden een compacte, self-contained en veilige manier om autorisatie­gegevens 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 algoritme­validatie.

  • 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_SECRET en JWT_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, aud en alg aan 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.

```