Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
JwtService
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
8 / 8
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 generateTokenPair
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 generateAccessToken
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 generateRefreshToken
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 validateToken
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 decodeToken
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getAccessTokenExpiry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRefreshTokenExpiry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Auth\Service;
6
7use App\Domain\Auth\Data\TokenData;
8use DateTimeImmutable;
9use Exception;
10use Firebase\JWT\JWT;
11use Firebase\JWT\Key;
12use RuntimeException;
13
14final class JwtService
15{
16    private readonly string $secret;
17    private readonly int $accessTokenExpiry;
18    private readonly int $refreshTokenExpiry;
19    private readonly string $issuer;
20
21    /**
22     * @param array<string, mixed> $settings
23     */
24    public function __construct(array $settings)
25    {
26        $this->secret = (string)$settings['secret'];
27        $this->accessTokenExpiry = (int)$settings['access_token_expiry'];
28        $this->refreshTokenExpiry = (int)$settings['refresh_token_expiry'];
29        $this->issuer = (string)($settings['issuer'] ?? 'flowstate-api');
30    }
31
32    public function generateTokenPair(int $userId, string $role): TokenData
33    {
34
35        return new TokenData(
36            accessToken: $this->generateAccessToken($userId, $role),
37            refreshToken: $this->generateRefreshToken($userId),
38            expiresIn: $this->accessTokenExpiry,
39        );
40    }
41
42    public function generateAccessToken(int $userId, string $role): string
43    {
44        $now = new DateTimeImmutable();
45
46        $payload = [
47            'iss' => $this->issuer,
48            'iat' => $now->getTimestamp(),
49            'exp' => $now->getTimestamp() + $this->accessTokenExpiry,
50            'sub' => $userId,
51            'userId' => $userId,
52            'role' => $role,
53            'type' => 'access',
54        ];
55
56        return JWT::encode($payload, $this->secret, 'HS256');
57    }
58
59    public function generateRefreshToken(int $userId): string
60    {
61        $now = new DateTimeImmutable();
62
63        $payload = [
64            'iss' => $this->issuer,
65            'iat' => $now->getTimestamp(),
66            'exp' => $now->getTimestamp() + $this->refreshTokenExpiry,
67            'sub' => $userId,
68            'userId' => $userId,
69            'type' => 'refresh',
70            'jti' => bin2hex(random_bytes(16)),
71        ];
72
73        return JWT::encode($payload, $this->secret, 'HS256');
74    }
75
76    /**
77     * @param string $token
78     * @return array<string, mixed>|null
79     */
80    public function validateToken(string $token): ?array
81    {
82        try {
83            $decoded = JWT::decode($token, new Key($this->secret, 'HS256'));
84
85            return (array)$decoded;
86        } catch (Exception $e) {
87            return null;
88        }
89    }
90
91    public function decodeToken(string $token): object
92    {
93        try {
94            return JWT::decode($token, new Key($this->secret, 'HS256'));
95        } catch (Exception $e) {
96            throw new RuntimeException('Invalid token: ' . $e->getMessage());
97        }
98    }
99
100    public function getAccessTokenExpiry(): int
101    {
102        return $this->accessTokenExpiry;
103    }
104
105    public function getRefreshTokenExpiry(): int
106    {
107        return $this->refreshTokenExpiry;
108    }
109}