1

In my implementation, it seems as if QNetworkAccessManager caches the credentials used for a specific url, after successful authentication.

A subsequent request to the same url using different credentials will succeed, but only because it uses the cached credentials instead of the new ones (the authenticationRequired signal is not emitted).

Is there some way in (Py)Qt4 (also PyQt 5.6) to clear the cache or otherwise force an update of the cached credentials? Should I just delete the manager and construct a new one? Or is there something else I am doing wrong?

Qt 5 offers a clearAccessCache() method, but Qt 4 does not, as far as I can see.

This small example illustrates the problem:

import sys
import json
from PyQt4 import QtNetwork, QtGui, QtCore

def show_reply_content(reply):
    content = json.loads(str(reply.readAll()))
    if 'authenticated' in content and content['authenticated']:
        print 'authentication successful for user {}'.format(
            content['user'])
        reply.manager().replies_authenticated += 1

    # Quit when all replies are finished
    reply.deleteLater()
    reply.manager().replies_unfinished -= 1
    if not reply.manager().replies_unfinished:
        print 'number of successful authentications: {}'.format(
            reply.manager().replies_authenticated)
        app.quit()


def provide_credentials(reply, authenticator):
    print 'Setting credentials for {}:\nusername: {}\n' \
          'password: {}\n\n'.format(reply.url(),
                                    reply.credentials['username'],
                                    reply.credentials['password'])
    authenticator.setUser(reply.credentials['username'])
    authenticator.setPassword(reply.credentials['password'])

# Some initialization
app = QtGui.QApplication(sys.argv)
manager = QtNetwork.QNetworkAccessManager()
manager.finished.connect(show_reply_content)
manager.authenticationRequired.connect(provide_credentials)
manager.replies_unfinished = 0
manager.replies_authenticated = 0

# Specify credentials
all_credentials = [dict(username='aap',
                        password='njkrfnq'),
                   dict(username='noot',
                        password='asdfber')]

# url authenticates successfully only for the specified credentials
url = 'http://httpbin.org/basic-auth/{}/{}'.format(
    *all_credentials[1].values())

# Schedule requests
replies = []
for credentials in all_credentials:
    replies.append(
        manager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(url))))

    # Add credentials as dynamic attribute
    replies[-1].credentials = credentials

    manager.replies_unfinished += 1

# Start event loop
app.exec_()

As soon as one request is successfully authenticated, the next one is as well, even though it should not be.

djvg
  • 11,722
  • 5
  • 72
  • 103
  • What specific version of Qt4 are you using? This bug seems quite similar: [QTBUG-15566](https://bugreports.qt.io/browse/QTBUG-15566). – ekhumoro Sep 01 '17 at 16:01
  • Also related: [QNetworkAccessManager - how to enforce authentication?](http://thread.gmane.org/gmane.comp.lib.qt.user/3141) – ekhumoro Sep 01 '17 at 16:21
  • Not sure about the Qt version, but I'm using PyQt4.11.4. I still have to look into that bug report, but the second link pointed me towards `QNetworkRequest.AuthenticationReuseAttribute`, which sounds promising. Unfortunately, my first attempt, using `request.setAttribute(QNetworkRequest.AuthenticationReuseAttribute, QNetworkRequest.Manual)`, did not work: Now all requests are denied with a 401. I'll dive into this later. – djvg Sep 01 '17 at 21:27
  • @ekhumoro, the bug mentioned above does look similar in that the cached credentials are used. But the behavior persists, also when I use PyQt 5.6. A quick check using WIreshark suggests that, as soon as a valid set of credentials has been found (as indicated by a status 200), these will be used for any subsequent requests to that same url, as well as for any pending requests (the manager keeps sending these as long as they receive a 401), and the `authorizationRequired` signal is not emitted again until a request to a different url (also requiring authorization) is made. – djvg Sep 04 '17 at 13:19
  • Perhaps [this](https://stackoverflow.com/a/1700751/4720018) is the way to go? – djvg Sep 04 '17 at 13:20
  • I'm afraid I don't really have much experience of using these particular APIs, so I'm reluctant to comment further, given the security concerns. Have you done a thorough search of the [Qt Bug Tracker](https://bugreports.qt.io/secure/Dashboard.jspa)? I'm surprised that this is still an issue in Qt5. Surely there would have been other bug reports by now, if it was still a genuine problem? – ekhumoro Sep 04 '17 at 14:15
  • Apparently I keep running into obscure problems. This ancient report sounds exactly like my issue: [QTBUG-16835](https://bugreports.qt.io/browse/QTBUG-16835). The comment: "... I am not sure we need new API for this, as nobody has asked for it before." I did not expect this scenario to be considered so exotic. – djvg Sep 04 '17 at 14:39
  • It's disappointing that this issue has still not been addressed properly. Hopefully this is the end of your run of bad luck ;-) – ekhumoro Sep 04 '17 at 15:57
  • @ekhumoro, I hope so too. :-) Thanks again for all your help. – djvg Sep 04 '17 at 17:11

1 Answers1

1

As it turns out there are several similar Qt bug reports that are still open: QTBUG-16835, QTBUG-30433...

I guess it should be possible to use QNetworkRequest.AuthenticationReuseAttribute, but I could not make that work with multiple requests to the same url with different (valid) sets of credentials.

A possible workaround would be to forget all about the authenticationRequired signal/slot, and add a raw header (RFC 7617) to each request, exactly as described here.

In the example that would mean removing the authenticationRequired signal/slot connection and replacing the manager.get() part by the following lines (for Python 2.7 with PyQt4):

request = QtNetwork.QNetworkRequest(QtCore.QUrl(url))
header_data = QtCore.QByteArray('{}:{}'.format(
    credentials['username'], credentials['password'])).toBase64()
request.setRawHeader('Authorization', 'Basic {}'.format(header_data))
replies.append(manager.get(request))

However, as @ekhumoro pointed out in the comments above: not sure how safe this is.

Community
  • 1
  • 1
djvg
  • 11,722
  • 5
  • 72
  • 103