from Guide to Hacking on Jun 25, 2023

How third-party login for desktop apps work

At some point, you've likely logged into a desktop app using a single sign on — again such as Google, Facebook, or Twitter. Here is how that desktop login worked and how to build one yourself.

This used to be a seamless experience that was contained in your desktop app end-to-end. However, this was a security risk: Malicious apps could capture your login information with a faked login page.

To combat this, login services started requiring "supported browsers" such as Chrome or Firefox, and this is where implementing third-party logins started to get more complicated. This process is awful confusing, so let's break down how it works and how to build it.

How third-party login used to work

To recap, here's how third-party login within a browser alone works. We'll use the same example as in How third-party login works, where we login to Slack's web app using Google. There are two critical requirements that need to be satisfied, among others:

  1. Identify the app: Slack must include information that identifies itself, when sending you to Google's signin page.
  2. Receive authorization token: Google must somehow send a special token back to the Slack web app.

To satisfy these requirements, Slack follows this process for third-party login in a web browser:

  1. You click "login" on slack.com. This satisfies requirement #1. At this point, Slack builds a Google login URL that includes information about the requestor — i.e., Slack.
  2. Your browser navigates you to accounts.google.com/signin.
  3. You login normally.
  4. You grant permission to Slack to access your information.
  5. You are redirected back to slack.com. This satisfies requirement #2. The redirect back to Slack includes the special token.
  6. Slack now uses that special token to ask Google for your information, such as your name and email address.

Let's now implement this flow in a desktop application. For this section only, we'll leverage a unique property of the pywebview Python-only desktop application — namely, the entire application is a webview. This allows us to redirect to Google completely within the application.

Create a new directory to house your project. I will create one on my desktop.

mkdir ~/Desktop/desktoplogin
cd ~/Desktop/desktoplogin

In this new directory, create a new virtual environment.

python -m venv env
source env/bin/activate

We now need to install the GUI library and web framework packages. Since our list of requirements is growing longer, add your requirements to a file named requirements.txt.

desktoplogin/requirements.txt

Flask==2.3.2
simple-flask-google-login==0.3.0
pywebview==4.2.2
cryptography==41.0.1

Then, install your requirements.

pip install -r requirements.txt

Now, start from our previous app.py in How third-party login works. Add a few extra lines at the bottom of the file to wrap your web server in a desktop application. Your file should then match the following.

desktoplogin/app.py

from flask import Flask, session
from simple_flask_google_login import SimpleFlaskGoogleLogin
import webview


app = Flask("Google Login App")
app.secret_key = "YourSecretKeyHere"  # Secret key is needed for OAuth 2.0
login = SimpleFlaskGoogleLogin(app)


@app.route("/")
def index():
    if 'name' in session:
        return f"Hello {session['name']}! <a href='/logout'>Logout</a>"
    return f"<a href='/login'>Login</a>"


if __name__ == '__main__':
    keyfile, certfile = webview.generate_ssl_cert()
    ssl = {'keyfile': keyfile, 'certfile': certfile}
    webview.create_window('Flask example', app, http_port=5000, server_args=ssl)
    webview.start()

Optionally, check against our reference implementation. Launch the desktop application with the following

python app.py

You should then see a desktop application like the following popup.

icon

Click Login and follow the onscreen prompts.

icon

Once logged in, you should now see a confirmation screen, showing a successful login.

icon

At this point, you've completed your very first desktop application with third-party login. With that said, you'll notice this flow is different from other desktop applications. Other applications will redirect you to your browser, for security and for convenience. Let's implement this more secure option now.

How third-party login now works on desktop

You've likely gone through this flow multiple times, so the process will sound familiar. Let's say you're trying to sign into Slack using your Google account; your default browser is Chrome. There are four requirements:

  1. Identify the app: Slack must include information that identifies itself, when sending you Google's signin page.
  2. Use a browser: User must login in a secure, pre-approved browser such as Chrome, rather than in an unsafe embedded browser
  3. Catch non-users: Slack wants to catch users that may have entered the login flow without installing the application.
  4. Receive authorization token: Google must somehow send a special login code back to the Slack desktop app.

