Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
38 / 40
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
JwtAuth
95.00% covered (success)
95.00%
38 / 40
71.43% covered (warning)
71.43%
5 / 7
13
0.00% covered (danger)
0.00%
0 / 1
 createToken
100.00% covered (success)
100.00%
7 / 7
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
 getUserIdFromCookie
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 setAuthCookie
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 clearAuthCookie
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 clearCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSecret
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
1<?php
2
3use Firebase\JWT\JWT;
4use Firebase\JWT\Key;
5
6/**
7 * JWT-based stateless authentication.
8 *
9 * Replaces session-based authentication with signed JWT tokens stored in cookies.
10 * Tokens contain user ID and expiration, signed with HMAC-SHA256.
11 *
12 * Usage:
13 *   JwtAuth::setAuthCookie($userId)      // Login: create token and set cookie
14 *   JwtAuth::getUserIdFromCookie()       // Check auth: get user ID from cookie
15 *   JwtAuth::clearAuthCookie()           // Logout: delete cookie
16 */
17class JwtAuth
18{
19    private const COOKIE_NAME = 'auth_token';
20    private const ALGORITHM = 'HS256';
21    private const DEFAULT_EXPIRY_SECONDS = 86400 * 30; // 30 days
22
23    /**
24     * @var int|null Cached user ID from token validation
25     */
26    private static ?int $cachedUserId = null;
27
28    /**
29     * Create a JWT token for the given user ID.
30     *
31     * @param int $userId The user ID to encode
32     * @param int|null $expirySeconds Seconds until expiration (null = default 30 days)
33     * @return string The JWT token
34     */
35    public static function createToken(int $userId, ?int $expirySeconds = null): string
36    {
37        $expiry = $expirySeconds ?? self::DEFAULT_EXPIRY_SECONDS;
38
39        $payload = [
40            'sub' => $userId,
41            'iat' => time(),
42            'exp' => time() + $expiry,
43        ];
44
45        return JWT::encode($payload, self::getSecret(), self::ALGORITHM);
46    }
47
48    /**
49     * Validate a JWT token and return the user ID.
50     *
51     * @param string $token The JWT token to validate
52     * @return int|null The user ID, or null if token is invalid/expired
53     */
54    public static function validateToken(string $token): ?int
55    {
56        try
57        {
58            $decoded = JWT::decode($token, new Key(self::getSecret(), self::ALGORITHM));
59            return (int) $decoded->sub;
60        }
61        catch (\Exception $e)
62        {
63            return null;
64        }
65    }
66
67    /**
68     * Get user ID from auth cookie.
69     *
70     * @return int|null The user ID, or null if not authenticated
71     */
72    public static function getUserIdFromCookie(): ?int
73    {
74        if (self::$cachedUserId !== null)
75            return self::$cachedUserId;
76
77        if (!isset($_COOKIE[self::COOKIE_NAME]) || empty($_COOKIE[self::COOKIE_NAME]))
78            return null;
79
80        self::$cachedUserId = self::validateToken($_COOKIE[self::COOKIE_NAME]);
81        return self::$cachedUserId;
82    }
83
84    /**
85     * Set auth cookie with JWT token for the given user ID.
86     *
87     * @param int $userId The user ID
88     * @return string The token that was set
89     */
90    public static function setAuthCookie(int $userId): string
91    {
92        $token = self::createToken($userId);
93        $_COOKIE[self::COOKIE_NAME] = $token;
94        setcookie(
95            self::COOKIE_NAME,
96            $token,
97            time() + self::DEFAULT_EXPIRY_SECONDS,
98            '/',
99            '',
100            true, // secure
101            true  // httpOnly
102        );
103        self::$cachedUserId = $userId;
104        return $token;
105    }
106
107    /**
108     * Clear auth cookie (logout).
109     */
110    public static function clearAuthCookie(): void
111    {
112        unset($_COOKIE[self::COOKIE_NAME]);
113        setcookie(self::COOKIE_NAME, '', time() - 3600, '/');
114        self::$cachedUserId = null;
115    }
116
117    /**
118     * Clear cached user ID (for testing).
119     */
120    public static function clearCache(): void
121    {
122        self::$cachedUserId = null;
123    }
124
125    /**
126     * Get the JWT secret key.
127     */
128    private static function getSecret(): string
129    {
130        $secret = Configure::read('Security.jwtSecret');
131        if (!$secret)
132        {
133            // Fall back to Security.salt if jwtSecret not configured
134            $secret = Configure::read('Security.salt');
135        }
136        if (!$secret)
137            throw new Exception('No JWT secret configured. Set Security.jwtSecret or Security.salt.');
138
139        return $secret;
140    }
141}