# JWT-Stateless

## Introduction

Les <abbr title="JSON Web Token">JWT</abbr> offrent une alternative sans session
pour conserver la trace d'une authentification réussie.

Contrairement aux sessions, ou un fichier (par session) est créé sur le serveur, 
les jetons JWT permettent un fonctionnement du serveur "sans état" (stateless).


## Principe de fonctionnement

### Avec session

![Diagramme des flux](Schéma sessions.svg "Mécanisme des sessions")

*La session devrait expirer après une certaine période d'inactivité. A chaque
requête, la fin de validité peut-être prolongée.*

### Avec jeton (JWT)

![Diagramme des flux](Schéma JWT.svg "Mécanisme des jetons")

Le serveur ne stocke plus d'état relatif à l'utilisateur :

- Lorsque l'authentification est réussie, le serveur génère un jeton (JWT)
contenant les données d'identité (adresse de courriel, nom d'utilisateur…)
et le signe (algorithme <abbr title="Hash-based Message Authentication Code">HMAC</abbr>)
avec un secret qu'il est seul à connaître.
- En réponse, il envoie le jeton au client, par exemple dans un cookie.

A chaque requête <abbr title="HyperText Transfer Protocol">HTTP</abbr> :

- Le client envoie son jeton au serveur.
- Le serveur vérifie la signature du jeton (pour s'assurer qu'il n'a pas été
falsifié) et accède aux données qu'il contient (identité…).

Attention :

- Le jeton ne doit contenir **aucun secret (il n'est pas chiffré)** ; *tout
le monde* peut le lire, mais seul celui qui connait le "mot de passe" peut
générer une signature valide qui sera reconnue par le serveur.
- Le mot de passe qui sert à signer les jetons ne doit pas être versionné.
Une stratégie consiste à utiliser une variable d'environnement (définie par
exemple dans un fichier `.htaccess`).

*Remarque : le jeton peut également être enregistré dans le stockage local
du navigateur, et envoyé en entête (`Authorization: Bearer`), mais cette solution
requiert l'usage du Javascript.*


## Classe utilitaire

Une bibliothèque de fonctions contenant la classe utilitaire `JWT` est mise
à disposition. L'étude de son code permet de comprendre la structure d'un
jeton et le mécanisme de signature / vérification.

```php
<?php
/**
 * JSON Web Tokens (JWT) minimalist library
 * Frank ENDRES, teacher (first dot last at ac-polynesie.pf) - 2023-12, Education Nationale
 * Licence: [CeCILL-C](http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.txt)
 * 
 * Usage:
 * 	- require 'path/to/this/file.php';
 * 
 * provided static methods:
 * 	- JWT::encode(string $secret, string $fieldname, string $value, int $timeout = 600, string $issuer = $_SERVER['SERVER_NAME']) -> string
 * 	- JWT::decode(string $secret, string $jwt) -> ?object (payload with 'exp', 'iss' and 'fieldname' fields)
 * 	- JWT::setcookie(string $secret, string $fieldname, string $value, int $timeout = JWT::TIMEOUT, array $options = []) : void
 * 	- JWT::getcookie(string $secret, string $name) : mixed
 * 	- JWT::delcookie(string $fieldname, array $options = []) : void
**/

class JWT {

	const TIMEOUT = 6000; //one hour
	const ALGORITHM = 'sha256';
	const HEADER = '{"alg":"HS256","typ":"JWT"}';
	const COOKIE_OPTIONS = ['secure' => true, 'httponly' => true, 'samesite' => 'Strict'];


	/**
	 * encodes a JWT
	 * payload fields are "exp", "iss" and the provided fieldname
	 * @param secret the secret key for HMAC
	 * @param fieldname the name of the value ('login', 'email', ... for example)
	 * @param value the value of the fieldname
	 * @param timeout the JWT timeout
	 * @param issuer the FQDN of the JWT issuer
	 * @return the JSON Web Token
	**/
	static function encode(string $secret, string $fieldname, string $value, 
						   int $timeout = self::TIMEOUT, string $issuer = ''): string {
		if ('' == $issuer) {
			$issuer = $_SERVER['SERVER_NAME'];
		}
		$payload = '{"iss":"' . $issuer . '","exp":' . (time() + $timeout) . ',"' . $fieldname . '":"' . $value . '"}';
		
		$b64u_header = rtrim(strtr(base64_encode(self::HEADER), '+/', '-_'), '='); //base64_url
		$b64u_payload = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
		$signature = hash_hmac(self::ALGORITHM, $b64u_header . '.' . $b64u_payload, $secret, true);
		$b64u_signature = rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
		$jwt = $b64u_header . '.' . $b64u_payload . '.' . $b64u_signature;
		
		return $jwt;
	}


	/**
	 * decodes a JWT
	 * @param secret the secret key for HMAC
	 * @param jwt the JSON Web Token to decode
	 * @return payload object or null if invalid
	**/
	static function decode(string $secret, string $jwt) : ?object {
		$payload = null;
		$parts = explode('.', $jwt, 3);
		if (3 == sizeof($parts)) {
			$b64u_header = $parts[0];
			$b64u_payload = $parts[1];
			$b64u_token_signature = $parts[2];
			$expected_signature = hash_hmac(self::ALGORITHM, $b64u_header . '.' . $b64u_payload, $secret, true);
			$b64u_expected_signature = rtrim(strtr(base64_encode($expected_signature), '+/', '-_'), '=');
			if ($b64u_token_signature == $b64u_expected_signature) {
				$rest = strlen($b64u_payload) % 4;
				$padding = $rest == 0 ? '' : str_repeat('=', 4 - $rest);
				$payload = json_decode(base64_decode(strtr($b64u_payload, '-_', '+/') . $padding));
			}
		}
		return $payload;
	}

	/**
	 * encodes a JWT and sets it as cookie
	 * payload fields are "exp", "iss" and the provided fieldname
	 * @param secret the secret key for HMAC
	 * @param fieldname the name of the value ('login', 'email', ... for example) and the cookie
	 * @param value the value of the fieldname
	 * @param timeout the JWT timeout
	 * @param options cookie options; defaults to httponly, secure and samesite=strict
	**/
	static function setcookie(string $secret, string $fieldname, string $value,
							  int $timeout = self::TIMEOUT, array $options = self::COOKIE_OPTIONS) : void {
		$jwt = self::encode($secret, $fieldname, $value, $timeout);
		$options['expires'] = time() + $timeout;
		setcookie($fieldname, $jwt, $options);
	}

	/**
	 * deletes a JWT cookie
	 * @param fieldname the name of the cookie to delete
	 * @param options cookie options; defaults to httponly, secure and samesite=strict
	**/
	static function delcookie(string $fieldname, array $options = self::COOKIE_OPTIONS) : void {
		$options['expires'] = time() - 3600;
		setcookie($fieldname, '', $options);
	}

	/**
	 * retrieves a value from a JWT cookie
	 * @param secret the secret key for HMAC
	 * @param name the name of the cookie and payload attribute to return
	 * @return the payload attribute or null if invalid or expired
	**/
	static function getcookie(string $secret, string $name) : mixed {
		$value = null;
		if (isset($_COOKIE[$name])) {
			$payload = self::decode($secret, $_COOKIE[$name]);
			if (null !== $payload && isset($payload->exp) && $payload->exp > time() && isset($payload->{$name})) {
				$value = $payload->{$name};
			}
		}
		return $value;
	}
}
?>
```

