Authentication using OAuth

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.

TIP

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.

Here are some links which we find useful:

Integration example

In this section, we will go through an example of OAuth integration using the Spotify APIAuthentication using OAuth - 图5 (opens new window). 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.

Creating the App

First, you will need to create a dedicated app on the Spotify Developer’s portalAuthentication using OAuth - 图6 (opens new window).

image

Once your app is created, you’ll receive two keys: a client id and a client secret.

image

The QML file

The process is divided in two phases:

  1. The application connects to the Spotify API, which in turns requests the user to authorize it;
  2. If authorized, the application displays the list of the top ten favourite artists of the user.

Let’s start with the first step:

  1. import QtQuick
  2. import QtQuick.Window
  3. import QtQuick.Controls
  4. import Spotify
  5. ApplicationWindow {
  6. width: 320
  7. height: 568
  8. visible: true
  9. title: qsTr("Spotify OAuth2")
  10. BusyIndicator {
  11. visible: !spotifyApi.isAuthenticated
  12. anchors.centerIn: parent
  13. }
  14. SpotifyAPI {
  15. id: spotifyApi
  16. onIsAuthenticatedChanged: if(isAuthenticated) spotifyModel.update()
  17. }
  18. Component.onCompleted: {
  19. spotifyApi.setCredentials("CLIENT_ID", "CLIENT_SECRET")
  20. spotifyApi.authorize()
  21. }
  22. }

When the application starts, we will first import a custom library, Spotify, that defines a SpotifyAPI component (we’ll come to that later).

  1. import Spotify

Once the application has been loaded, this SpotifyAPI component will request an authorization to Spotify:

  1. Component.onCompleted: {
  2. spotifyApi.setCredentials("CLIENT_ID", "CLIENT_SECRET")
  3. spotifyApi.authorize()
  4. }

TIP

Please note that for security reasons, the API credentials should never be put directly into a QML file!

Until the authorization is provided, a busy indicator will be displayed in the center of the app:

  1. BusyIndicator {
  2. visible: !spotifyApi.isAuthenticated
  3. anchors.centerIn: parent
  4. }

The next step happens when the authorization has been granted. To display the list of artists, we will use the Model/View/Delegate pattern:

  1. SpotifyModel {
  2. id: spotifyModel
  3. spotifyApi: spotifyApi
  4. }
  5. ListView {
  6. visible: spotifyApi.isAuthenticated
  7. width: parent.width
  8. height: parent.height
  9. model: spotifyModel
  10. delegate: Pane {
  11. topPadding: 0
  12. Column {
  13. width: 300
  14. spacing: 10
  15. Rectangle {
  16. height: 1
  17. width: parent.width
  18. color: model.index > 0 ? "#3d3d3d" : "transparent"
  19. }
  20. Row {
  21. spacing: 10
  22. Item {
  23. width: 20
  24. height: width
  25. Rectangle {
  26. width: 20
  27. height: 20
  28. anchors.top: parent.top
  29. anchors.right: parent.right
  30. color: "black"
  31. Label {
  32. anchors.centerIn: parent
  33. font.pointSize: 16
  34. text: model.index + 1
  35. color: "white"
  36. }
  37. }
  38. }
  39. Image {
  40. width: 80
  41. height: width
  42. source: model.imageURL
  43. fillMode: Image.PreserveAspectFit
  44. }
  45. Column {
  46. Label { text: model.name; font.pointSize: 16; font.bold: true }
  47. Label { text: "Followers: " + model.followersCount }
  48. }
  49. }
  50. }
  51. }
  52. }
  53. }

The model SpotifyModel is defined in the Spotify library. To work properly, it needs a SpotifyAPI:

  1. SpotifyModel {
  2. id: spotifyModel
  3. spotifyApi: spotifyApi
  4. }

The ListView displays a vertical list of artists. An artist is represented by a name, an image and the total count of followers.

SpotifyAPI

