Implicit OAuth2 grant with PyQt4/5
Question: 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.


Answer: 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?](http://stackoverflow.com/questions/17982594/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()
    