*L'intérêt de l'encodage base64 (standard JWT) est qu'il permet de stocker
des données même binaires.*

## Mise en oeuvre

### Le secret

Créer un fichier `.htaccess` à la racine de l'application (à côté de `index.php`),
avec le contenu suivant :

	SetEnv JWT_SECRET "Votre se<ret lOng et κoμπλεχε."

Avant toute utilisation du secret, il faudra charger cette variable d'environnement :

```php
if (!defined('JWT_SECRET')) define('JWT_SECRET', getenv('JWT_SECRET'));
```

### Repérer les modification à apporter

Le mécanisme d'authentification ne change pas. Ce qui change, c'est l'endroit
ou est mémorisé la trace de l'authentification réussie : ce n'est plus dans
un fichier de session du serveur, mais dans un jeton JWT (stocké dans les
cookies du navigateur).

Reprendre le code de l'activité précédente sur l'authentification et commenter
les `session_start()` et `session_destroy()` ainsi que les instructions `$_SESSION`.

### Le challenge

Le challenge peut également être enregistré dans un JWT (en plus d'être mis
en champ caché du formulaire) ; c'est important, car il ne faut pas permettre
au client de choisir le challenge à signer (possibilité de rejeu) :

```php
JWT::setcookie(JWT_SECRET, 'challenge', $challenge, 300);
```

Le cookie devrait être supprimé après authentification :

```php
JWT::delcookie('challenge');
```

### Le jeton d'identification

Lorsque l'authentification est validée, un jeton contenant l'adresse de courriel
(identifiant l'utilisateur) est émis par le serveur :

```php
JWT::setcookie(JWT_SECRET, 'email', $email, INACTIVITY_TIMEOUT);
```

Utiliser la même instruction pour raffraîchir le jeton (dans `check-auth.php`)

La déconnexion s'effectue simplement en détruisant le cookie :

```php
JWT::delcookie('email');
```


## Questions de réflexion

- Qu'est-ce qu'un rejeu. Comment un attaquant pourrait-il rejouer la signature
d'un challenge ?
- Décoder manuellement ce jeton JWT (la signature ne peut être vérifiée) :
`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ3ZWIuc2lvLmxvY2FsIiwiZXhwIjoxNzc3MzUyMjUzLCJlbWFpbCI6Imx1Y2t5QGx1a2UudXMifQ.-KWzTA0zS5DOfnMjm20-wyALkUofQ2odwhcroHrLVeI`