Let’s now get a bit deeper into the authentication flow. We’ll focus on the SpotifyAPI class, a QML_ELEMENT defined on the C++ side.

  1. #ifndef SPOTIFYAPI_H
  2. #define SPOTIFYAPI_H
  3. #include <QtCore>
  4. #include <QtNetwork>
  5. #include <QtQml/qqml.h>
  6. #include <QOAuth2AuthorizationCodeFlow>
  7. class SpotifyAPI: public QObject
  8. {
  9. Q_OBJECT
  10. QML_ELEMENT
  11. Q_PROPERTY(bool isAuthenticated READ isAuthenticated WRITE setAuthenticated NOTIFY isAuthenticatedChanged)
  12. public:
  13. SpotifyAPI(QObject *parent = nullptr);
  14. void setAuthenticated(bool isAuthenticated) {
  15. if (m_isAuthenticated != isAuthenticated) {
  16. m_isAuthenticated = isAuthenticated;
  17. emit isAuthenticatedChanged();
  18. }
  19. }
  20. bool isAuthenticated() const {
  21. return m_isAuthenticated;
  22. }
  23. QNetworkReply* getTopArtists();
  24. public slots:
  25. void setCredentials(const QString& clientId, const QString& clientSecret);
  26. void authorize();
  27. signals:
  28. void isAuthenticatedChanged();
  29. private:
  30. QOAuth2AuthorizationCodeFlow m_oauth2;
  31. bool m_isAuthenticated;
  32. };
  33. #endif // SPOTIFYAPI_H

First, we’ll import the <QOAuth2AuthorizationCodeFlow> class. This class is a part of the QtNetworkAuth module, which contains various implementations of OAuth.

  1. #include <QOAuth2AuthorizationCodeFlow>

Our class, SpotifyAPI, will define a isAuthenticated property:

  1. Q_PROPERTY(bool isAuthenticated READ isAuthenticated WRITE setAuthenticated NOTIFY isAuthenticatedChanged)

The two public slots that we used in the QML files:

  1. void setCredentials(const QString& clientId, const QString& clientSecret);
  2. void authorize();

And a private member representing the authentication flow:

  1. QOAuth2AuthorizationCodeFlow m_oauth2;

On the implementation side, we have the following code:

  1. #include "spotifyapi.h"
  2. #include <QtGui>
  3. #include <QtCore>
  4. #include <QtNetworkAuth>
  5. SpotifyAPI::SpotifyAPI(QObject *parent): QObject(parent), m_isAuthenticated(false) {
  6. m_oauth2.setAuthorizationUrl(QUrl("https://accounts.spotify.com/authorize"));
  7. m_oauth2.setAccessTokenUrl(QUrl("https://accounts.spotify.com/api/token"));
  8. m_oauth2.setScope("user-top-read");
  9. m_oauth2.setReplyHandler(new QOAuthHttpServerReplyHandler(8000, this));
  10. m_oauth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant> *parameters) {
  11. if(stage == QAbstractOAuth::Stage::RequestingAuthorization) {
  12. parameters->insert("duration", "permanent");
  13. }
  14. });
  15. connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl);
  16. connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged, [=](QAbstractOAuth::Status status) {
  17. if (status == QAbstractOAuth::Status::Granted) {
  18. setAuthenticated(true);
  19. } else {
  20. setAuthenticated(false);
  21. }
  22. });
  23. }
  24. void SpotifyAPI::setCredentials(const QString& clientId, const QString& clientSecret) {
  25. m_oauth2.setClientIdentifier(clientId);
  26. m_oauth2.setClientIdentifierSharedKey(clientSecret);
  27. }
  28. void SpotifyAPI::authorize() {
  29. m_oauth2.grant();
  30. }
  31. QNetworkReply* SpotifyAPI::getTopArtists() {
  32. return m_oauth2.get(QUrl("https://api.spotify.com/v1/me/top/artists?limit=10"));
  33. }

The constructor task mainly consists in configuring the authentication flow. First, we define the Spotify API routes that will serve as authenticators.

  1. m_oauth2.setAuthorizationUrl(QUrl("https://accounts.spotify.com/authorize"));
  2. m_oauth2.setAccessTokenUrl(QUrl("https://accounts.spotify.com/api/token"));

We then select the scope (= the Spotify authorizations) that we want to use:

  1. m_oauth2.setScope("user-top-read");

Since OAuth is a two-way communication process, we instanciate a dedicated local server to handle the replies:

  1. m_oauth2.setReplyHandler(new QOAuthHttpServerReplyHandler(8000, this));

