Reputation: 763
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
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.
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