202 lines
6.6 KiB
PHP
202 lines
6.6 KiB
PHP
<?php
|
|
require_once('UserDataSet.php');
|
|
|
|
/**
|
|
* Backend Authentication service for handling JWT authentication
|
|
* https://jwt.io/introduction
|
|
* This cost me blood, sweat and tears, mostly tears.
|
|
*/
|
|
class AuthService {
|
|
private string $secretKey;
|
|
private int $tokenExpiry;
|
|
|
|
/**
|
|
* Initialises the authentication service
|
|
* Loads configuration from environment variables
|
|
* @throws Exception if OpenSSL extension is not loaded
|
|
*/
|
|
public function __construct() {
|
|
// Load environment variables from .env file (:D more configuration needs to be added to .env, but scope creep already huge)
|
|
$envFile = __DIR__ . '/../.env';
|
|
if (file_exists($envFile)) {
|
|
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
foreach ($lines as $line) {
|
|
// Skip comments
|
|
if (strpos($line, '#') === 0) continue;
|
|
|
|
// Parse environment variable
|
|
list($name, $value) = explode('=', $line, 2);
|
|
$name = trim($name);
|
|
$value = trim($value);
|
|
|
|
if (!empty($name)) {
|
|
putenv(sprintf('%s=%s', $name, $value));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set configuration from environment variables with defaults
|
|
$this->secretKey = getenv('JWT_SECRET_KEY') ?: 'your-256-bit-secret';
|
|
$this->tokenExpiry = (int)(getenv('JWT_TOKEN_EXPIRY') ?: 3600);
|
|
|
|
// Verify OpenSSL extension is available. This should be on by default regardless, but just in case.
|
|
if (!extension_loaded('openssl')) {
|
|
throw new Exception('OpenSSL extension is required for JWT');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a JWT token
|
|
* @param array $userData User information to include in token
|
|
* @return string The generated JWT token
|
|
*/
|
|
public function generateToken(array $userData): string {
|
|
$issuedAt = time();
|
|
$expire = $issuedAt + $this->tokenExpiry;
|
|
|
|
// Create payload with user data
|
|
$payload = [
|
|
'iat' => $issuedAt,
|
|
'exp' => $expire,
|
|
'uid' => $userData['id'],
|
|
'username' => $userData['username'],
|
|
'accessLevel' => $userData['userType']
|
|
];
|
|
|
|
return $this->encodeJWT($payload);
|
|
}
|
|
|
|
/**
|
|
* Validates a JWT token
|
|
* @param string $token The JWT token to validate
|
|
* @return array|null The decoded payload if valid, null otherwise
|
|
*/
|
|
public function validateToken(string $token): ?array {
|
|
try {
|
|
$payload = $this->decodeJWT($token);
|
|
|
|
// Check if token is expired
|
|
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time()) {
|
|
return null;
|
|
}
|
|
|
|
return $payload;
|
|
} catch (Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encodes data into a JWT token
|
|
* @param array $payload The data to encode
|
|
* @return string The encoded JWT token
|
|
*/
|
|
private function encodeJWT(array $payload): string {
|
|
// Create and encode header
|
|
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
|
$header = $this->base64UrlEncode($header);
|
|
|
|
// Create and encode payload
|
|
$payload = json_encode($payload);
|
|
$payload = $this->base64UrlEncode($payload);
|
|
|
|
// Create and encode signature
|
|
$signature = hash_hmac('sha256', "$header.$payload", $this->secretKey, true);
|
|
$signature = $this->base64UrlEncode($signature);
|
|
|
|
return "$header.$payload.$signature"; //Wooooooo!!! JWT is a thing!
|
|
}
|
|
|
|
/**
|
|
* Decodes a JWT token
|
|
* @param string $token The JWT token to decode
|
|
* @return array|null The decoded payload if valid, null otherwise
|
|
*/
|
|
private function decodeJWT(string $token): ?array {
|
|
// Split token into components
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 3) {
|
|
return null;
|
|
}
|
|
|
|
[$header, $payload, $signature] = $parts;
|
|
|
|
// Verify signature
|
|
$validSignature = $this->base64UrlEncode(
|
|
hash_hmac('sha256', "$header.$payload", $this->secretKey, true)
|
|
);
|
|
|
|
if ($signature !== $validSignature) {
|
|
return null;
|
|
}
|
|
|
|
// Decode and return payload
|
|
return json_decode($this->base64UrlDecode($payload), true);
|
|
}
|
|
|
|
/**
|
|
* Encodes data using base64url encoding
|
|
* @param string $data The data to encode
|
|
* @return string The encoded data
|
|
*/
|
|
private function base64UrlEncode(string $data): string {
|
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
|
}
|
|
|
|
/**
|
|
* Decodes base64url encoded data
|
|
* @param string $data The data to decode
|
|
* @return string The decoded data
|
|
*/
|
|
private function base64UrlDecode(string $data): string {
|
|
return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', 3 - (3 + strlen($data)) % 4));
|
|
}
|
|
|
|
/**
|
|
* Generates a refresh token for a user
|
|
* @param array $userData User information to include in token
|
|
* @return string The generated refresh token
|
|
*/
|
|
public function generateRefreshToken(array $userData): string {
|
|
$issuedAt = time();
|
|
$expire = $issuedAt + ($this->tokenExpiry * 24); // Refresh token lasts 24 times longer than access token
|
|
|
|
$payload = [
|
|
'iat' => $issuedAt,
|
|
'exp' => $expire,
|
|
'uid' => $userData['id'],
|
|
'username' => $userData['username'],
|
|
'type' => 'refresh'
|
|
];
|
|
|
|
return $this->encodeJWT($payload);
|
|
}
|
|
|
|
/**
|
|
* Refreshes an access token using a refresh token
|
|
* @param string $refreshToken The refresh token
|
|
* @return string|null The new access token if valid, null otherwise
|
|
*/
|
|
public function refreshToken(string $refreshToken): ?string {
|
|
try {
|
|
$payload = $this->decodeJWT($refreshToken);
|
|
|
|
// Check if token is expired or not a refresh token
|
|
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time() ||
|
|
!isset($payload['type']) || $payload['type'] !== 'refresh') {
|
|
return null;
|
|
}
|
|
|
|
// Generate a new access token
|
|
$userData = [
|
|
'id' => $payload['uid'],
|
|
'username' => $payload['username'],
|
|
'userType' => isset($payload['accessLevel']) ? $payload['accessLevel'] : 0
|
|
];
|
|
|
|
return $this->generateToken($userData);
|
|
} catch (Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|