Finally, we configure two signals and slots.

  1. connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl);
  2. connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged, [=](QAbstractOAuth::Status status) { /* ... */ })

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.

  1. void SpotifyAPI::authorize() {
  2. m_oauth2.grant();
  3. }

Finally, the getTopArtists() calls the web api using the authorization context provided by the m_oauth2 network access manager.

  1. QNetworkReply* SpotifyAPI::getTopArtists() {
  2. return m_oauth2.get(QUrl("https://api.spotify.com/v1/me/top/artists?limit=10"));
  3. }

The Spotify model

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.

  1. #ifndef SPOTIFYMODEL_H
  2. #define SPOTIFYMODEL_H
  3. #include <QtCore>
  4. #include "spotifyapi.h"
  5. QT_FORWARD_DECLARE_CLASS(QNetworkReply)
  6. class SpotifyModel : public QAbstractListModel
  7. {
  8. Q_OBJECT
  9. QML_ELEMENT
  10. Q_PROPERTY(SpotifyAPI* spotifyApi READ spotifyApi WRITE setSpotifyApi NOTIFY spotifyApiChanged)
  11. public:
  12. SpotifyModel(QObject *parent = nullptr);
  13. void setSpotifyApi(SpotifyAPI* spotifyApi) {
  14. if (m_spotifyApi != spotifyApi) {
  15. m_spotifyApi = spotifyApi;
  16. emit spotifyApiChanged();
  17. }
  18. }
  19. SpotifyAPI* spotifyApi() const {
  20. return m_spotifyApi;
  21. }
  22. enum {
  23. NameRole = Qt::UserRole + 1,
  24. ImageURLRole,
  25. FollowersCountRole,
  26. HrefRole,
  27. };
  28. QHash<int, QByteArray> roleNames() const override;
  29. int rowCount(const QModelIndex &parent) const override;
  30. int columnCount(const QModelIndex &parent) const override;
  31. QVariant data(const QModelIndex &index, int role) const override;
  32. signals:
  33. void spotifyApiChanged();
  34. void error(const QString &errorString);
  35. public slots:
  36. void update();
  37. private:
  38. QPointer<SpotifyAPI> m_spotifyApi;
  39. QList<QJsonObject> m_artists;
  40. };
  41. #endif // SPOTIFYMODEL_H

This class defines a spotifyApi property:

  1. Q_PROPERTY(SpotifyAPI* spotifyApi READ spotifyApi WRITE setSpotifyApi NOTIFY spotifyApiChanged)

An enumeration of Roles (as per QAbstractListModel):

  1. enum {
  2. NameRole = Qt::UserRole + 1, // The artist's name
  3. ImageURLRole, // The artist's image
  4. FollowersCountRole, // The artist's followers count
  5. HrefRole, // The link to the artist's page
  6. };

A slot to trigger the refresh of the artists list:

  1. public slots:
  2. void update();

And, of course, the list of artists, represented as JSON objects:

  1. public slots:
  2. QList<QJsonObject> m_artists;

