OAuth is an open protocol to allow secure authorization in a simple and standard method from web, mobile, and desktop applications. OAuth is used to authenticate a client against common web-services such as Google, Facebook, and Twitter.
For a custom web-service you could also use the standard HTTP authentication for example by using the XMLHttpRequest username and password in the get method (e.g. xhr.open(verb, url, true, username, password))
OAuth is currently not part of a QML/JS API. So you would need to write some C++ code and export the authentication to QML/JS. Another issue would be the secure storage of the access token.
In this section, we will go through an example of OAuth integration using the Spotify API. This example uses a combination of C++ classes and QML/JS. To discover more on this integration, please refer to Chapter 16.
This application's goal is to retrieve the top ten favourite artists of the authenticated user.
First, we'll import the <QOAuth2AuthorizationCodeFlow> class. This class is a part of the QtNetworkAuth module, which contains various implementations of OAuth.
#include <QOAuth2AuthorizationCodeFlow>
Our class, SpotifyAPI, will define a isAuthenticated property:
The constructor task mainly consists in configuring the authentication flow. First, we define the Spotify API routes that will serve as authenticators.
The first one configures the authorization to happen within a web-browser (through &QDesktopServices::openUrl), while the second makes sure that we are notified when the authorization process has been completed.
The authorize() method is only a placeholder for calling the underlying grant() method of the authentication flow. This is the method that triggers the process.
This class is a QML_ELEMENT that subclasses QAbstractListModel to represent our list of artists. It relies on SpotifyAPI to gather the artists from the remote endpoint.
An enumeration of Roles (as per QAbstractListModel):
enum {
NameRole = Qt::UserRole + 1, // The artist's name
ImageURLRole, // The artist's image
FollowersCountRole, // The artist's followers count
HrefRole, // The link to the artist's page
};
A slot to trigger the refresh of the artists list:
public slots:
void update();
And, of course, the list of artists, represented as JSON objects:
public slots:
QList<QJsonObject> m_artists;
On the implementation side, we have:
#include "spotifymodel.h"
#include <QtCore>
#include <QtNetwork>
SpotifyModel::SpotifyModel(QObject *parent): QAbstractListModel(parent) {}
QHash<int, QByteArray> SpotifyModel::roleNames() const {
static const QHash<int, QByteArray> names {
{ NameRole, "name" },
{ ImageURLRole, "imageURL" },
{ FollowersCountRole, "followersCount" },
{ HrefRole, "href" },
};
return names;
}
int SpotifyModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return m_artists.size();
}
int SpotifyModel::columnCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return m_artists.size() ? 1 : 0;
}
QVariant SpotifyModel::data(const QModelIndex &index, int role) const {
Q_UNUSED(role);
if (!index.isValid())
return QVariant();
if (role == Qt::DisplayRole || role == NameRole) {
return m_artists.at(index.row()).value("name").toString();
}
if (role == ImageURLRole) {
const auto artistObject = m_artists.at(index.row());
const auto imagesValue = artistObject.value("images");
Q_ASSERT(imagesValue.isArray());
const auto imagesArray = imagesValue.toArray();
if (imagesArray.isEmpty())
return "";
const auto imageValue = imagesArray.at(0).toObject();
return imageValue.value("url").toString();
}
if (role == FollowersCountRole) {
const auto artistObject = m_artists.at(index.row());
const auto followersValue = artistObject.value("followers").toObject();
return followersValue.value("total").toInt();
}
if (role == HrefRole) {
return m_artists.at(index.row()).value("href").toString();
}
return QVariant();
}
void SpotifyModel::update() {
if (m_spotifyApi == nullptr) {
emit error("SpotifyModel::error: SpotifyApi is not set.");
return;
}
auto reply = m_spotifyApi->getTopArtists();
connect(reply, &QNetworkReply::finished, [=]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit error(reply->errorString());
return;
}
const auto json = reply->readAll();
const auto document = QJsonDocument::fromJson(json);
Q_ASSERT(document.isObject());
const auto rootObject = document.object();
const auto artistsValue = rootObject.value("items");
Q_ASSERT(artistsValue.isArray());
const auto artistsArray = artistsValue.toArray();
if (artistsArray.isEmpty())
return;
beginResetModel();
m_artists.clear();
for (const auto artistValue : qAsConst(artistsArray)) {
Q_ASSERT(artistValue.isObject());
m_artists.append(artistValue.toObject());
}
endResetModel();
});
}
The update() method calls the getTopArtists() method and handle its reply by extracting the individual items from the JSON document and refreshing the list of artists within the model.
auto reply = m_spotifyApi->getTopArtists();
connect(reply, &QNetworkReply::finished, [=]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit error(reply->errorString());
return;
}
const auto json = reply->readAll();
const auto document = QJsonDocument::fromJson(json);
Q_ASSERT(document.isObject());
const auto rootObject = document.object();
const auto artistsValue = rootObject.value("items");
Q_ASSERT(artistsValue.isArray());
const auto artistsArray = artistsValue.toArray();
if (artistsArray.isEmpty())
return;
beginResetModel();
m_artists.clear();
for (const auto artistValue : qAsConst(artistsArray)) {
Q_ASSERT(artistValue.isObject());
m_artists.append(artistValue.toObject());
}
endResetModel();
});
The data() method extracts, depending on the requested model role, the relevant attributes of an Artist and returns as a QVariant:
if (role == Qt::DisplayRole || role == NameRole) {
return m_artists.at(index.row()).value("name").toString();
}
if (role == ImageURLRole) {
const auto artistObject = m_artists.at(index.row());
const auto imagesValue = artistObject.value("images");
Q_ASSERT(imagesValue.isArray());
const auto imagesArray = imagesValue.toArray();
if (imagesArray.isEmpty())
return "";
const auto imageValue = imagesArray.at(0).toObject();
return imageValue.value("url").toString();
}
if (role == FollowersCountRole) {
const auto artistObject = m_artists.at(index.row());
const auto followersValue = artistObject.value("followers").toObject();
return followersValue.value("total").toInt();
}
if (role == HrefRole) {
return m_artists.at(index.row()).value("href").toString();
}