public function authenticate ($appId, $secretKey, StorageInterface $storage = null)
    {
        // Default to file storage.
        if (! $storage) {
            $storage = new File(sys_get_temp_dir());
        }

        $hash = $storage->hash($appId, $secretKey);
        $cached = $storage->retrieve($hash);

        // Cached access token is still valid. Return it.
        if ($cached instanceof AccessToken && $cached->valid()) {
            return $cached;
        }

        $request = new Request(
            'GET',
            "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appId}&secret={$secretKey}"
        );
        $response = $this->client->send($request);
        $json = json_decode((string)$response->getBody());

        if (isset($json->access_token, $json->expires_in)) {
            $token = new AccessToken(
                $json->access_token,
                \DateTime::createFromFormat('U', time() + $json->expires_in)
            );

            $storage->store($hash, $token);

            return $this->client->withAccessToken($token);
        }
        
        throw new Exception("bad response: expected properties `access_token`, `expires_in`");
    }