Daniel Schreij
Daniel Schreij

Reputation: 763

Implicit OAuth2 grant with PyQt4/5

I have been working on a python app that uses OAuth2 to identify users. I seem to have successfully implemented the workflow of an OAuth2 implicit grant (commonly used for installed and user-agent apps), but at the last step of receiving the token, something appears to be going wrong.

Whenever the user needs to authenticate, a PyQt QWebView window (based on webkit) is spawned which shows the login page. After the user has logged in and allowed the scoped permissions for my app, the OAuth2 server redirects to the prespecified redirect_uri.

The problem is that when using the QWebView browser, the token string, normally occurring after the #, seems to have been dropped from the URL: the URL that QWebView returns is just the base redirect_uri.

If I copy paste the OAuth authorization URL and follow through these same steps of logging in and authorizing in a normal web browser such as Chrome or Firefox, I do get to see the redirect_uri including the token string, so the problem does not lie in the OAuth2 process, but must go wrong somewhere at the implementation at my side.

Is this behavior inherent to the implementation of QWebView or webkit? I am reading out the QUrl incorrectly?

For completeness, here is my code:

osf.py module that generates the OAuth2 URLs for the Open Science Framework.

# Import basics
import sys
import os

# Module for easy OAuth2 usage, based on the requests library,
# which is the easiest way to perform HTTP requests.

# OAuth2Session object
from requests_oauthlib import OAuth2Session
# Mobile application client that does not need a client_secret
from oauthlib.oauth2 import MobileApplicationClient

#%%----------- Main configuration settings ----------------
client_id = "cbc4c47b711a4feab974223b255c81c1"
# TESTED, just redirecting to Google works in normal browsers
# the token string appears in the url of the address bar
redirect_uri = "https://google.nl"

# Generate correct URLs
base_url = "https://test-accounts.osf.io/oauth2/"
auth_url = base_url + "authorize"
token_url = base_url + "token"
#%%--------------------------------------------------------

mobile_app_client = MobileApplicationClient(client_id)

# Create an OAuth2 session for the OSF
osf_auth = OAuth2Session(
    client_id, 
    mobile_app_client,
    scope="osf.full_write", 
    redirect_uri=redirect_uri,
)

def get_authorization_url():
    """ Generate the URL with which one can authenticate at the OSF and allow 
    OpenSesame access to his or her account."""
    return osf_auth.authorization_url(auth_url)

def parse_token_from_url(url):
    token = osf_auth.token_from_fragment(url)
    if token:
        return token
    else:
        return osf_auth.fetch_token(url)

The main program, that opens up a QWebView browser window with login screen

# Oauth2 connection to OSF
import off
import sys
from PyQt4 import QtGui, QtCore, QtWebKit

class LoginWindow(QtWebKit.QWebView):
    """ A Login window for the OSF """

    def __init__(self):
        super(LoginWindow, self).__init__()
        self.state = None
        self.urlChanged.connect(self.check_URL)

    def set_state(self,state):
        self.state = state

    def check_URL(self, url):
        #url is a QUrl object, covert it to string for easier usage
        url_string = url.toEncoded()
        print(url_string)

        if url.hasFragment():
            print("URL CHANGED: On token page: {}".format(url))
            self.token = osf.parse_token_from_url(url_string)
            print(self.token)
        elif not osf.base_url in url_string:
            print("URL CHANGED: Unexpected url")

if __name__ == "__main__":
    """ Test if user can connect to OSF. Opens up a browser window in the form
    of a QWebView window to do so."""
    # Import QT libraries

    app = QtGui.QApplication(sys.argv)
    browser = LoginWindow()

    auth_url, state = osf.get_authorization_url()
    print("Generated authorization url: {}".format(auth_url))

    browser_url = QtCore.QUrl.fromEncoded(auth_url)
    browser.load(browser_url)
    browser.set_state(state)
    browser.show()

    exitcode = app.exec_()
    print("App exiting with code {}".format(exitcode))
    sys.exit(exitcode)

Basically, the url that is provided to the check_URL function by the QWebView's url_changed event never contains the OAuth token fragment when coming back from the OAuth server, whatever I use for redirect_uri (in this example I simply redirect to google for the sake of simplicity).

Could anyone please help me with this? I have exhausted my option of where to look for a solution to this problem.

Upvotes: 2

Views: 1355

Answers (1)

Daniel Schreij
Daniel Schreij

Reputation: 763

This appears to be a known bug in Webkit/Safari:

https://bugs.webkit.org/show_bug.cgi?id=24175 https://phabricator.wikimedia.org/T110976#1594914

Basically it is not fixed because people do not agree on what the desired behavior should be according to the HTTP specification. A possible fix is described at How do I preserve uri fragment in safari upon redirect? but I have not been able to test this.

EDIT

I have managed to find a (not-so elegant) work around to solve this problem. Instead of using the urlChanged event from QWebView (which shows nothing of the 301 redirects done by the OAuth server), I have used QNetworkAccessManager's finished() event. This gets fired after any http request is finished (so also for all the linked content of page such as images, stylesheets and the such, so you have to do a lot of filtering).

So now my code looks like this:

class LoginWindow(QtWebKit.QWebView):
    """ A Login window for the OSF """
    # Login event is emitted after successfull login
    logged_in = QtCore.pyqtSignal(['QString'])  

    def __init__(self):
        super(LoginWindow, self).__init__()

        # Create Network Access Manager to listen to all outgoing
        # HTTP requests. Necessary to work around the WebKit 'bug' which
        # causes it drop url fragments, and thus the access_token that the
        # OSF Oauth system returns
        self.nam = self.page().networkAccessManager()

        # Connect event that is fired if a HTTP request is completed.
        self.nam.finished.connect(self.checkResponse)

    def checkResponse(self,reply):
        request = reply.request()
        # Get the HTTP statuscode for this response
        statuscode = reply.attribute(request.HttpStatusCodeAttribute)
        # The accesstoken is given with a 302 statuscode to redirect

        if statuscode == 302:
            redirectUrl = reply.attribute(request.RedirectionTargetAttribute)
            if redirectUrl.hasFragment():
                r_url = redirectUrl.toString()
                if osf.redirect_uri in r_url:
                    print("Token URL: {}".format(r_url))
                    self.token = osf.parse_token_from_url(r_url)
                    if self.token:
                        self.logged_in.emit("login")
                        self.close()

Upvotes: 2

Related Questions