Here is the flow from your perspective as the user. Additionally, here's how these steps address the requirements above.

  1. Click "login" inside of the Slack desktop app. This satisfies requirement #1. Slack builds a URL that contains information about itself.
  2. This pops you out of Slack and into Chrome, which opens Google's login page. This satisfies requirement #2. You are now signing in via secure, pre-approved browser.
  3. In Chrome, you login normally.
  4. In Chrome, you are redirected to the Slack website. This partially satisfies requirement #4, as the redirect request includes a special code. Now, the Slack website has access to the special code.
  5. A popup asks if you want to open a link in the Slack desktop app. This satisfies requirement #3. If the Slack desktop app is not installed, you will be prompted at this point to access the Slack web app instead.
  6. Once you confirm, you are redirected back to your Slack desktop app, and login has now been completed. This satisfies requirement #4. The redirect request again includes the same special login code. Now, the Slack desktop app has the special code too and can complete login.

Here's the process in more technical detail and which steps address which requirements above.

  1. You click "login" in the Slack desktop app.

    1. Slack app creates a login page URL using some application identifiers and a redirect URL like slack.com/success. This satisfies requirement #1.
  2. This pops you out of Slack and into Chrome, which opens the link generated above, accounts.google.com/signin?somedata=here.

    1. This satisfies requirement #2, where you must signin through a known, secure browser.
  3. In Chrome, you login normally.

  4. In Chrome, you are redirected back to slack.com/success?code=asdf. Notice the redirect contains a special code in the query parameter.

    1. This query parameter partially satisfies requirement #4, as the code has been passed from Google to slack.com/success. However, we now need to pass the code from slack.com/success to the Slack desktop app.
  5. In Chrome, slack.com/success asks if you want to open a link in Slack. This link is a deeplink like slack://signin?code=asdf that will pass the special code to your desktop app.

    1. This redirect satisfies requirement #3. If you logged in without the Slack app, then you won't see a popup. Instead, you'll see just this webpage slack.com/success, which asks if you want to signin to the web version of Slack.
  6. Once you confirm opening the deeplink, you'll be redirected back to the Slack app.

    1. This finally fully satisfies requirement #4, as you've now passed the special code back to the Slack desktop app via the deeplink slack://signin?code=asdf.
    2. Now, the Slack desktop uses the code to complete authentication with Google. From the user's perspective, signin is complete.

This completes our introduction to third-party logins on desktop. Now that you have the process down, let's augment our desktop application to follow this process.

How to build third-party login on desktop

To build this new flow into our desktop application, we need a few changes:

  1. Redirect the user to the browser: Upon clicking Login, the user should be redirected to the browser instead of loading a webpage within the desktop application.
  2. Intermediate webpage suggesting deeplink: We will setup an intermediate webpage, which Google will redirect to. This intermediate webpage will then prompt the user to open the deeplink if they have the desktop application installed. If the application is not installed, the user will be prompted to install it.
  3. Accept deeplink with login token: After the user confirms opening the deeplink, the user is redirected back to the desktop application. We need to setup the application to accept this custom deeplink and to process the login token contained in that deeplink.

Let's now effect each of these changes.

Step 1 — Redirect the user to the browser.

To do this, we'll leverage two existing features of pywebview:

  1. Any link with the target set to _blank will open in the browser.
  2. Use Python's built-in webbrowser.open (documentation), which opens links in browsers and is supported by pywebview.

In your main app.py, customize your login endpoint to open the web browser and to show a clickable link if browser-opening fails.

desktoplogin/app.py

from flask import Flask, session from simple_flask_google_login import SimpleFlaskGoogleLogin import webview
import webbrowser def url_handler(url): webbrowser.open(url) return f"If you aren't redirected automatically, click <a href='{url}' target='_blank'>here</a>"
app = Flask("Google Login App") app.secret_key = "YourSecretKeyHere" # Secret key is needed for OAuth 2.0 login = SimpleFlaskGoogleLogin(app, authorization_url_handler=url_handler)

Additionally, configure your application to use your custom handler.

desktoplogin/app.py

return f"If you aren't redirected automatically, click <a href='{url}' target='_blank'>here</a>" app = Flask("Google Login App") app.secret_key = "YourSecretKeyHere" # Secret key is needed for OAuth 2.0
login = SimpleFlaskGoogleLogin(app, authorization_url_handler=url_handler)
@app.route("/") def index(): if 'name' in session:

Check with our reference implementation. Launch the application to test this flow, using the following.

python app.py