On the implementation side, we have:

  1. #include "spotifymodel.h"
  2. #include <QtCore>
  3. #include <QtNetwork>
  4. SpotifyModel::SpotifyModel(QObject *parent): QAbstractListModel(parent) {}
  5. QHash<int, QByteArray> SpotifyModel::roleNames() const {
  6. static const QHash<int, QByteArray> names {
  7. { NameRole, "name" },
  8. { ImageURLRole, "imageURL" },
  9. { FollowersCountRole, "followersCount" },
  10. { HrefRole, "href" },
  11. };
  12. return names;
  13. }
  14. int SpotifyModel::rowCount(const QModelIndex &parent) const {
  15. Q_UNUSED(parent);
  16. return m_artists.size();
  17. }
  18. int SpotifyModel::columnCount(const QModelIndex &parent) const {
  19. Q_UNUSED(parent);
  20. return m_artists.size() ? 1 : 0;
  21. }
  22. QVariant SpotifyModel::data(const QModelIndex &index, int role) const {
  23. Q_UNUSED(role);
  24. if (!index.isValid())
  25. return QVariant();
  26. if (role == Qt::DisplayRole || role == NameRole) {
  27. return m_artists.at(index.row()).value("name").toString();
  28. }
  29. if (role == ImageURLRole) {
  30. const auto artistObject = m_artists.at(index.row());
  31. const auto imagesValue = artistObject.value("images");
  32. Q_ASSERT(imagesValue.isArray());
  33. const auto imagesArray = imagesValue.toArray();
  34. if (imagesArray.isEmpty())
  35. return "";
  36. const auto imageValue = imagesArray.at(0).toObject();
  37. return imageValue.value("url").toString();
  38. }
  39. if (role == FollowersCountRole) {
  40. const auto artistObject = m_artists.at(index.row());
  41. const auto followersValue = artistObject.value("followers").toObject();
  42. return followersValue.value("total").toInt();
  43. }
  44. if (role == HrefRole) {
  45. return m_artists.at(index.row()).value("href").toString();
  46. }
  47. return QVariant();
  48. }
  49. void SpotifyModel::update() {
  50. if (m_spotifyApi == nullptr) {
  51. emit error("SpotifyModel::error: SpotifyApi is not set.");
  52. return;
  53. }
  54. auto reply = m_spotifyApi->getTopArtists();
  55. connect(reply, &QNetworkReply::finished, [=]() {
  56. reply->deleteLater();
  57. if (reply->error() != QNetworkReply::NoError) {
  58. emit error(reply->errorString());
  59. return;
  60. }
  61. const auto json = reply->readAll();
  62. const auto document = QJsonDocument::fromJson(json);
  63. Q_ASSERT(document.isObject());
  64. const auto rootObject = document.object();
  65. const auto artistsValue = rootObject.value("items");
  66. Q_ASSERT(artistsValue.isArray());
  67. const auto artistsArray = artistsValue.toArray();
  68. if (artistsArray.isEmpty())
  69. return;
  70. beginResetModel();
  71. m_artists.clear();
  72. for (const auto artistValue : qAsConst(artistsArray)) {
  73. Q_ASSERT(artistValue.isObject());
  74. m_artists.append(artistValue.toObject());
  75. }
  76. endResetModel();
  77. });
  78. }

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.

  1. auto reply = m_spotifyApi->getTopArtists();
  2. connect(reply, &QNetworkReply::finished, [=]() {
  3. reply->deleteLater();
  4. if (reply->error() != QNetworkReply::NoError) {
  5. emit error(reply->errorString());
  6. return;
  7. }
  8. const auto json = reply->readAll();
  9. const auto document = QJsonDocument::fromJson(json);
  10. Q_ASSERT(document.isObject());
  11. const auto rootObject = document.object();
  12. const auto artistsValue = rootObject.value("items");
  13. Q_ASSERT(artistsValue.isArray());
  14. const auto artistsArray = artistsValue.toArray();
  15. if (artistsArray.isEmpty())
  16. return;
  17. beginResetModel();
  18. m_artists.clear();
  19. for (const auto artistValue : qAsConst(artistsArray)) {
  20. Q_ASSERT(artistValue.isObject());
  21. m_artists.append(artistValue.toObject());
  22. }
  23. endResetModel();
  24. });

The data() method extracts, depending on the requested model role, the relevant attributes of an Artist and returns as a QVariant:

  1. if (role == Qt::DisplayRole || role == NameRole) {
  2. return m_artists.at(index.row()).value("name").toString();
  3. }
  4. if (role == ImageURLRole) {
  5. const auto artistObject = m_artists.at(index.row());
  6. const auto imagesValue = artistObject.value("images");
  7. Q_ASSERT(imagesValue.isArray());
  8. const auto imagesArray = imagesValue.toArray();
  9. if (imagesArray.isEmpty())
  10. return "";
  11. const auto imageValue = imagesArray.at(0).toObject();
  12. return imageValue.value("url").toString();
  13. }
  14. if (role == FollowersCountRole) {
  15. const auto artistObject = m_artists.at(index.row());
  16. const auto followersValue = artistObject.value("followers").toObject();
  17. return followersValue.value("total").toInt();
  18. }
  19. if (role == HrefRole) {
  20. return m_artists.at(index.row()).value("href").toString();
  21. }

image