114 lines
3.1 KiB
PHP
114 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Staff;
|
|
|
|
class StaffTimeOffLinkService
|
|
{
|
|
private const TOKEN_CONTEXT = 'timeoff_notify';
|
|
|
|
private const DEFAULT_TTL = 1209600;
|
|
|
|
private string $secret;
|
|
|
|
private int $ttl;
|
|
|
|
public function __construct(?string $secret = null, ?int $ttlSeconds = null)
|
|
{
|
|
$this->secret = $secret ?: (string) env('JWT_SECRET', 'change-me-in-env');
|
|
$this->ttl = ($ttlSeconds !== null && $ttlSeconds > 0) ? $ttlSeconds : self::DEFAULT_TTL;
|
|
}
|
|
|
|
public function createToken(array $payload): string
|
|
{
|
|
$now = time();
|
|
$data = array_merge($payload, [
|
|
'iat' => $now,
|
|
'exp' => $now + $this->ttl,
|
|
'ctx' => self::TOKEN_CONTEXT,
|
|
]);
|
|
|
|
return $this->jwtEncode($data, $this->secret);
|
|
}
|
|
|
|
public function parseToken(?string $token): ?array
|
|
{
|
|
if (! is_string($token) || $token === '') {
|
|
return null;
|
|
}
|
|
|
|
$payload = $this->jwtDecode($token, $this->secret);
|
|
if (! is_array($payload)) {
|
|
return null;
|
|
}
|
|
|
|
if (($payload['ctx'] ?? null) !== self::TOKEN_CONTEXT) {
|
|
return null;
|
|
}
|
|
|
|
$exp = (int) ($payload['exp'] ?? 0);
|
|
if ($exp > 0 && $exp < time()) {
|
|
return null;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
private function jwtEncode(array $payload, string $secret): string
|
|
{
|
|
$header = ['typ' => 'JWT', 'alg' => 'HS256'];
|
|
$segments = [];
|
|
$segments[] = $this->base64UrlEncode(json_encode($header, JSON_UNESCAPED_SLASHES));
|
|
$segments[] = $this->base64UrlEncode(json_encode($payload, JSON_UNESCAPED_SLASHES));
|
|
$signingInput = implode('.', $segments);
|
|
|
|
$signature = hash_hmac('sha256', $signingInput, $secret, true);
|
|
$segments[] = $this->base64UrlEncode($signature);
|
|
|
|
return implode('.', $segments);
|
|
}
|
|
|
|
private function jwtDecode(string $token, string $secret): ?array
|
|
{
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 3) {
|
|
return null;
|
|
}
|
|
|
|
[$header64, $payload64, $signature64] = $parts;
|
|
$header = json_decode($this->base64UrlDecode($header64), true);
|
|
$payload = json_decode($this->base64UrlDecode($payload64), true);
|
|
$signature = $this->base64UrlDecode($signature64);
|
|
|
|
if (! is_array($header) || ! is_array($payload)) {
|
|
return null;
|
|
}
|
|
|
|
$signingInput = $header64.'.'.$payload64;
|
|
if (($header['alg'] ?? '') !== 'HS256') {
|
|
return null;
|
|
}
|
|
|
|
$expected = hash_hmac('sha256', $signingInput, $secret, true);
|
|
if (! hash_equals($expected, $signature)) {
|
|
return null;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
private function base64UrlEncode(string $data): string
|
|
{
|
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
|
}
|
|
|
|
private function base64UrlDecode(string $data): string
|
|
{
|
|
$padding = strlen($data) % 4;
|
|
if ($padding > 0) {
|
|
$data .= str_repeat('=', 4 - $padding);
|
|
}
|
|
|
|
return base64_decode(strtr($data, '-_', '+/')) ?: '';
|
|
}
|
|
}
|