|null * } */ public static function diagnose(string $token, string $secret, array $options = []): array { $parts = explode('.', $token); if (count($parts) !== 3) { return self::failure('malformed_token'); } [$headerB64, $payloadB64, $signatureB64] = $parts; $expected = self::base64UrlEncode( hash_hmac('sha256', "$headerB64.$payloadB64", $secret, true) ); if (! hash_equals($expected, $signatureB64)) { return self::failure('invalid_signature', null, self::decodePayload($payloadB64)); } $header = json_decode(self::base64UrlDecode($headerB64), true); $payload = json_decode(self::base64UrlDecode($payloadB64), true); if (! is_array($header) || ! is_array($payload)) { return self::failure('invalid_json'); } if (($header['alg'] ?? '') !== 'HS256') { return self::failure('unsupported_algorithm', $payload); } $issuer = $options['issuer'] ?? null; if ($issuer !== null && ($payload['iss'] ?? '') !== $issuer) { return self::failure('issuer_mismatch', $payload); } $audience = $options['audience'] ?? null; if ($audience !== null && ($payload['aud'] ?? '') !== $audience) { return self::failure('audience_mismatch', $payload); } $leeway = (int) ($options['leeway'] ?? 0); $now = time(); if (($payload['exp'] ?? 0) < ($now - $leeway)) { return self::failure('token_expired', $payload); } if (($payload['nbf'] ?? 0) > ($now + $leeway)) { return self::failure('token_not_yet_valid', $payload); } return [ 'valid' => true, 'reason' => null, 'payload' => $payload, 'claims' => self::summarizeClaims($payload), ]; } public static function isLaravelEncryptedSecret(?string $secret): bool { if (! is_string($secret) || $secret === '') { return false; } $decoded = base64_decode($secret, true); if ($decoded === false) { return false; } $json = json_decode($decoded, true); return is_array($json) && array_key_exists('iv', $json) && array_key_exists('value', $json) && array_key_exists('mac', $json); } /** * @return array|null */ protected static function decodePayload(string $payloadB64): ?array { $payload = json_decode(self::base64UrlDecode($payloadB64), true); return is_array($payload) ? $payload : null; } /** * @param array|null $payload * @return array{valid: false, reason: string, payload: null, claims: array|null} */ protected static function failure(string $reason, ?array $payload = null, ?array $claims = null): array { return [ 'valid' => false, 'reason' => $reason, 'payload' => null, 'claims' => $claims ?? ($payload ? self::summarizeClaims($payload) : null), ]; } /** * @param array $payload * @return array */ protected static function summarizeClaims(array $payload): array { return [ 'iss' => $payload['iss'] ?? null, 'aud' => $payload['aud'] ?? null, 'exp' => $payload['exp'] ?? null, 'nbf' => $payload['nbf'] ?? null, 'ic_number' => $payload['ic_number'] ?? null, 'member_number' => $payload['member_number'] ?? null, ]; } protected static function base64UrlDecode(string $data): string { $remainder = strlen($data) % 4; if ($remainder) { $data .= str_repeat('=', 4 - $remainder); } return base64_decode(strtr($data, '-_', '+/'), true) ?: ''; } protected static function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } }