Login system in PHP using AJAX
Your querying practices look secure to me.  Your are using a prepared statement with bound parameters and using password_hash() and password_verify().  It really can be as simple as doing those basic/essential things.
As for other refinements, I recommend writing the failure branches before successful ones, doing early exit()s, and only passing back an empty or populated reason to reduce the data structure to its vital value.

I will assume/hope that the username is a UNIQUE table column, so LIMIT 1 provides no value.
If you want the result set / row to be an object, just tell pdo that that is what you want -- fetch(PDO::FETCH_OBJ).
The result set will either be an object or false, so just check for a falsey result set.  For this reason, you can remove id from the SELECT.
I don't like to give too much specificity when giving failed login responses.  I would tell the user that the credentials generally failed without spelling out which field was the problem.

Recommendation:
public function login(Request $request)
{
    $this-&gt;middleware('json_response', $request);
    $this-&gt;middleware('xauth_protected', $request);
    
    if(!$request-&gt;has('username') || !$request-&gt;has('password')) {
        exit(json_encode(['reason' =&gt; 'Please fill in the required fields']);
    }
    
    $stmt = Database::getInstance()-&gt;Prepare(
        &quot;SELECT hash, session FROM _users WHERE username = ?&quot;
    );
    $stmt-&gt;execute([$request-&gt;username]);
    $rowObject = $stmt-&gt;fetch(PDO::FETCH_OBJ);
    
    if (!$rowObject || !password_verify($request-&gt;password, $rowObject-&gt;hash)) {
        exit(json_encode(['reason' =&gt; 'Invalid credentials']));
    }

    $request-&gt;setSession('oauth', $rowObject-&gt;session);
    exit(json_encode(['reason' =&gt; null]));
}

Then in your js, you can use this:
.then(data =&gt; {
    if (data.reason) {
        document.getElementById('login-error').innerHTML = data.reason;
    } else {
        window.location.href = App.__viewFactory.dashboard.view;
    }
})