Authentication using OAuth

    TIP

    For a custom web-service you could also use the standard HTTP authentication for example by using the 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:

    In this section, we will go through an example of OAuth integration using the Spotify API (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.

    First, you will need to create a dedicated app on the .

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

    image

    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:

    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.

    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. QOAuth2AuthorizationCodeFlow m_oauth2;
    30. bool m_isAuthenticated;
    31. };
    32. #endif // SPOTIFYAPI_H

    First, we’ll import the 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:

    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;
    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.

    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. }

    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. void update();

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

    1. public slots:

    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. 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();