Once the desktop application is launched, click on Login and you should be redirected to a Google login page in your browser. If you attempt to login, you'll be redirected to an invalid address. This is normal, and we'll update this redirect next.

Step 2 — Redirect to an intermediate webpage.

We'll now setup an intermediate webpage on Glitch. This webpage will then prompt the user to open a deeplink and complete login.

Click on this link to make a copy of our template redirect webpage: https://glitch.com/edit/#!/remix/desktop-login.

In your remixed project, you'll then find the following in index.html.

glitch/index.html

<html>
  <p>If you aren't redirected automatically, click <a href="" id="link">here</a>.</p>
  <script>
      window.onload = function() {
          const url = 'myapp://login' + window.location.search;
          document.getElementById('link').setAttribute('href', url);
          window.location.replace(url);
      }
  </script>
</html>

The above webpage prompts the user to open a deeplink, and if the automatic prompt fails to show, provides a clickable deeplink.

On the bottom left, click on Preview, then Preview in a new window. You should then see a webpage like the following, prompting you to open a deeplink.

icon

Copy the URL for your webpage. In our example above, the URL is https://grandiose-glow-golf.glitch.me. Amend your login configuration, with the following.

desktoplogin/app.py

return f"If you aren't redirected automatically, click <a href='{url}' target='_blank'>here</a>" app = Flask("Google Login App") app.secret_key = "YourSecretKeyHere" # Secret key is needed for OAuth 2.0
login = SimpleFlaskGoogleLogin( app, authorization_url_handler=url_handler, redirect_uri='https://desktop-login.glitch.me' )
@app.route("/") def index(): if 'name' in session:

Additionally, access your Oauth configuration page in your Google developer console's credentials page. Add this address to your list of Authorized Redirect URIs.

icon

At this point, we no longer need to serve the local application over https. Additionally, we'll be using PyQt shortly, whose web engine does not accept self-signed certificates2. Fortunately, since our redirect is an intermediate webpage with an https protocol, this change is acceptable3.

At this point, we also no longer need to fix the port to 5000; previously, we fixed the port so that we could construct redirect_uri using the local server's host URL. Now, the redirect_uri is a different server entirely.

Simplify your app configuration to the following.

desktoplogin/app.py

if 'name' in session: return f"Hello {session['name']}! <a href='/logout'>Logout</a>" return f"<a href='/login'>Login</a>"
if __name__ == '__main__': webview.create_window('Flask example', app) webview.start()

Check against our reference implementation. Launch the application to test this flow, using the following.

python app.py

Once the desktop application is launched, click on Login. You will be redirected to your browser, where you can login with Google. Google should then redirect you to the Glitch URL above, where you'll be prompted to open a deeplink. At this point, the deeplink is not yet functional.

We'll follow the same process we did in How a Python-only desktop app works. Add some requirements to your requirements.txt.

desktoplogin/requirements.txt

# test with Python 3.11.4 Flask==2.3.2 simple-flask-google-login==0.3.0 pywebview==4.2.2
py2app==0.28.6 qtpy==2.3.1 pyqt6==6.5.1 PyQt6-WebEngine==6.5.0

Then, install all requirements.

pip install -r requirements.txt

Define a configuration file for building your desktop application. The below file includes the following:

Save the following in setup.py.

desktoplogin/setup.py

from setuptools import setup

OPTIONS = {
    'plist': {
        'CFBundleURLTypes': [  # for custom URL schemes (i.e., deeplinks)
            {
                'CFBundleURLName': 'MyApplication',  # arbitrary
                'CFBundleURLSchemes': ['myapp'],  # deeplink will be myapp://
            },
        ],
    },
}

setup(
    app=['app.py'],  # your main application
    options={'py2app': OPTIONS},
    setup_requires=['py2app', 'pywebview', 'qtpy', 'pyqt6', 'PyQt6-WebEngine'],
    data_files=['client_secrets.json'],
)

In your app.py, underneath your existing imports, define a custom application with an event handler. For now, simply print the deeplink.

desktoplogin/app.py

from flask import Flask, session from simple_flask_google_login import SimpleFlaskGoogleLogin import webview import webbrowser
import sys from qtpy.QtWidgets import QApplication from qtpy.QtCore import QEvent import webview.platforms.qt # needed for us to define a custom application class MyApp(QApplication): def event(self, event: QEvent): # override the event method if event.type() == QEvent.Type.FileOpen: # filter the File Open event print(event.url()) return super(QApplication, self).event(event)
def url_handler(url): webbrowser.open(url) return f"If you aren't redirected automatically, click <a href='{url}' target='_blank'>here</a>"

