Starting January 4, 2021, Google is blocking all single sign-on (SSO) requests using OAuth 2.0, when they come from an app’s embedded web view. This matters to you, if your native app used to show a web view to load the Google login screen, and then it used the Google cookies to operate. That flow is no longer allowed and you need to find an alternative. Here we describe how to do Google SSO with Qt.
We are going to talk about three different topics:
- What Qt provides in terms of OAuth 2.0 support
- How to use Qt functionality to authenticate against Google
- How to make your native app talk to an embedded web view
Let’s get started! Along the way we will include plenty of code samples to make you can adapt the solution to your case.
Native Qt support for OAuth 2.0 flows
Google announced this change well in advance.. But if you’re caught by surprise, your first instinct would be to look at the support provided by Qt for this and other similar OAuth backends. In fact, there is a module added in Qt 5.8 called QNetworkAuth which helps with this very purpose. There is even a blog post on connecting your Qt application with Google Services using OAuth 2.0. Further, if you’d rather learn by example, you can take a look at the code for the sample Reddit and Twitter.
Limitations of the documented solutions
The provided and well documented code samples are a great start. However, they have some inherent limitations that you may be running into. If you’re reading this article, it’s perhaps because you are on the same boat. Here are some of the more common problems:
- The Qt example suggests you use the first URL provided by Google as your redirection URI. If you do that, you app is probably not even hearing back from your browser.
- Let’s assume you managed to get your app to respond after the OAuth flow. You may still be getting a mysterious “qoauth2authorizationcodeflow.cpp => Unexpected call” error message when you call
grant()
. Also a “Error transferring https://oauth2.googleapis.com/token – server replied: Bad Request”. And worst of all, this is all happening inside Qt without any practical way to debug your code. - In a perfect world, you have the actual OAuth flow working. Now you have to make your embedded web view invoke C++ code in your app. That’s how your app learns that the user clicked the “sign in with Google” button and wants to login. Interestingly, the modern way to embed web views in Qt using
QWebEngineView
does not supportpostMessage
calls. Cocoa and other frameworks do, so we need a different way to interact with the embedded web page.
In the remainder of this post, we discuss these problems one by one. We include code samples that you can adapt for your own purposes.
How to implement Google Sign-In using QOAuth2AuthorizationCodeFlow
If you’re starting with the code samples provided by Qt, you’re off to a good start. But you will probably run into the first two problems described above. Let’s narrow down the scope of our work and solve one issue at a time. I recommend adding a button in your interface to invoke the Google SSO code. That way you don’t have to deal with capturing the event from your web view, for now. Once we are able to complete a SSO flow, you can remove the button. Then we will complete the interaction by capturing events from the web view. But let’s keep it simple for now.
Anatomy of an OAuth2 authorization flow
This is what is supposed to happen when your user wants to authenticate to your app using Google:
- The user clicks a “Google login” button in your app.
- In response, your app opens a web browser window in your computer, pointing to a Google URL.
- At the same time, your app opens a temporary web server listening on a custom port of your choice. Use a port number higher than 1024 to avoid needing superuser permission to run your app. In this document, we use port 1234 because, why not.
- The OS may ask the user for confirmation that it’s OK to let your app listen for incoming connections. Hopefully the user will approve that!
- The user completes the login authorization on the web browser. They tell Google that they trust your app, and then the user is redirected to a local URL as described earlier.
- Your app’s temporary web server detects that request which contains a login code from Google.
- The next hidden step is the exchange of a SSO
code
for a login token, done internally by Qt. - If all goes well your app has a token that can be used to access whatever Google APIs you had defined in the requested scopes.
What to do if your app doesn’t hear back from Google
The first problem you may face is that your app does not notice when the login token was granted. If you follow the Qt example, you may be using the first URI that you get from the Google Developers console. That is, your SSO flow uses a redirection URL similar to urn:ietf:wg:oauth:2.0:oob
. This might or might not work depending on your system. Instead, it is more reliable to use http://127.0.0.1:1234/
which points to your own machine. Note that this is preferable to http://localhost:1234/
because there is no need for a DNS resolution step. We want to eliminate as many potential pitfalls from our code. Also, make sure you keep the / at the end, because without that, some users report errors.
I like to avoid over-engineering my code at all costs. To keep things simple, we can add some simple definitions as follows:
#define CLIENT_ID "YOUR-CLIENT-ID" #define CLIENT_SECRET "YOUR-CLIENT-SECRET" #define AUTH_URI "https://accounts.google.com/o/oauth2/auth" #define TOKEN_URI "https://oauth2.googleapis.com/token" //#define REDIRECT_URI "urn:ietf:wg:oauth:2.0:oob" //#define REDIRECT_URI "http://localhost:1234/" #define REDIRECT_URI "http://127.0.0.1:1234/"
What if my app gets a login code, but fails to get a token
Once you make the above changes to your app, it will likely hear back from Google. If not, look at the Qt console because there may be some errors like:
qt.networkauth.oauth2: Unexpected call
qt.networkauth.replyhandler: Error transferring https://oauth2.googleapis.com/token - server replied: Bad Request
This happens all inside of the Qt module, and technically you could trace the steps by loading the Qt sources. But perhaps you have some experience with these 400 errors. Your intuition might be that there’s some encoding problem. Indeedd the issue here is that the code
Google sends your way is url-encoded. We need to decode the string before you can utilize it in any other context. This issue is particularly insidious because, depending factors including luck, you may sometimes get a valid code
.
To address this problem consistently and reliably, use setModifyParametersFunction
to intercept and modify the code
parameter as shown:
this->google->setModifyParametersFunction([](QAbstractOAuth::Stage stage, QVariantMap* parameters) { // Percent-decode the "code" parameter so Google can match it if (stage == QAbstractOAuth::Stage::RequestingAccessToken) { QByteArray code = parameters->value("code").toByteArray(); (*parameters)["code"] = QUrl::fromPercentEncoding(code); } });
This way, Google will recognize the login code against their records, and your app will get a login token.
Show me the authentication code
Now, let’s put it all together showing our code. Here is an abridged version of our code, starting with the header:
// GoogleSSO.h #include <QNetworkReply> #include <QOAuth2AuthorizationCodeFlow> class GoogleSSO : public QObject { Q_OBJECT public: GoogleSSO(QObject *parent=nullptr); virtual ~GoogleSSO(); public slots: void authenticate(); signals: void gotToken(const QString& token); private: QOAuth2AuthorizationCodeFlow * google; };
…and then the implementation, with some commented-out code left in place for demonstration purposes:
// GoogleSSO.cpp #include "googlesso.h" #include <QString> #include <QDir> #include <QUrl> #include <QOAuthHttpServerReplyHandler> #include <QDesktopServices> // Get these from https://console.developers.google.com/apis/credentials #define CLIENT_ID "YOUR-CLIENT-ID" #define CLIENT_SECRET "YOUR-CLIENT-SECRET" #define AUTH_URI "https://accounts.google.com/o/oauth2/auth" #define TOKEN_URI "https://oauth2.googleapis.com/token" //#define REDIRECT_URI "urn:ietf:wg:oauth:2.0:oob" //#define REDIRECT_URI "http://localhost" //#define REDIRECT_URI "http://localhost:1234/" #define REDIRECT_URI "http://127.0.0.1:1234/" GoogleSSO::GoogleSSO(QObject *parent) : QObject(parent) { this->google = new QOAuth2AuthorizationCodeFlow(this); this->google->setScope("email"); connect(this->google, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl); const QUrl authUri(AUTH_URI); const auto clientId = CLIENT_ID; const QUrl tokenUri(TOKEN_URI); const auto clientSecret(CLIENT_SECRET); const QUrl redirectUri(REDIRECT_URI); const auto port = static_cast<quint16>(redirectUri.port()); this->google->setAuthorizationUrl(authUri); this->google->setClientIdentifier(clientId); this->google->setAccessTokenUrl(tokenUri); this->google->setClientIdentifierSharedKey(clientSecret); this->google->setModifyParametersFunction([](QAbstractOAuth::Stage stage, QVariantMap* parameters) { // Percent-decode the "code" parameter so Google can match it if (stage == QAbstractOAuth::Stage::RequestingAccessToken) { QByteArray code = parameters->value("code").toByteArray(); (*parameters)["code"] = QUrl::fromPercentEncoding(code); } }); QOAuthHttpServerReplyHandler* replyHandler = new QOAuthHttpServerReplyHandler(port, this); this->google->setReplyHandler(replyHandler); connect(this->google, &QOAuth2AuthorizationCodeFlow::granted, [=](){ const QString token = this->google->token(); emit gotToken(token); // Alternatively, just use the token for your purposes // auto reply = this->google->get(QUrl("https://people.googleapis.com/v1/{resourceName=people/me}")); // connect(reply, &QNetworkReply::finished, [reply](){ // qInfo() << reply->readAll(); // }); }); } GoogleSSO::~GoogleSSO() { delete this->google; } // Invoked externally to initiate void GoogleSSO::authenticate() { this->google->grant(); }
At this point you should have a working authentication flow. But if your previous experience relied on an embedded web view, you need to somehow connect your web view with your app. That is what we are going to discuss in the following section.
How to invoke C++ code from a webview embedded in a Qt app
The straightforward, old school way to have a native app interact with an embedded web view was to let it operate independently. Then, when needed, use its QNetworkManager, or borrow its cookies into your own QNetworkCookieJar for more flexibility. However, we can’t authenticate solely using the embedded web view. Instead, the web view needs to communicate with the native app to indicate that the user wants to login. In other words, we need to connect the “Login with Google” button with the native, C++ code of our app.
What about capturing a postMessage call?
In other frameworks like Cocoa it’s possible to bubble up a JavaScript window.postMessage
to the containing iframe, and that way it’s easy for the web view to communicate with your app. However when using Qt, this is not currently supported, and instead we need to use web channels, as follows.
QWebChannel to the rescue
The way QWebChannel interacts with a web view is by publishing an object that can be accessed from the web page, and therefore invoked as a regular JavaScript object, therefore opening a communication channel between the JavaScript and native/C++ codes.
The following code shows how to expose a webobj
object to your web view so you can invoke it via JavaScript:
QWebEngineView* wv = prepareWebview(); QWebChannel *channel = new QWebChannel(this); channel->registerObject("webobj", this); wv->page()->setWebChannel(channel); wv->load(url);
For this to work, Qt provides a qwebchannel.js file that you must load prior to attempting to communicate with the channel object. Once loaded, we can access the exposed objects from the JavaScript side, invoke its methods, and in general, communicate with the app.
The following code demonstrates this idea:
<script src="path/to/qwebchannel.js"></script> <script> var googleSignInButton = document.getElementById('googleSignIn'); if (googleSignInButton) { googleSignInButton.onclick = function () { new QWebChannel(qt.webChannelTransport, function(channel) { var webobj = channel.objects.webobj; window.webobj = webobj; webobj.googleSignIn(); // Run it just the once, after installing it } ); // Alternatively, run later // webobj.googleSignIn(); }; } </script>
The code above does several things in just a few lines:
- It first looks for an HTML button with id=”googleSignIn”.
- Then it adds an
onclick
handler to install the QWebChannel object and make it accessible to rest of the JavaScript code. - Right at that moment, it invokes the
googleSignIn()
method of the exposed object. Alternatively, you can install the handler at load time, but this is a lighter flow in case the user does NOT want to invoke your SSO flow.
Conclusion
If you followed all the way here, you should have everything you need to adapt your Qt app to the new restrictions of Google’s SSO system. If you have any questions, you can use our contact form so that one of our friendly team members gets back to you promptly.
This post is a departure from our typical content in order to share some of the technical work we do at Appfluence. We expect to keep posting content like this whenever we have something interesting to say.