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