Instantiate your custom application, and change webview's backend to be qt.

desktoplogin/app.py

if 'name' in session: return f"Hello {session['name']}! <a href='/logout'>Logout</a>" return f"<a href='/login'>Login</a>"
if __name__ == '__main__': application = MyApp(sys.argv) webview.create_window('Flask example', app) webview.start(gui='qt')

Check against our reference implementation. Let's now build our desktop application. To build the desktop application, use the py2app command.

python setup.py py2app -A

Move the built application to the ~/Applications folder.

mv ./dist/app.app /Applications

Open the application once, normally. Run the following to do so, with logs printed to your console.

/Applications/app.app/Contents/MacOS/app

Now, you can test your deeplink by opening myapp://whatever_you_like_here. You can open this in your browser, or by typing the following in your terminal on Mac.

open myapp://whatever_you_like_here

Either way, you should see the deeplink being printed in your console, like the following.

PyQt6.QtCore.QUrl('myapp://whatever_you_want_here')

With our deeplink now working, let's finish integrating and handling the deeplink.

Now, expose the window globally, so that we can reference and redirect this window in our event handler.

desktoplogin/app.py

if 'name' in session: return f"Hello {session['name']}! <a href='/logout'>Logout</a>" return f"<a href='/login'>Login</a>"
if __name__ == '__main__': application = MyApp(sys.argv) window = webview.create_window('Flask example', app) webview.start(gui='qt')

Now, in your event handler, perform the following two steps:

  1. Check the login code was passed in the deeplink.
  2. If so, ask the application to redirect the main webview to the login token handler, just as our previous in-app example worked.

desktoplogin/app.py

from qtpy.QtWidgets import QApplication from qtpy.QtCore import QEvent import webview.platforms.qt
class MyApp(QApplication): def event(self, event: QEvent): if event.type() == QEvent.Type.FileOpen: url = event.url() if 'code=' in url.query(): window.load_url(f"/login/callback?{url.query()}") return super(QApplication, self).event(event)
def url_handler(url): webbrowser.open(url) return f"If you aren't redirected automatically, click <a href='{url}' target='_blank'>here</a>"

This now completes our login flow. Check your source code against our reference implementation. Open the application to try the flow.

open /Applications/app.app

With the application now open, you can once again click on the Login button to start the login process.

icon

Now, you'll be sent to the browser to login normally via Google.

icon

From Google, you'll then be redirected to your intermediate webpage at Glitch, where you'll see the prompt to open the deeplink in your desktop application.

icon

Once you follow the deeplink, your application will then complete the login flow by extracting and sending the login token to your Flask app. At that point, your Flask endpoint will complete login. Just like before, once your login is successful, you should see a confirmation screen like the below.

icon

Success! At this point, you've now successfully written a desktop application supporting third-party login, using a secure and convenient flow for users to follow.

Conclusion

In this post, you've covered how third-party login for desktop works, in a secure and convenient method for users. You've also leveraged what we learned previously in browser-based Python-only development of desktop applications.

You should now have a solid foundation in both understanding third-party login flows, as well as an understanding of the implementation. For the source code for all steps, as well as the finished product, see my "Guide to Hacking" repository.


back to Guide to Hacking



  1. We use pythonw instead, as this executable allows Python libraries to hook into the GUI event loop. In other words, your Python desktop application can now capture keyboard inputs and mouse clicks. The vanilla venv tool that ships with Python doesn't have this issue. 

  2. According to SO, PyQt does not accept self-signed certificates like ours. To sidestep this, it's technically possible to force acceptance by ignoring the certificate error. To do this, you would monkey patch webview.platforms.qt.QWebPage to handle and ignore certificate errors using error.ignoreCertificateError(). With that said, in my attempt to do this, the error was successfully ignored but the webview was blank. 

  3. In the Oauth flow for third-party login, the redirect URL must use the https protocol. Fortunately for us, our redirect URL is now a Glitch intermediate webpage that indeed has the https protocol. As a result, we satisfy this requirement. Given the requirement is satisfied, we can demote our local server from https to http. This ensures that the PyQt webview can load our local web server without issues.