Implementing OAuth using PyQt5 (Google, Facebook, Apple) using QWebEngine
By
John Phung

There are a few ways to implement social authentication/sign in for a desktop application built using Qt5 and 6.
One of them is using the inbuilt QOAuth2AuthorizationFlow method that hides a lot of the authorization process complexity and exposes a local server to listen for redirect responses.
Another is using Custom Protocol Handlers to setup a dedicated route locally that the application can listen in on for auth responses.
And the one I will focus on in this article is using QWebEngine which is a inbuilt chromium browser to listen to URL changes made from the social auth provider that contain authentication information. The reason being:
- Easiest to implement that I’ve found
- QOAuth2AuthorizationFlow does not work for social auth providers that do not allow localhost redirect URI (Facebook, Apple). Not to mention that the Qt documentation for OAuth seems outdated at this time of writing.
- Not a simple way to do cross platform Custom Protocol Handlers (i.e Windows and Mac OS) for PyQt5 applications (Registering info.plist and working with registers)
I will describe the process of setting up the QWebEngine for OAuth below for PyQt5
Construct QWebEngine Class
Create Social Auth Class for example Class AppleWrapper which inherits QWebEngine. You will call & initialise this class and connect the login signal to a method which suits your application.
1import logging2import uuid3import requests45from urllib.parse import parse_qs, urlparse6from PyQt6.QtWebEngineWidgets import QWebEngineView7from PyQt6.QtCore import QUrl, pyqtSignal, pyqtSlot89logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')1011class AppleWrapper(QWebEngineView):1213 authenticated = pyqtSignal(object)14 auth_url = ""1516 def __init__(self):17 super(AppleWrapper, self).__init__()18 self.nam = self.page()19 self.setUrl(auth_url)20 self.show()21 self.urlChanged.connect(self._interceptUrl)
To explain the code snippet above, I created a new class which inherits QWebEngine, set the url that the browser will open to be auth_url, show the browser and connect the url signal changes to self._intercept_url which is yet to be defined.
For Apple auth, the auth_url variable is the login url constructed based on
- client_id — obtained via Apple developer portal for apple sign in
- redirect_uri — a https:// domain for the apple callback
- response_type = “code id_token”
- scope = “name email”
- state = JSON stringified object or string for security reasons
- nouce = random number/string
1auth_url = QUrl(f'https://appleid.apple.com/auth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type={response_type}&scope={scope}&response_mode={response_mode}&state={state}&nouce={nouce}')
Similarly for other social auth, you will construct the login url a little bit different based on what is required according to their documentation.
Defining Intercept URL Method
This is where the magic happens. On successfully auth request, the social provider will redirect the url to the provided redirect_uri containing the necessary user information in the query parameters/fragment.
Using the intercept url method, we can parse the url and extract the required information such as user info, authorization code et al.
We will add a intercept_url method to the AppleWrapper class as follows:
1def _intercept_url(self, url):2 try:3 parsed_url = url_parse(url.toString())4 id_token = parse_qs(parsed_url.query)['id_token'][0]5 first_name = parse_qs(parsed_url.query)['firstName'][0]6 last_name = parse_qs(parsed_url.query)['lastName'][0]7 #self.auth_login(id_token)8 except Exception as e:9 logging.error(e)
Keep in mind that the intercept_url method will look different for different social auth as the query parameters returned will look different, so basically you will want to inspect the parsed_url dictionary return and look for what you want to extract out of it.
Login/Creating user to your backend (Optional)
You may have noticed I commented out a self.auth_login method in the code block above, because after receiving validation that this user exists from the social auth provider, we can simply carry on with the application flow without doing additional authentication steps.
However, if we want to ‘sign in’ or ‘login’ a user into our existing web authentication system from the desktop app, then we want to take the id_token and call the necessary API endpoints.
The complete code for the Apple Wrapper is found below. I have also included a login to my web auth and a signal to emit the user information to be used elsewhere in the application.
1import logging2import uuid3import requests45from urllib.parse import parse_qs, urlparse6from PyQt6.QtWebEngineWidgets import QWebEngineView7from PyQt6.QtCore import QUrl, pyqtSignal, pyqtSlot8910logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')11121314class AppleWrapper(QWebEngineView):1516 auth_url = QUrl(f'https://appleid.apple.com/auth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type={response_type}&scope={scope}&response_mode={response_mode}&state={state}&nouce={nouce}')17 authenticated = pyqtSignal(object)1819 def __init__(self):20 super(AppleWrapper, self).__init__()21 self.nam = self.page()22 self.setUrl(auth_url)23 self.show()24 self.urlChanged.connect(self._interceptUrl)2526 def _interceptUrl(self, url):27 try:28 parsed_url = urlparse(url.toString())29 #logging.info(parsed_url)30 id_token = parse_qs(parsed_url.query)['id_token'][0]31 first_name = parse_qs(parsed_url.query)['firstName'][0]32 last_name = parse_qs(parsed_url.query)['lastName'][0]33 self.apple_login(id_token, first_name, last_name)3435 except Exception as e:36 logging.error(e)3738 def apple_login(self, id_token, first_name, last_name):39 try:40 url = 'auth/apple-login'41 data = {'id_token': id_token, "first_name": first_name, 'last_name': last_name}42 response = requests.post(url, json=data)43 res_json = response.json()44 email = res_json['user']['email']45 name = f"""{res_json['user']['first_name']} {res_json['user']['last_name']}"""4647 self.emit_auth(name, email)48 self.close()4950 except Exception as e:51 logging.error(e)525354 @pyqtSlot()55 def emit_auth(self, name, email):56 self.authenticated.emit({'name':name, 'email': email})
Several Gotcha’s using QWebEngine
Several issues I have encountered working with QWebEngine
- PyQtWebEngine is a separate pip module to be installed separately from PyQt5
- PyQt5 WebEngine does not work on apple silicon. You must install PyQt6 and PyQt6 WebEngine
- Compiling on Windows requires a qt.conf file in order to correctly look for the WebEngine files when compiling with pyinstaller
- Running application after pyinstaller compilation on Mac OS encounters missing file for translations for QWebEngine. I got around this by copying the qtwebengine_locales and all files in this folder (PyQt5/lib/QtWebEngine installation folder) into the .app/Content/MacOS/ folder
1cp -r path/to/QtWebEngine .app/Contents/Resources